引子
前面介绍过乐观锁、悲观锁。传送门:Mysql乐观锁探究,Mysql悲观锁探究
回头思考一个问题,为什么会有悲观锁、乐观锁?它究竟为了解决什么问题而产生?
记得读高中时候,有一个数学老师说过(该数学老师人不高,还有点微胖,大家都亲切的称之为"大叔",还很喜欢打篮球;一晃都好多年没回去过了。唉,估计人老了,一想往事就收不住了,眼里进了鳄鱼。言归正传,他说过一个名句):如果做题的时候,不知道怎么解,就回到定义、公式!
现在做技术的都感觉的到,各种新技术层出不穷,各种语言百花齐放,业务场景也是千差万别,但是要解决的问题本质其实变化不大。并发问题一直是程序员要解决的和碰到的问题。
看一下场景
- 现在的很多电商网站,都是分布式服务甚至微服务,即进行了服务的拆分,比如用户系统、订单系统、支付系统、库存系统等等。系统间用RPC或webservice调用进行交互。一个用户购买商品,首先订单系统下单,进行了库存的扣减(假设库存系统事务提交,进行了真实的扣减),然后系统挂了或者网络抖动导致没有返回响应给订单系统,订单系统这时候怎么办?是直接当做失败还是进行返回用户成功然后重试?
- 给用户发送通知短信,如果不做幂等控制,很有可能造成重复发送导致投诉
幂等的概念
这里为什么会提到幂等定义呢?因为很多系统交互都是通过接口调用,如果一个接口被反复调用,结果不变,就认为接口具有幂等性。比如上面的例子,如果订单系统进行重试,如果不保证幂等,可能会造成库存重复扣减导致错误。那怎么判断请求是否幂等呢,这就是涉及到幂等号!在请求时,指定一个字段用来判断请求是否幂等。
幂等概念(来源于网上):幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。 在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数,SQL语句中的select语句也是天然幂等操作,不管查询多少次,结果都不会变。更复杂的操作幂等保证是利用唯一交易号(流水号)实现
解决方案
幂等号(流水号)
在设计接口的时候,尤其是涉及到外部的交互,都是需要考虑到幂等控制的。这种一般是通过在接口层面增加幂等请求字段,让调用方可以手动指定。
/** 业务订单号,幂等使用*/
private String bizNo;
整体的系统交互如下
- 系统A调用第一次系统B的接口methodB,并传入一个幂等字段bizNo,值为bizNo1
- 系统B受到该请求,先根据bizNo判断是否处理过该请求,发现没有处理过,执行业务处理,并返回结果
- 系统再次调用系统B的接口methodB,并传入相同的幂等字段值bizNo1,系统B根据bizNo1判断依据处理过了,则不进行业务执行,直接返回上一次执行结果
上面就是一次幂等控制的流程:会发现有一步很关键,就是幂等判断。那么一般这种幂等怎么判断呢?可以把bizNo单独存储到数据库一个字段上,放到业务数据里面;每次请求过来,先用bizNo取出记录,然后判断记录处理过,如果成功处理过就直接返回,不再重复处理。这样就避免了重复处理造成的数据不一致问题。
消息重复
现在很多系统一般会用到mq,用消息进行系统解耦及提高系统吞吐量。但是mq有一个严重问题,就是消息重复投递的问题。因此,消息消费者必须保证消息消费的幂等性,否则就可能出现数据不一致的状态。
消息队列产品一般会明确其 “消息投递” 的服务质量保证 (QoS),常见的质量等级包括以下三种:
- 至少投递一次 (At-least-once)
- 最多投递一次 (At-most-once)
- 严格只投递一次 (Exactly-once)
最常见的服务质量保证是 “至少投递一次 (At-least-once)”,一般消息流程时序图如下所示:
针对同一个消息对象,消息消费者 (Subscriber/ Consumer) 可能收到一次或者多次;收到多次的场景可能由以下几种原因导致:
- 网络异常,导致消息消费者未收到消息对象。
- 消息消费者进程宕机,导致未收到消息对象。
- 消息消费者收到消息对象,但处理耗时过长,例如超过 10 秒。
- 消息消费者收到消息对象并正常处理,但回执 ACK 丢失。
- 消息消费者收到消息对象,处理过程抛出异常或者主动通知消息队列 broker 无法正常消费消息。
比如上面的例子,如果是mq消息交互,不做幂等控制,订单系统发起重试,可能就会造成库存重复扣减。对于一般的消息体,可能如下:
messageId = "1e4cf860680dad989dceec17f1adb837"
TOPIC = "TEST_TRADE"
eventcode = "TEST_ORDER"
payload = Account("200XXX", "800080001","60.00","2018-07-30 12:00:00,000", "operator")
那么针对这种消息可以这么做幂等控制呢?
- 一种直观的解决方案是把消息存储下来,根据messageId标识这个消息是否处理过。
- 或者根据业务主键比如订单号orderId确认该比订单是否处理过。
可以很容易判断,用业务主键是一种更优的方式:如果用messageId判断,需要额外的存在消息,数据库需要频繁的插入查询非业务数据,更重要的是messageId 相同的消息对象,业务数据对象一定是相同的;但 messageId 不同的消息对象,业务数据对象也可能是相同的,这取决于消息生产者的实现逻辑。
所以,对于 “至少投递一次 (At-least-once)” 的消息投递服务质量,保证消息消费幂等性是必须的,不是可选的。
其它场景
可以把上面的消息及api视为一种场景,即幂等处理。但是还有一些其它的场景,通过防止并发从而避免产生幂等性问题
- 悲观锁:通过获取数据的时候加锁获取,就不再具体介绍了,参考上面Mysql悲观锁探究
- 乐观锁:Mysql乐观锁探究
- 分布式锁:如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供);
- 状态机幂等:上面的消息重复处理可以视为应用了状态机幂等,即一条业务数据是有状态的,比如初始化-》处理中-》成功/失败