围观:基于事件机制的内部解耦之心路历程

每篇文章都有属于它本身的故事,没有故事的文章是没有灵魂的文章。而我就是这个灵魂摆渡人。git

主人公张某某,这边不方便透露姓名,就叫小张吧。小张在一家小型的互联网创业团队中就任。github

职位是Java后端开发,因此总体和业务代码打交道在所不免。数据库

以前有个搜索相关的需求,并且数量量也算比较大,就采用了ElasticSearch来作搜索。初版因为时间比较赶,作的比较粗糙。越到后面发现代码越难写下去了,主要是在更新索引数据的场景没处理好,才有了今天的故事。后端

基础入门

Spring Event

Spring的事件就是观察者设计模式,一个任务结束后须要通知任务进行下一步的操做,就可使用事件来驱动。设计模式

在Spring中使用事件机制的步骤以下:bash

  • 自定义事件对象,继承 ApplicationEvent
  • 自定义事件监听器,实现 ApplicationListener 或者经过 @EventListener 注解到方法上实现监听
  • 自定义发布者,经过 applicationContext.publishEvent()发布事件

Spring Event在不少开源框架中都有使用案例,好比Spring Cloud中的Eureka里面就有使用微信

event包架构

图片

定义Eventapp

图片

发布Event框架

图片

Guava EventBus

EventBus是Guava的事件处理机制,在使用层面和Spring Event差很少。这里不作过多讲解,今天主要讲Spring Event。

业务背景

全部的数据都会有一个定时任务去同步数据到ElasticSearch中,业务中直接从ElasticSearch查询数据返回给调用方。

之因此把全部数据都存入ElasticSearch是为了方便,若是只存储搜索的字段,那么搜索出来后就还须要去数据库查询其余信息进行组装。

就是因为全部数据都会存储ElasticSearch中,因此当这些数据发生变动的时候咱们须要去刷新ElasticSearch中的数据,这个就是咱们今天文章的核心背景。

假设咱们ElasticSearch中的数据是文章信息,也就是咱们常常看的技术文章,这个文章中存储了访问量,点赞量,评论量等信息。

当这些动做发生的时候,都须要去更新ElasticSearch的数据才行,咱们默认的操做都是更新数据库中的数据,ElasticSearch是由定时任务去同步的,同步会有周期,作不到毫秒别更新。

实现方案-倔强青铜

倔强青铜就是在每一个会涉及到数据变动的地方,去手动调用代码进行数据的刷新操做,弊端在于每一个地方都要去调用,这仍是简单的场景,有复杂的业务场景,一个业务操做可能会涉及到不少数据的刷新,也就是须要调用不少次,模拟代码以下:

// 浏览
public void visit() {
	articleIndexService.reIndex(articleId);
    XXXIndexService.reIndex(articleId);
    ........
}
	
// 评论
public void comment() {
	articleIndexService.reIndex(articleId);
}
复制代码

实现方案-秩序白银

倔强青铜的弊端在于不解耦,并且是同步调用,若是在事务中会加长事务的时间。因此咱们须要一个异步的方案来执行重建索引的逻辑。

通过你们激烈的讨论,而项目也是以Spring Boot为主,因此选择了Spring Event来做为异步方案。

定义一个重建文章索引的Event,代码以下:

public class ArticleReIndexEvent extends ApplicationEvent {
	private String id;
	
	public ArticleReIndexEvent(Object source, String id) {
		super(source);
		this.id = id;
	}
	
	public String getId() {
		return id;
	}
	
}
复制代码

而后写一个EventListener来监听事件,进行业务逻辑处理,代码以下:

@Component
public class MyEventListener {
	
	@EventListener
	public void onEvent(ArticleReIndexEvent event) {
		System.out.println(event.getId());
	}
}
复制代码

使用的地方只须要发布一个Event就能够,这个动做默认是同步的,若是咱们想让这个操做不会阻塞,变成异步只须要在@EventListener上面再增长一个@Async注解。

// 浏览
public void visit() {
	applicationContext.publishEvent(new ArticleReIndexEvent(this, articleId));
	applicationContext.publishEvent(new XXXReIndexEvent(this, articleId));
}
	
