最近在做交易订单的事情,随着系统越来越复杂,交易量越来越大,出现不一致的情况也频发。不禁引人思考,这里做一些总结记录。分布式事务有一些理论,最常听的是CAP,ACID等。这里就不做介绍了,大家自己去查资料看看。分布式事务也经常提到2PC,3PC,因为不太适用互联网场景,这里也不做介绍。
下面我们来看看一个电商典型的场景,下单后发送消息给其他业务系统,很多同学喜欢这样做
@Transactional
public void submitOrder(){
insertDb();
sendMsg();
}
用Spring注解来实现事务,首先先插数据到DB,然后发消息。仔细分析遇到的各种情况:
- 1.插入数据和发送消息都成功,这是最常见的情况,当然是皆大欢喜。
- 2.插入数据失败,抛出异常,消息当然也不会发,这也没什么问题。
- 3.插入数据成功,只是在提交commit的时候由于网络原因连接超时,抛出异常,回滚无效,消息仍旧发送成功。算是有惊无险。
- 4.事务在commit的时候失败回滚,消息已经发送,造成消息多发。
- 5.消息发送失败,事务回滚,没什么问题。
- 6.消息发送成功,网络原因连接超时,事务回滚,造成消息多发。
- 7.事务提交的时候,机器宕机,也有可能造成消息多发。
分析以上几种情况,除了消息多发,也没什么大问题,消息订阅者可以反查一下订单,如果查不到可以丢弃该消息,但是会对业务方造成一定的困惑。
但是如果把sendMsg替换一个服务,情况就不同了。假设取消订单需要先更新订单状态,然后回冲库存。
@Transactional
public void cancelOrder(){
updateDB();
inventoryService.rollback();
}
从之前的案例分析来看,这样会造成订单状态更新失败的情况下,调用库存接口成功,显然出现了不一致情况。无论怎么样调整操作顺序,都会出现类似问题。那么怎么样尽量规避这种问题发生?
本地事务表
核心思路是将分布式事务转换为本地事务。新建message表,保存业务ID和自身状态。更新订单操作和插入message在同一个库中做事务,能保证一致性。
@Transactional
public void cancelOrder(){
updateDB();
insertMessage();
}
message表
id | status |
---|---|
orderId | 未处理 |
如果上述事务成功后了,接下来发送消息给库存服务或者直接调用库存服务,成功后更新消息表状态。这里有个问题,如果下来操作失败怎么办?这时候message表就派上用场了,需要另外一个务定时扫描message表,将没有处理发送的消息再次发送出去。库存服务还需要保持幂等性。
外置事务表
有些服务本身不开启本地事务,而是调用其他RPC服务,例如下面接口调用了serviceA,serviceB,serviceC接口。怎么保证这3个接口要么都调用成功?其实没有确切答案。只能借鉴前面的思路,设计一个外部事务表。每次调用前预先生成全局序列号id,记录这次事务需要调用哪些接口。每次调用成功后更新状态为成功。当然还是存在多次调用的情况,被调用方需要做好幂等防重措施。这里还有很多需要考虑的地方,一个是性能优化,还有就是完善失败后补偿机制,
全局序列号 | 服务 | 调用服务 | 顺序 | 状态 |
---|---|---|---|---|
123 | doSomething | serviceA | 1 | OK |
123 | doSomething | serviceB | 2 | OK |
123 | doSomething | serviceC | 3 | OK |
public void doSomething(){
serviceA();
serviceB();
serviceC();
}
事务消息
Rocketmq有类似实现。
首先先发送prepare消息给Rocketmq,这时消息并不发送。等事务提交后,发送确认消息给Rocketmq,消息再发送出去。如果Rocketmq没有收到确认消息,会询问业务方是否要发送。
数据库日志
通过数据库日志,比如监听mysql的binlog日志,通过canal等中间件把数据变更日志推送出去,然后再调用后续服务,也是可行方案。
其他方案
支付宝TCC,这里转发一张图片,有兴趣的自己查阅资料
最后屏障
自动对账,人工介入
总结
实现分布式事务一些思路
- 一定有一个协调者,在事务的不同阶段介入,保证事务的最终一致性。这个协调者是一个高度抽象,它可能是一个中间件,可能是一个服务,甚至是一个任务,不管怎么说,它一定是各个事务单元的第三者,协调各个事务单元最终达成一致性。
- 协调者必须知道分布式事务的全貌,知道了发生了什么,没发生什么,该发生什么。
- 事务发生前结果应该是预知的,或者说事务发生后结果是确定的。这就需要协调者能处理各种情况发生,不能遗漏任何情况,知道各种情况下的补偿策略,或回滚,或重试,或者尽最大努力送达,或者通知人工介入。