RPC【幂等性】
一、定义
幂等性:一次操作与多次操作产生的结果相同。
二、业务场景
RPC场景中,因为重试机制或者没有实现幂等性而导致重复数据问题,需要引起重视。比如,可能会导致一次购买,创建多笔订单,一条通知信息被发送多次等问题。
调用失败时程序没有显示重试,按理不会产生重复数据的问题,但是RPC框架在集群容错机制中自动进行了重试。
消费者调用RPC,最熟悉业务,做一RPC框架只要提供一些策略供消费者选择即可。
三、如何做重试
以BUBBO为例,它阿里的一款分布式服务框架,或者说是RPC框架,提供了如下集群容错策略供消费者选择。
3.1、Failover【故障转移策略】【默认策略】
默认策略,当消费发生异常时,通过负载均衡策略再选择一个生产者节点进行调用,直到达到重试次数。
3.2、Failfast【快速失败策略】
消费者只消费一次服务,当发生异常时则直接抛出。
3.3、Failsafe【安全失败策略
】
消费者只消费一次服务,如果消费失败则包装一个空结果,不抛出异常。
3.4、Failback【异步重试策略】
消费发生异常时返回一个空结果,失败请求将会进行异步重试。如果重试超过最大重试次数还不成功,放弃重试并不抛出异常。
3.5、Forking【并行调用策略】
消费者通过线程池并发调用多个生产者,只要有一个成功就算成功。
3.6、Broadcast【广播调用策略】
消费者遍历调用所有生产者节点,任何一个出现异常则抛出异常。
四、如何做幂等
RPC框架自带的重试机制可能会造成数据重复问题,那么在使用中必须考虑幂等性。幂等性是指一次操作与多次操作产生结果相同,并不会因为多次操作而产生不一致性。常见幂等方案有取消重试、幂等表、数据库锁、状态机这些方案。
4.1、取消重试
有两种办法:设置重试次数为零,选择不重试的集群容错策略。
<!-- 设置重试次数为零 --> <dubbo:reference id="helloService" interface="com.java.front.dubbo.demo.provider.HelloService" retries="0" /> <!-- 选择集群容错方案 --> <dubbo:reference id="helloService" interface="com.java.front.dubbo.demo.provider.HelloService" cluster="failfast" />
4.2、幂等表
假设用户支付成功后,支付系统将支付成功消息,发送至消息队列。物流系统订阅到这个消息,准备为这笔订单创建物流单。
但是消息队列可能会重复推送,物流系统有可能接收到多次这条消息。我们希望达到效果:无论接收到多少条重复消息,只能创建一笔物流单。
解决方案是幂等表方案。新建一张幂等表,该表就是用来做幂等,无其它业务意义,有一个字段名为key建有唯一索引,这个字段是幂等标准。
物流系统订阅到消息后,首先尝试插入幂等表,订单编号作为key字段。如果成功则继续创建物流单,如果订单编号已经存在则违反唯一性原则,无法插入成功,说明已经进行过业务处理,丢弃消息。
这张表数据量会比较大,我们可以通过定时任务对数据进行归档,例如只保留7天数据,其它数据存入归档表。
还有一种广义幂等表就是我们可以用Redis替代数据库,在创建物流单之前,我们可以检查Redis是否存在该订单编号数据,同时可以为这类数据设置7天过期时间。
4.3、状态机
物流单创建成功后会发送消息,订单系统订阅到消息后更新状态为完成,假设变更是将订单状态0更新至状态1。订单系统也可能收到多条消息,可能在状态已经被更新至状态1之后,依然收到物流单创建成功消息。
解决方案是状态机方案。首先绘制状态机图,分析状态流转形态。例如经过分析状态1已经是最终态,那么即使接收到物流单创建成功消息也不再处理,丢弃消息。
4.4、数据库锁
数据库锁又可以分为悲观锁和乐观锁两种类型,悲观锁是在获取数据时加锁:
select * from table where col='xxx' for update
乐观锁是在更新时加锁,第一步首先查出数据,数据包含version字段。第二步进行更新操作,如果此时记录已经被修改则version字段已经发生变化,无法更新成功:
update table set xxx, version = #{version} + 1 where id = #{id} and version = #{version}