// 评论
public void comment() {
	applicationContext.publishEvent(new ArticleReIndexEvent(this, articleId));
}
复制代码

实现方案-荣耀黄金

秩序白银的方案在代码层面确实解耦了,可是使用者发布事件须要关注的点太多了,也就是我改了某个表的数据,我得知道有哪些索引会用到这张表的数据,我得把这些相关的事件都发送出去,这样数据才会异步进行刷新。

当业务复杂后或者有新来的同事,不是那么的了解业务,压根不可能知道说我改了这个数据对其余那些索引有影响,因此这个方案仍是有优化的空间。

荣耀黄金的方案是将全部的事件都统一为一个,而后在事件里加属性来区分修改的数据是哪里的。每一个数据须要同步变动的索引都有本身的监听器,去监听这个统一的事件,这样对于发布者来讲我只须要发送一个事件告诉你,我这边改数据了,你要不要消费,要不要更新索引我并不关心。

定义一个数据表发生修改的事件,代码以下:

public class TableUpdateEvent extends ApplicationEvent {
	private String table;
	private String id;
	
	public TableUpdateEvent(Object source, String id, String table) {
		super(source);
		this.id = id;
		this.table = table;
	}
	
	public String getId() {
		return id;
	}
	
	public String getTable() {
		return table;
	}
	
}
复制代码

而后每一个索引都须要消费这个事件,只须要关注这个索引中数据的来源表有没有变更,若是有变更则去刷新索引。

好比索引A的数据都是article表中过来的,因此只要是article表中的数据发生了变动,索引A都要作对应的处理,因此索引A的监听器只须要关注article表有没有修改便可。

@Component
public class MyEventListener {
	
	private List<String> consumerTables = Arrays.asList("article");
	
	@Async
	@EventListener
	public void onEvent(TableUpdateEvent event) {
		System.out.println(event.getId() + "\t" + event.getTable());
		if (consumerTables.contains(event.getTable())) {
			System.out.println("消费本身关注的表数据变更,而后处理。。。");
		}
	}
	
}
复制代码

好比索引B的数据是从comment和comment_reply两个表中过来的,因此只要是comment和comment_reply两个表的数据发生了变动,索引B都须要作对应的处理,因此索引B的监听器只须要关注comment和comment_reply两个表有没有修改便可。

@Component
public class MyEventListener2 {
	
	private List<String> consumerTables = Arrays.asList("comment", "comment_replay");
	
	@Async
	@EventListener
	public void onEvent(TableUpdateEvent event) {
		System.out.println(event.getId() + "\t" + event.getTable());
		if (consumerTables.contains(event.getTable())) {
			System.out.println("消费本身关注的表数据变更,而后处理。。。");
		}
	}
	
}
复制代码

实现方案-尊贵铂金

荣耀黄金的方案已经很完美了,代码解耦不说,使用者关注点也少了,不容易出错。

但还有一个致命的问题就是全部涉及到业务修改的方法中,得手动往外发送一个事件,从代码解耦的场景来讲还残留了一点瑕疵,至少仍是有那么一行代码来发送事件。

尊贵铂金的方案将彻底解耦,不须要写代码的时候手动去发送事件。咱们将经过订阅MySql的binlog来统一发送事件。

binlog是MySQL数据库的二进制日志,用于记录用户对数据库操做的SQL语句信息,MySQL的主从同步也是基于binlog来实现的,对于咱们这种数据异构的场景再合适不过了。

binlog订阅的方式有不少种,开源的框架通常都是用canal来实现。

canal:github.com/alibaba/can…

若是你买的云数据库,像ALI云就有dts数据订阅服务,跟canal同样。

以后的方案图以下:

图片

实现方案-永恒钻石

没有什么方案和架构是永恒的,跟着业务的变动而演进,符合当前业务的需求才是王道。越后面考虑的东西越多,毕竟最后是要升级到最强王者的,哈哈。

感兴趣的能够关注下个人微信公众号 猿天地,更多技术文章第一时间阅读。个人GitHub也有一些开源的代码 github.com/yinjihuan

相关文章
相关标签/搜索