基于Guava、RocketMQ的事件主线
前言
人间四月芬芳尽,产品测试一体化。2020必将是被裁入史册的一年,为了应对疫情,我司某某部门顺应开源节流号召,推出产品测试一体化体系,好多测试小伙伴纷纷下岗。回归正题,去年在老大推动下,开始采用领域驱动的方式进行开发,领域驱动有一个很重要的概念是:领域事件。领域事件是为了解耦代码,对于传统的MVC 3层结构,个人觉得也可以合理使用事件。对于事件,Spring事件、Guava事件等,但是这2种对于严格要求幂等性的场景不在适用,服务一旦宕机、重启,jvm内存中的事件将不复存在。RocketMQ作为一个成熟的消息中间件,借助于RocketMQ可以持久化事件,但是若使用RocketMQ应对不同的事件,可能要用不同的Topic或Tag,十分麻烦。显然Spring事件、Guava事件这种基于类类型的订阅机制十分灵活。因此,笔者结合了GuavaEvent和RocketMQ两者的各自优点,封装了一个事件总线,并应用于线上项目。
架构
如果你看过GuavaEvent的源码,你会发现,其实现机制就是发布订阅模式。如果事件监听器要想收到事件,必须注册到EventBus中。
图中GuavaEventBus、GuavaAsyncEventBus其实就是基于Gauva中EventBus的二次封装。但是MqEventBus与前2者不同,它其实是发送了一个包含对象类型的MQ消息,消息订阅器收到消息后,会解析为对应的Java对象,然后在用GuavaEventBus发送一个同步事件,借助Guava实现事件分发。
代码实现
为了省去注册监听器的步骤,项目的中Listener可以继承AbstractEventListener,AbstractEventListener会在后置处理方法中注册实现类自身到3个EventBus中,AbstractEventListener的代码如下:
public class AbstractEventListener implements IEventListener {
/**
* MqEventBus依赖Spring容器中Bean,这里显式注入,防止MqEventBus空指针异常
*/
@Autowired
private MqEventBus mqEventBus;
@Override
public void afterPropertiesSet() throws Exception {
GuavaEventBus.registerListener(this);
GuavaAsyncEventBus.registerListener(this);
MqEventBus.registerListener(this);
}
@Override
public void destroy() throws Exception {
GuavaEventBus.unregisterListener(this);
GuavaAsyncEventBus.unregisterListener(this);
MqEventBus.unregisterListener(this);
}
}
MqEventBus发送了一个包含对象类型的MQ消息,MqEventSubscriber订阅事件,转换为Guava同步事件发送。代码如下:
@Override
public void post(IEvent event) {
Objects.requireNonNull(event, "event can't be null");
String content = JSON.toJSONString(event, new SerializerFeature[]{SerializerFeature.WriteClassName});
if (log.isDebugEnabled()) {
log.debug("[{}] send event, content:[{}]", this.getClass().getName(), content);
}
SendCallback callback = new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("[{}] send event success, content:[{}], send result[{}]:",
this.getClass().getName(), content, sendResult);
}
@Override
public void onException(Throwable throwable) {
log.error("[{}] send event fail, content:[{}], error:",
this.getClass().getName(), content, throwable);
}
};
rocketMQTemplate.asyncSendOrderly(carpEventBusProperties.getTopic(),
content, event.getEventKey(), callback);
}
//-------------------------------------------------------------------------------
@Override
public void onMessage(String msg) {
log.info("MqEventSubscriber receive msg:[{}]", msg);
if (StringUtils.isBlank(msg)) {
log.warn("MqEventSubscriber receive msg, but msg is blank");
return;
}
try {
Object data = JSON.parse(msg,config);
if(data instanceof IEvent){
GuavaEventBus.send((IEvent) data);
}else {
log.error("MqEventSubscriber ignore not event type msg , context:[{}]",msg);
}
} catch (Exception e) {
log.error("Msg:[] parse to event or send event fail, error:", msg, e);
}
}
改进计划
笔者在司编写的是基于阿里云版本的RocketMQ,可以支持精度为秒级的任意时间的延时消息。因此,在司封装的一套代码是支持延时事件的,适用场景:发送一个订单超时事件,监听到事件后系统自动取消订单操作。而且这种需要延时的场景也是比较多见的,但是,RocketMQ开源版本只支持18个精度等级的延时,不能满足大部分业务场景。后续,笔者可能基于HashedWheelTimer+Redis实现可持久化的时间轮,用于实现任意延时的事件。
改进
HashedWheelTimer的TimerTask很难序列化到Redis,要想Redis持久化Task必须对Netty的HashedWheelTimer进行大量的改造。笔者实现任意时间延时的事件机制原理是:
1)找到18个延时等级中与实际延时最接近且比实际延时小的作为延时Level
2)若收到延时事件后,发现事件没有到达延时事件点,重复1);若到达延时时间点,则进行消费
使用
笔者将组件以jar包的形式上传到了maven中央仓库。由于现在企业级项目基本上都是spring boot或spring cloud,所以jar包以starter的形式进行了封装,传统MVC项目应该也可以使用,但是可能配置麻烦一些,因此,这里以spring boot为例,进行说明。
1. 引入依赖
pom中引入附件中依赖,carp-eventbus-starter会引入rocketmq-spring-boot-starter等相关依赖,如有冲突,请自行解决。
2. 配置
在application.yml添加如下配置:
rocketmq:
//配置注册服务nameserver
name-server: carp-dev:9876
carp:
eventbus:
rocketmq:
//发送或消费事件的Group
group: GID_carp_eventbus
//发送或消费事件的Topic
topic: Topic_carp_eventbus
3. 编写代码
1)定义一个事件
//自定义定义一个事件,需要实现IEvent接口
@Data
public class MqEvent implements IEvent {
private static final long serialVersionUID = 8905941578121623522L;
private String orderNo;
private BigDecimal price;
private Date createTime;
private Integer count;
@Override
public String getEventKey() {
return IEvent.uuid();
}
}
2)编写一个监听器
@Slf4j
@Component
public class TestEventListener extends AbstractEventListener {
//监听事件
@Subscribe
public void subMqEvent(MqEvent event) {
log.info("Subscribe order event:{}", event);
}
}
3)编写一个controller
@Api(value = "rmq")
@RestController
@RequestMapping("/rmq/eventbus")
public class EventBusController {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@ApiOperation(tags = "eventbus", value = "发送MQ异步事件")
@PostMapping("/mq/event/send")
public Object testSendEvent() {
MqEvent event = new MqEvent();
event.setOrderNo("9999999966661111");
event.setCreateTime(new Date());
event.setPrice(new BigDecimal(999));
event.setCount(10);
//发送MQ事件
MqEventBus.send(event, 10L);
return "ok";
}
}
4)测试发送一个事件
附件
1. 依赖
<dependency>
<groupId>com.github.rxyor</groupId>
<artifactId>carp-eventbus-starter</artifactId>
<version>1.0.14.12</version>
</dependency>
2. 源码
3. 微信
CH—You