如上所述,这种方式Exactly Once语义的实现,实际上有很多局限性,这种局限性使得这个方案基本不具备广泛应用的价值 。并且由于基于事务,可能导致锁表时间过长等性能问题 。
例如我们以一个比较常见的一个订单申请的消息来举例,可能有以下几步(以下统称为步骤X):
1、 检查库存(RPC)
2、 锁库存(RPC)
3、 开启事务 , 插入订单表(MySQL)
4、 调用某些其他下游服务(RPC)
5、 更新订单状态
6、 commit 事务(MySQL)
这种情况下 , 我们如果采取消息表+本地事务的实现方式,消息消费过程中很多子过程是不支持回滚的 , 也就是说就算我们加了事务,实际上这背后的操作并不是原子性的 。怎么说呢,就是说有可能第一条小在经历了第二步锁库存的时候,服务重启了,这时候实际上库存是已经在另外的服务里被锁定了,这并不能被回滚 。当然消息还会再次投递下来 , 要保证消息能至少消费一遍,换句话说 , 锁库存的这个RPC接口本身依旧要支持“幂等” 。
再者,如果在这个比较耗时的长链条场景下加入事务的包裹,将大大的降低系统的并发 。所以通常情况下,我们处理这种场景的消息去重的方法还是会使用一开始说的业务自己实现去重逻辑的方式 , 如前面加select for update,或者使用乐观锁 。
那我们有没有方法抽取出一个公共的解决方案,能兼顾去重、通用、高性能呢?
其中一个思路是把上面的几步,拆解成几个不同的子消息,例如:
1、库存系统消费A:检查库存并做锁库存,发送消息B给订单服务
2、订单系统消费消息B:插入订单表(MySQL),发送消息C给自己(下游系统)消费
3、下游系统消费消息C:处理部分逻辑,发送消息D给订单系统
4、订单系统消费消息D:更新订单状态
注:上述步骤需要保证本地事务和消息是一个事务的(至少是最终一致性的) , 这其中涉及到分布式事务消息相关的话题,不在本文论述 。
可以看到这样的处理方法会使得每一步的操作都比较原子,而原子则意味着是小事务,小事务则意味着使用消息表+事务的方案显得可行 。
然而 , 这太复杂了!这把一个本来连续的代码逻辑割裂成多个系统多次消息交互!那还不如业务代码层面上加锁实现呢 。
上面消息表+本地事务的方案之所以有其局限性和并发的短板,究其根本是因为它依赖于关系型数据库的事务,且必须要把事务包裹于整个消息消费的环节 。
如果我们能不依赖事务而实现消息的去重,那么方案就能推广到更复杂的场景例如:RPC、跨库等 。
例如 , 我们依旧使用消息表,但是不依赖事务 , 而是针对消息表增加消费状态,是否可以解决问题呢?
67_1.png
以上是去事务化后的消息幂等方案的流程 , 可以看到,此方案是无事务的,而是针对消息表本身做了状态的区分:消费中、消费完成 。只有消费完成的消息才会被幂等处理掉 。而对于已有消费中的消息 , 后面重复的消息会触发延迟消费(在RocketMQ的场景下即发送到RETRY TOPIC),之所以触发延迟消费是为了控制并发场景下,第二条消息在第一条消息没完成的过程中,去控制消息不丢(如果直接幂等 , 那么会丢失消息(同一个消息id的话),因为上一条消息如果没有消费完成的时候,第二条消息你已经告诉broker成功了,那么第一条消息这时候失败broker也不会重新投递了)
上面的流程不再细说,后文有github源码的地址,读者可以参考源码的实现,这里我们回头看看我们一开始想解决的问题是否解决了:
推荐阅读
- 视频运营跳槽直播,视频运营跳槽直播是真的吗
- 显卡芯片烧了会怎么样,显卡芯片坏了
- 怎么开发家居小程序,家居设计小程序
- 越野摩托游戏单机版,越野摩托游戏大全
- mysql怎么设置排序 mysql如何进行数据的排序
- 运城无货源电商erp系统的简单介绍
- sqlserverpercent的简单介绍
- 如何去除word的文本框,如何去除word的文本框背景
- mysql怎么加列数据 mysql 加列