消息队列的消费幂等性如何保证
什么是幂等?
任意多次执行所产生的影响均与一次执行的影响相同就可以称为幂等。
什么是消息幂等?
当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响。
为什么我们要保证幂等性,不保证幂等性,会不会有问题?
这个问题其实没法准确回答。回答这个问题的根源得从业务场景上进行分析。比如正常业务情况下,我们是不允许同个订单重复支付,这种业务场景我们就需要确保幂等性。再比如日志记录,这种业务场景,我们可能就不需要做幂等判断。
因此是否要保证幂等性,得基于业务进行考量
消息队列的消费幂等性如何保证?
没法保证
。前面说了要保证幂等性,得基于业务场景进行考量。消息队列他本身就不是给你用来做业务幂等性用的。如果你要实现业务幂等性,靠消息队列是没法帮你完成的,你自己得根据自身业务场景,来实现幂等。
常用的业务幂等性保证方法
1、利用数据库的唯一约束实现幂等
比如将订单表中的订单编号设置为唯一索引,创建订单时,根据订单编号就可以保证幂等
2、去重表
这个方案本质也是根据数据库的唯一性约束来实现。其实现大体思路是:首先在去重表上建唯一索引,其次操作时把业务表和去重表放在同个本地事务中,如果出现重现重复消费,数据库会抛唯一约束异常,操作就会回滚
3、利用redis的原子性
每次操作都直接set到redis里面,然后将redis数据定时同步到数据库中
4、多版本(乐观锁)控制
此方案多用于更新的场景下。其实现的大体思路是:给业务数据增加一个版本号属性,每次更新数据前,比较当前数据的版本号是否和消息中的版本一致,如果不一致则拒绝更新数据,更新数据的同时将版本号+1
5、状态机机制
此方案多用于更新且业务场景存在多种状态流转的场景
【注】网络底层设计:链接管理用到很多状态机转换机制。
6、token机制
生产者发送每条数据的时候,增加一个全局唯一的id,这个id通常是业务的唯一标识,比如订单编号。在消费端消费时,则验证该id是否被消费过,如果还没消费过,则进行业务处理。处理结束后,在把该id存入redis,同时设置状态为已消费。如果已经消费过了,则不进行处理。
总结
消息队列没法帮你做到消费端的幂等性,消费端的幂等性得基于业务场景进行实现。不过消息队列必须得保证消息不能丢,至少保证被消费一次,不然消息都丢了,没数据搞啥业务幂等。在实现消费端处理业务时,要确保消费端是采用手工确认应答机制,而不是自动应答机制。这样能够确保消费端一旦业务处理失败,生产者还能再次发送同个消息给消费端
FAQ
RTM即时聊天(信令)系统幂等性的重要性
上下行消息(常用rpc或者nginx代理)
如果客户端发送失败,可能的场景:
客户端发送成功了,只是没有收到服务端的应答; 客户端重新发送,如果没设计成幂等性,就会出现后端存储相同的2条或N条消息; —— 所以设计成幂等性非常有必要,发送请求携带ID号;后端接收到重复的ID号,在写队列时业务丢弃或在写库时通过数据库过滤掉重复ID的消息数据;
网络原因导致客户端发送失败,后端没法收到消息; 客户端重发机制:重发1条消息,携带相同ID号,后端服务接收;
下行消息(websocket push)
push成功,但因为链路中某个环节异常,没有收到ACK消息; 服务端重发,如果没设计成幂等性,就会出现客户端收到相同的2条或N条消息;如果同时要对push消息入库,也会出现入库2条消息; —— 所以幂等性设计非常有必要;
push时携带ID号:
** 在写队列时业务丢弃或在写库时通过数据库过滤掉重复ID的消息数据;
** 客户端通过移动时间窗口,过滤掉重复ID的消息数据;或者在接入侧服务端做过滤ID操作;
【注】需要重点设计移动时间窗口;
push未成功发送到客户端的场景; 服务端重新push机制:重发1条消息,携带相同ID号,后端入库,并客户端接收;