牛客网中级项目学习笔记-异步消息处理

Redis异步消息处理机制

写在前面,其实实现异步队列可以用到BlockingQueue同步队列,不过本次我们用Redis的list数据结构来作为异步机制的先进先出队列。

点赞、回复评论的时候,表面上是赞数增加了,其实还有很多其他的工作要做。比如,对方要收到消息提醒,成就值增加。一些行为会引起一系列连锁反应。如果在点赞时立马处理,会影响程序运行效率,所以大型服务需要异步化。
redis异步处理的实现(把耗时的操作异步化,让网站的操作速度更快),异步处理就是把不是很紧急的事情留在后台慢慢的更新,把紧急的数据返给前端,把业务切开,比如评论后的积分值增长,就不需要很紧急,又比如点赞操作后系统发送站内信通知被点赞了。
异步处理的框架:
牛客网中级项目学习笔记-异步消息处理_第1张图片
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同步队列代替,这里简单放张图介绍下,有时间再来实现下:
牛客网中级项目学习笔记-异步消息处理_第2张图片

你可能感兴趣的:(后端开发)