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


要解决上面并发场景下的消息幂等问题 , 一个可取的方案是开启事务把select 改成 select for update语句,把记录进行锁定 。
但这样消费的逻辑会因为引入了事务包裹而导致整个消息消费可能变长,并发度下降 。
当然还有其他更高级的解决方案 , 例如更新订单状态采取乐观锁,更新失败则消息重新消费之类的 。但这需要针对具体业务场景做更复杂和细致的代码开发、库表设计,不在本文讨论的范围 。
但无论是select for update,还是乐观锁这种解决方案 , 实际上都是基于业务表本身做去重 , 这无疑增加了业务开发的复杂度,一个业务系统里面很大部分的请求处理都是依赖MQ的,如果每个消费逻辑本身都需要基于业务本身而做去重/幂等的开发的话,这是繁琐的工作量 。本文希望 探索 出一个通用的消息幂等处理的方法,从而抽象出一定的工具类用以适用各个业务场景 。
在消息中间件里,有一个投递语义的概念,而这个语义里有一个叫”Exactly Once” , 即消息肯定会被成功消费,并且只会被消费一次 。以下是阿里云里对Exactly Once的解释:
在我们业务消息幂等处理的领域内,可以认为业务消息的代码肯定会被执行,并且只被执行一次,那么我们可以认为是Exactly Once 。
但这在分布式的场景下想找一个通用的方案几乎是不可能的 。不过如果是针对基于数据库事务的消费逻辑,实际上是可行的 。
假设我们业务的消息消费逻辑是:更新MySQL数据库的某张订单表的状态:
要实现Exaclty Once即这个消息只被消费一次(并且肯定要保证能消费一次),我们可以这样做:在这个数据库中增加一个消息消费记录表 , 把消息插入到这个表,并且把原来的订单更新和这个插入的动作放到同一个事务中一起提交,就能保证消息只会被消费一遍了 。
1、开启事务
2、插入消息表(处理好主键冲突的问题)
3、更新订单表(原消费逻辑)
4、提交事务
说明:
1、这时候如果消息消费成功并且事务提交了,那么消息表就插入成功了,这时候就算RocketMQ还没有收到消费位点的更新再次投递,也会插入消息失败而视为已经消费过,后续就直接更新消费位点了 。这保证我们消费代码只会执行一次 。2、如果事务提交之前服务挂了(例如重启),对于本地事务并没有执行所以订单没有更新 , 消息表也没插入成功;而对于RocketMQ服务端来说,消费位点也没更新,所以消息还会继续投递下来 , 投递下来发现这个消息插入消息表也是成功的,所以可以继续消费 。这保证了消息不丢失 。
事实上,阿里云ONS的EXACTLY-ONCE语义的实现上,就是类似这个方案基于数据库的事务特性实现的 。更多详情可参考:
基于这种方式 , 的确这是有能力拓展到不同的应用场景,因为他的实现方案与具体业务本身无关——而是依赖一个消息表 。
但是这里有它的局限性
1、消息的消费逻辑必须是依赖于关系型数据库事务 。如果消费的消费过程中还涉及其他数据的修改,例如Redis这种不支持事务特性的数据源,则这些数据是不可回滚的 。
2、数据库的数据必须是在一个库 , 跨库无法解决
注:业务上,消息表的设计不应该以消息ID作为标识,而应该以业务的业务主键作为标识更为合理,以应对生产者的重发 。阿里云上的消息去重只是RocketMQ的messageId,在生产者因为某些原因手动重发(例如上游针对一个交易重复请求了)的场景下起不到去重/幂等的效果(因消息id不同) 。

推荐阅读