基于Guava、RocketMQ的事件主线

基于Guava、RocketMQ的事件主线

前言

人间四月芬芳尽,产品测试一体化。2020必将是被裁入史册的一年,为了应对疫情,我司某某部门顺应开源节流号召,推出产品测试一体化体系,好多测试小伙伴纷纷下岗。回归正题,去年在老大推动下,开始采用领域驱动的方式进行开发,领域驱动有一个很重要的概念是:领域事件。领域事件是为了解耦代码,对于传统的MVC 3层结构,个人觉得也可以合理使用事件。对于事件,Spring事件、Guava事件等,但是这2种对于严格要求幂等性的场景不在适用,服务一旦宕机、重启,jvm内存中的事件将不复存在。RocketMQ作为一个成熟的消息中间件,借助于RocketMQ可以持久化事件,但是若使用RocketMQ应对不同的事件,可能要用不同的Topic或Tag,十分麻烦。显然Spring事件、Guava事件这种基于类类型的订阅机制十分灵活。因此,笔者结合了GuavaEvent和RocketMQ两者的各自优点,封装了一个事件总线,并应用于线上项目。

架构

image.png
如果你看过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)测试发送一个事件

image.png

附件

1. 依赖
<dependency>
  <groupId>com.github.rxyor</groupId>
  <artifactId>carp-eventbus-starter</artifactId>
  <version>1.0.14.12</version>
</dependency>
2. 源码

https://github.com/rxyor/carp

3. 微信

CH—You

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值