写在前面,其实实现异步队列可以用到BlockingQueue同步队列,不过本次我们用Redis的list数据结构来作为异步机制的先进先出队列。
点赞、回复评论的时候,表面上是赞数增加了,其实还有很多其他的工作要做。比如,对方要收到消息提醒,成就值增加。一些行为会引起一系列连锁反应。如果在点赞时立马处理,会影响程序运行效率,所以大型服务需要异步化。
redis异步处理的实现(把耗时的操作异步化,让网站的操作速度更快),异步处理就是把不是很紧急的事情留在后台慢慢的更新,把紧急的数据返给前端,把业务切开,比如评论后的积分值增长,就不需要很紧急,又比如点赞操作后系统发送站内信通知被点赞了。
异步处理的框架:
Biz即事件,EventProducer是把事件接过来并传入Redis的队列,EventConsumer是把事件分发出去,交由预先写好的Handler类处理。
这里介绍下将对象加入redis队列的一种方式:直接在redis里存储一个对象,通过JSON串的方式将它序列化与反序列化:
public void setObject(String key,Object obj) {
//对象缓存入redis的一种方式,值包装成JSON
set(key,JSON.toJSONString(obj));//Json将对象转化为Json字符串
}
public T getObject(String key,Class c) {//对应的取出该对象
String value=get(key);
if(value!=null) return JSON.parseObject(value, c);//JSON将Json字符串转化为相应的对象
return null;
}
redis实现异步消息处理过程:
1.首先要有需要异步的事件类型和对象:
public enum EventType {//枚举了几种可异步处理的类型
LIKE(0),
COMMENT(1),
LOGIN(2),
MAIL(3),
SCORE(4);
private int value;
EventType(int value){
this.value=value;
}
}
public class EventModel {//异步触发事件的信息保存,单向/优先队列队列里的内容
private EventType eventType;//异步事件类型,有枚举
private int actorId;//触发者
private int entityId;//触发对象
private int entityType;
private int entityOwnerId;//触发对象拥有者,拥有者会收到通知
//触发事件的其它参数或数据,触发当下有什么数据要保存下来,以后要用,用map保存,map真万能
private Map exts=new HashMap<>();
public EventModel() {
super();
// TODO Auto-generated constructor stub
}
public EventModel(EventType eventType) {
super();
this.eventType = eventType;
}
public EventModel set(String key,String value) {
//使用return this可以让代码执行链路化操作,可以直接set set set
//后面的set都改用这种方式
exts.put(key, value);
return this;
}
public String get(String key) {
return exts.get(key);
}
public EventType getEventType() {
return eventType;
}
public EventModel setEventType(EventType eventType) {
this.eventType = eventType;
return this;
}
public int getActorId() {
return actorId;
}
public EventModel setActorId(int actorId) {
this.actorId = actorId;
return this;
}
public int getEntityId() {
return entityId;
}
public EventModel setEntityId(int entityId) {
this.entityId = entityId;
return this;
}
public int getEntityType() {
return entityType;
}
public EventModel setEntityType(int entityType) {
this.entityType = entityType;
return this;
}
public int getEntityOwnerId() {
return entityOwnerId;
}
public EventModel setEntityOwnerId(int entityOwnerId) {
this.entityOwnerId = entityOwnerId;
return this;
}
public Map getExts() {
return exts;
}
public void setExts(Map exts) {
this.exts = exts;
}
}
2.其次要有生产者EventProducer把事件接过来推进redeis的异步优先队列,对了采用redis的list集合。
@Service
public class EventProducer {//事件生产者,负责把事件放进消息队列
private static final Logger logger = LoggerFactory.getLogger(EventProducer.class);
@Autowired
private JedisAdapter jedisAdapter;
public boolean fireEvent(EventModel em) {//事件接过来并放进队列
try {
String eventModel=JSONObject.toJSONString(em);//把事件序列化成字符串
String list=RedisKeyUtil.getEventQueueKey();//存入队列
jedisAdapter.lpush(list, eventModel);
return true;
}catch(Exception e) {
logger.error("事件放入队列失败:"+e.getMessage());
return false;
}
}
}
3.消费者负责监听redis队列,有事件时就取出来并根据映射关系交由指定的handler们处理,这里简单一提Jedis的brpop(timeout,key)方法,该方法是在消息队列尾阻塞地取出消息,参数0表示一直阻塞下去直到有消息传来,这样就能在多线程内开个无限循环监听队列,一有消息便可进行分发。
@Service
public class EventConsumer implements InitializingBean,ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(EventConsumer.class);
@Autowired
JedisAdapter jedisAdapter;
private ApplicationContext applicationContext;//继承ApplicationContextAware类,用于获取所有注入的handler
//事件要分发给指定handler处理,要一个映射表,映射处理事件的所有触发handler
private Map> config=new HashMap<>();
@Override
public void afterPropertiesSet() throws Exception {
// TODO Auto-generated method stub
//获取EventHandler的所有实现类
Map beans=applicationContext.getBeansOfType(EventHandler.class);
if(beans!=null) {//遍历实现类,反向填充事件类型->执行handler
for(Map.Entry entry:beans.entrySet()) {
List eventTypes=entry.getValue().getSupportsEventTypes();
for(EventType eventType:eventTypes) {
if(!config.containsKey(eventType)) {
config.put(eventType, new ArrayList());
}
config.get(eventType).add(entry.getValue());
}
}
}
Thread t= new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {//在多线程内持续监听队列等待处理事件
String key=RedisKeyUtil.getEventQueueKey();
List events=jedisAdapter.brpop(0, key);//一直阻塞直到有事件
for(String event:events) {
if(event.equals(key)) continue;//redis自带消息key要过滤掉
EventModel em=JSONObject.parseObject(event, EventModel.class);
if(!config.containsKey(em.getEventType())) {
logger.error("不能识别的事件");
continue;
}
//有了map就知道事件由哪些handler来处理
for(EventHandler handler:config.get(em.getEventType())) {
handler.doHandle(em);
}
}
}
}
});
t.start();
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// TODO Auto-generated method stub
this.applicationContext=applicationContext;
}
}
4.创建handler类,这里只实现了用户点赞资讯后的站内信通知和异常登录邮件发送。
@Component
public interface EventHandler {//标示处理异步事件的接口
//处理事件的方法,不同实现类处理方式不同
void doHandle(EventModel eventModel);
//关注某些类型,只要是这些类型就由我这个handler处理
List getSupportsEventTypes();
}
@Component
public class LikeHandler implements EventHandler {
@Autowired
private MessageService messageService;
@Autowired
private UserService userService;
@Override
public void doHandle(EventModel eventModel) {
// TODO Auto-generated method stub
int userId=eventModel.getActorId();
User user=userService.getUser(userId);
Message message=new Message();
message.setFromId(3);//可以理解为管理员通过站内信向当前用户发送的点赞信息
message.setToId(eventModel.getEntityOwnerId());
//message.setToId(eventModel.getActorId());
message.setContent("用户" + user.getName() + "赞了你的资讯"
+ ",http://127.0.0.1:8088/news/" + eventModel.getEntityId());
message.setCreatedDate(new Date());
messageService.addMessage(message);//列举了点赞行为的其中一种产生效应,发送站内信
}
@Override
public List getSupportsEventTypes() {
// TODO Auto-generated method stub
return Arrays.asList(EventType.LIKE);
}
}
@Component
public class LoginExceptionHandler implements EventHandler {
@Autowired
private MessageService messageService;
@Autowired
MailSender mailSender;
@Override
public void doHandle(EventModel eventModel) {
// TODO Auto-generated method stub
Message message=new Message();
message.setToId(eventModel.getActorId());//通过站内信向当前用户发送登录异常信息,谁登录就发给谁
message.setFromId(3);//可以理解为管理员ID
message.setContent("你上次登录的ip异常");
message.setCreatedDate(new Date());
messageService.addMessage(message);
//邮件发送通知
Map map = new HashMap<>();
map.put("username", eventModel.get("username"));
mailSender.sendWithHTMLTemplate(eventModel.get("email"),"登录异常","mails/welcome.html", map);
}
@Override
public List getSupportsEventTypes() {
// TODO Auto-generated method stub
return Arrays.asList(EventType.LOGIN);
}
}
java库里发邮件是通过引用下面的包:
javax.mail
mail
1.4.7
实现MailSender类:
@Service
public class MailSender implements InitializingBean {//邮件发送
private static final Logger logger = LoggerFactory.getLogger(MailSender.class);
private JavaMailSenderImpl mailSender;
@Autowired
private VelocityEngine velocityEngine;
public boolean sendWithHTMLTemplate(String to, String subject,
String template, Map model) {
try {
String nick = MimeUtility.encodeText("牛客中级课");
InternetAddress from = new InternetAddress("dasdas");
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage);
String result = VelocityEngineUtils
.mergeTemplateIntoString(velocityEngine, template, "UTF-8", model);
mimeMessageHelper.setTo(to);
mimeMessageHelper.setFrom(from);
mimeMessageHelper.setSubject(subject);
mimeMessageHelper.setText(result, true);
mailSender.send(mimeMessage);
return true;
} catch (Exception e) {
logger.error("发送邮件失败" + e.getMessage());
return false;
}
}
@Override
public void afterPropertiesSet() throws Exception {
mailSender = new JavaMailSenderImpl();
// 请输入自己的邮箱和密码,用于发送邮件
mailSender.setUsername("asdasd");
mailSender.setPassword("dasdas");
mailSender.setHost("smtp.exmail.qq.com");
// 请配置自己的邮箱和密码
mailSender.setPort(465);
mailSender.setProtocol("smtps");
mailSender.setDefaultEncoding("utf8");
Properties javaMailProperties = new Properties();
javaMailProperties.put("mail.smtp.ssl.enable", true);
mailSender.setJavaMailProperties(javaMailProperties);
}
}
除了用redis实现优先队列外,还可以用BlockingQueue同步队列代替,这里简单放张图介绍下,有时间再来实现下: