mysql主键冲突怎么办 mysql主键的建立有几种方法( 四 )


1、 消息已经消费成功了,第二条消息将被直接幂等处理掉(消费成功) 。
2、 并发场景下的消息,依旧能满足不会出现消息重复,即穿透幂等挡板的问题 。
3、 支持上游业务生产者重发的业务重复的消息幂等问题 。
关于第一个问题已经很明显已经解决了,在此就不讨论了 。
关于第二个问题是如何解决的?主要是依靠插入消息表的这个动作做控制的,假设我们用MySQL作为消息表的存储媒介(设置消息的唯一ID为主键),那么插入的动作只有一条消息会成功,后面的消息插入会由于主键冲突而失败,走向延迟消费的分支 , 然后后面延迟消费的时候就会变成上面第一个场景的问题 。
关于第三个问题,只要我们设计去重的消息键让其支持业务的主键(例如订单号、请求流水号等),而不仅仅是messageId即可 。所以也不是问题 。
如果细心的读者可能会发现这里实际上是有逻辑漏洞的,问题出在上面聊到的个三问题中的第2个问题(并发场景),在并发场景下我们依赖于消息状态是做并发控制使得第2条消息重复的消息会不断延迟消费(重试) 。但如果这时候第1条消息也由于一些异常原因(例如机器重启了、外部异常导致消费失败)没有成功消费成功呢?也就是说这时候延迟消费实际上每次下来看到的都是消费中的状态,最后消费就会被视为消费失败而被投递到死信Topic中(RocketMQ默认可以重复消费16次) 。
有这种顾虑是正确的!对于此,我们解决的方法是,插入的消息表必须要带一个最长消费过期时间,例如10分钟,意思是如果一个消息处于消费中超过10分钟 , 就需要从消息表中删除(需要程序自行实现) 。所以最后这个消息的流程会是这样的:
67_2.png
我们这个方案实际上没有事务的,只需要一个存储的中心媒介,那么自然我们可以选择更灵活的存储媒介,例如Redis 。使用Redis有两个好处:
1、性能上损耗更低
2、上面我们讲到的超时时间可以直接利用Redis本身的ttl实现
当然Redis存储的数据可靠性、一致性等方面是不如MySQL的,需要用户自己取舍 。
以上方案针对RocketMQ的Java实现已经开源放到Github中,具体的使用文档可以参考,
以下仅贴一个Readme中利用Redis去重的使用样例,用以意业务中如果使用此工具加入消息去重幂等的是多么简单:
以上代码大部分是原始RocketMQ的必须代码,唯一需要修改的仅仅是创建一个DedupConcurrentListener示例,在这个示例中指明你的消费逻辑和去重的业务键(默认是messageId) 。
更多使用详情请参考Github上的说明 。
实现到这里 , 似乎方案挺完美的,所有的消息都能快速的接入去重,且与具体业务实现也完全解耦 。那么这样是否就完美的完成去重的所有任务呢?
很可惜,其实不是的 。原因很简单:因为要保证消息至少被成功消费一遍,那么消息就有机会消费到一半的时候失败触发消息重试的可能 。还是以上面的订单流程X:
1、 检查库存(RPC)
2、 锁库存(RPC)
3、 开启事务,插入订单表(MySQL)
4、 调用某些其他下游服务(RPC)
5、 更新订单状态
6、 commit 事务(MySQL)
当消息消费到步骤3的时候,我们假设MySQL异常导致失败了 , 触发消息重试 。因为在重试前我们会删除幂等表的记录,所以消息重试的时候就会重新进入消费代码 , 那么步骤1和步骤2就会重新再执行一遍 。如果步骤2本身不是幂等的 , 那么这个业务消息消费依旧没有做好完整的幂等处理 。

推荐阅读