每年支付宝在双11和双12的活动中,都展示了绝佳的技术能力。这个能力不但体现在处理高TPS量的访问,更体现在几乎不会出错,不会出现重复支付的情况,那这个是怎么做到的呢?
诚然,为了实现在高并发下仍不会出错的技术目标,支付宝下了很多功夫,比如幂等性的处理,分布式事务的使用等等,但是个人觉得其中最关键的一点就是“一锁二判三更新”这句看似毫不起眼的口诀。
何为“一锁二判三更新”? 简单来说就是当任何一个并发请求过来的时候
1. 我们先锁定关联单据
2. 然后判断关联单据状态,是否之前已经更新过对应状态了
3. 如果基于第2步判断,之前并没有请求更新过对应状态,则本次请求可以更新并完成相关业务逻辑。
如果之前已经有更新过状态了,则本次不能更新,也不能完成业务逻辑。
示意图
话不多说,我们直接上代码:
//第1步锁当前支付单
PaymentInfo resultPaymentInfo = commonPayCoreService
.queryPaymentForUpdate(createPaymentInfo.getId());
if (resultPaymentInfo.isFinalStatus()) {
//第2步,判断当前支付单状态,如果是终态,则直接返回
//不做任何更新
return resultPaymentInfo;
}
//第3步更新当前支付单状态到终态,并完成相关业务逻辑(支付成功)
payCoreService.updateRequestResult(payChannelResult);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
基于以上方案可以100%确保在并发情况下不会出现重复更新问题,按理论来说,就是每次状态机变更前,都要在并发安全情况下判断状态是否已经发生过变更了。
如果第1步或第2步缺失了,会发生什么问题,我们来看一下:
第1步缺失
第2步缺失
只要把这3步作为我们的代码规范,则可以避免大部分的并发重复操作问题。对于异步并发重复消息的处理亦是如此,加深对状态机的判断后还可以处理消息乱序问题。
对于锁的使用可根据实际情况选择悲观锁和乐观锁。
关于悲观锁(数据库行锁),乐观锁(数据库版本锁或分布式锁)的实现方式和坑我们以后再详细说。
可能有人会问不管是悲观锁还是乐观锁对系统的并发量都是有影响的,这个怎么解决?我的观点是在现代分布式系统中,如果追求高可用和稳定则必须在方案上优先满足,对于性能可以通过优化代码逻辑,优化技术架构,扩展数据库资源等方式来解决。
在之前蚂蚁金服的压测中,我负责的结算系统内部有10次左右SQL调用以及一次远程调用(约花费100ms),总流程花费180ms左右。在一台4核8G的机器上压测,java服务并发可以达到150TPS,结果还是令人满意的,通过水平服务器扩展完全没有问题。
在整个支付宝技术架构中,只有一个场景是没有用锁和判断直接更新的,就是2016年的春节五福红包,高达上百万的TPS访问,为了保证用户的顺畅体验,牺牲了状态判断的安全性,在事后再做一次对账(虽然就算出错也于事无补了 :))