但这在分布式的场景下想找一个通用的方案几乎是不可能的 。不过如果是针对基于数据库事务的消费逻辑,实际上是可行的 。
基于关系数据库事务插入消息表假设我们业务的消息消费逻辑是:更新MySQL数据库的某张订单表的状态:
update t_order set status = 'SUCCESS' where order_no= 'order123';要实现Exaclty Once即这个消息只被消费一次(并且肯定要保证能消费一次),我们可以这样做:在这个数据库中增加一个消息消费记录表,把消息插入到这个表,并且把原来的订单更新和这个插入的动作放到同一个事务中一起提交,就能保证消息只会被消费一遍了 。
- 开启事务
- 插入消息表(处理好主键冲突的问题)
- 更新订单表(原消费逻辑)
- 提交事务
- 这时候如果消息消费成功并且事务提交了,那么消息表就插入成功了,这时候就算RocketMQ还没有收到消费位点的更新再次投递,也会插入消息失败而视为已经消费过,后续就直接更新消费位点了 。这保证我们消费代码只会执行一次 。
- 如果事务提交之前服务挂了(例如重启),对于本地事务并没有执行所以订单没有更新,消息表也没插入成功;而对于RocketMQ服务端来说,消费位点也没更新,所以消息还会继续投递下来,投递下来发现这个消息插入消息表也是成功的,所以可以继续消费 。这保证了消息不丢失 。
基于这种方式,的确这是有能力拓展到不同的应用场景,因为他的实现方案与具体业务本身无关——而是依赖一个消息表 。
但是这里有它的局限性
- 消息的消费逻辑必须是依赖于关系型数据库事务 。如果消费的消费过程中还涉及其他数据的修改,例如redis这种不支持事务特性的数据源,则这些数据是不可回滚的 。
- 数据库的数据必须是在一个库,跨库无法解决
更复杂的业务场景如上所述,这种方式Exactly Once语义的实现,实际上有很多局限性,这种局限性使得这个方案基本不具备广泛应用的价值 。并且由于基于事务,可能导致锁表时间过长等性能问题 。
例如我们以一个比较常见的一个订单申请的消息来举例,可能有以下几步(以下统称为步骤X):
- 检查库存(RPC)
- 锁库存(RPC)
- 开启事务,插入订单表(MySQL)
- 调用某些其他下游服务(RPC)
- 更新订单状态
- commit 事务(MySQL)
再者,如果在这个比较耗时的长链条场景下加入事务的包裹,将大大的降低系统的并发 。所以通常情况下,我们处理这种场景的消息去重的方法还是会使用一开始说的业务自己实现去重逻辑的方式,如前面加select for update,或者使用乐观锁 。
那我们有没有方法抽取出一个公共的解决方案,能兼顾去重、通用、高性能呢?
拆解消息执行过程其中一个思路是把上面的几步,拆解成几个不同的子消息,例如:
- 库存系统消费A:检查库存并做锁库存,发送消息B给订单服务
- 订单系统消费消息B:插入订单表(MySQL),发送消息C给自己(下游系统)消费
- 下游系统消费消息C:处理部分逻辑,发送消息D给订单系统
- 网友热议|新郎新娘同名同姓同年生 两家相距不足百米!网友:娃娃可以叫洋幂
- 杨幂婚礼的伴娘是谁?
- 十二岁女孩失踪九天最新消息 9岁失踪女孩最新进展
- |杨幂五月首封造型翻车,脖子怪异脸型回归年轻时,还不如私服好看
- 面试官竟然问我消息队列为啥会丢失消息?幸亏我总结了全套八股文
- |有一种裙子,叫杨幂的“虎皮裙”,看似普通,却不是谁都能穿好看
- 靳东|终于等到靳东新剧要播的消息了,积压了1年,阵容奢华,有剧追了
- 日照明日叶公司最新消息,明日叶健康料理
- 土豆丝|好消息!李晓霞找到新工作,杨倩瘦身成功,张常宁陪富二代郊游
- netty实现websocket客户端与服务端消息透传
