针对近期处理的两例并发场景引发的「互斥性」「幂等性」问题进行过程分析、输出系统方案。
阅读本文前,我想重申一个观点: 在相对复杂的工业代码中(绕来绕去,不是简单的一个函数调用),排查「简单问题」也不是一件简单的事。
背景
近期处理租赁合同工单、报警,发现有些接口在并发、调用重试、用户连击等情况下的存在幂等性、数据一致性问题。
本文借由一些问题实例,来系统性分析解决幂等性、数据一致性问题,同时针对这类问题做一下系统性的梳理。
问题案例
案例 | 影响 |
---|---|
问题1:已签约合同状态瞬时回退到起草状态 | 下游服务收到消息乱序。 |
问题2:重复写入 biz_record | 合同详情报错,获取合同完整数据处操作完全阻塞。 |
具体的排查、分析过程放在内网工单处理过程wiki。
本文我们核心目标是从上往下审视系统设计,以此来完成「理论指导实践」的落地,具体的代码细节不做过多展示(脱敏)。
首先对问题逐个进行问题核心原因的分析。
问题1
剖析
以问题2为例,租赁合同场景,系统主要用户:
- 经纪人
- 业客(业主+租客)
签约过程的核心状态机:
其中盖章由经纪人在我们PC后台、App端完成。业客完成签字后,合同状态从盖章流转到签约状态。
此时由于业客已经完成了签字动作,合同状态正常只能往后流转,即过户(物业交割)。
真实场景下,如果合同确实存在变更(业客变更、条款变更),则应走变更流程。变更单提交并且通过审核后,合同状态才会回到起草,从头开始作业。
而问题1案例中,根据下游服务的RD反馈,他们连续收到了两条消息:
- 2024-03-04 19:31:27.296 合同已签约
- 2024-03-04 19:31:27.467 合同起草
经过细致的对日志+源码进行排查分析,我最终成功还原了当时并发的场景,如图:
简单描述下当时的情景:
- 首先业客首先完成了签署;
- 状态更新的时刻:Mar 4, 2024 @ 19:31:27.290 随后事务提交;
- 随后分布式锁释放;
- 然后在稍后的同一时刻,经纪人修改了合同;
- 接口进入时刻:Mar 4, 2024 @ 19:31:27.277 此时业客完成签署的状态事务还未提交;
- 此时首先开启事务并查询了合同状态,查到的状态为旧的【已盖章】;
- 接着进行了一段较耗时的dubbo调用,调用收到响应的时刻 2024-03-04T19:31:27.361 ,对比上面的时间,可以发现此时业客签署已经处理完毕;
- 接着获取分布式锁(由于业客签署的接口已经执行完释放了锁,因此此处可正常获取到锁);
- 在锁范围内进行一系列业务校验、业务更新;
前人此处考虑到了多个业务流间保证操作的互斥性,但是部分代码迭代后没处理好锁的范围。
前人想做到的效果图例:
一句话总结问题:使用了分布式锁,意图想控制业客签署、修改合同两个业务流的互斥访问,但是在锁外先做了状态判断,导致锁内更新数据错误。
此处使用分布式锁不当,导致两个应该互斥访问的操作隔离性不足。
问题1核心是未保证多个互斥接口间的并发访问,从而导致数据一致性出错。
解决
问题核心原因是在获取锁之前就判断了业务状态,两个接口间未保证同步性(访问互斥性)。
仅需保证先获取锁,在锁内进行数据状态的获取与判断,问题即可解决。
解决口诀:一锁、二判、三更新。
问题2
剖析
问题2是一个典型的幂等性问题。
由于代码脱敏原因,针对并发场景,我们使用图例还原下并发错误时的过程:
一句话总结下问题:有分布式锁,但是没锁全。导致两个先后到达的请求可并发进入内部方法 sendMessageToUser
,并发时,此方法使用事务无法保证幂等性。
因此最后写入了多条 biz_record
数据,正常情况一个合同应该只有一条 biz_record
。
解决
问题核心是分布式锁有,但是没锁全整个过程。
因此只要保证锁住整个方法,问题即可解决。
解决口诀:一锁、二判、三更新。
总结
处理完上述两个问题,我们总结下共性。
问题1:多个应当保证访问互斥的接口间,使用了分布式锁,但是在锁外先判断了业务状态,导致锁内拿到的其实是过期数据。
问题2:接口应当保证幂等性,使用了分布式锁,但是未锁住需保证幂等的完整范围。
可以看到,在这两类问题中,我们都只要做到:在需要保证访问互斥、幂等的接口上使用悲观锁对完整的代码范围加锁,同时需遵循【先加锁后做判断与更新】原则,问题都能解决。
使用分布式锁确保业务互斥性的正反例对比:
使用分布式锁确保接口幂等性的正反例对比: )