常见的分布式事务解决方案,你会几种?
The following article is from Java技术迷 Author 汪伟俊
今天我们来聊一聊分布式事务,在传统的单体应用中,事务的控制非常简单,Spring框架都为我们做了封装,我们只需简单地使用@Transactional注解就能进行事务的控制,然而在分布式应用中,传统的事务方案就出现了极大的问题:
分布式事务解决方案
下面列举一些分布式事务的解决方案:
2PC模式 TCC事务补偿 最大努力通知 可靠消息
2PC模式
2PC意为二阶段提交,又叫XA Transactions,其中,XA是一个两阶段提交协议,该协议将事务分为两个阶段:
第一阶段:事务协调器要求每个涉及到事务的数据库预提交此操作,并响应是否可以提交 第二阶段:事务协调器要求每个数据库提交数据,其中,如果有任何一个数据库否决此次提交,那么所有的数据库都会被要求回滚它们在此事务中修改的内容
TCC事务补偿
2PC模式遵循的是ACID原则,即:原子性、一致性、隔离性、持久性,它是一种强一致性的设计,而事实上,很多情况下我们都无法做到强一致性,或者想要实现强一致性成本比较高。eBay 的架构师 Dan Pritchett在ACM上发表文章提出了BASE理论,该理论指出即使无法做到强一致性,但应用可以采用合适的方式达到最终一致性,最终一致性对于数据的要求并不是非常严格,它不需要系统做到数据实时保持最新状态,而是数据在经过一段时间后,最终能够达到一致即可,TCC事务补偿方案就是一种柔性事务的设计,它能够保证数据的最终一致性,一般是在业务层进行实现的。
最大努力通知
最大努力通知方案也是柔性事务的设计,它按规律进行消息通知,不保证数据一定能通知成功,但会提供可查询接口进行核对。如果你在项目中对接过支付宝支付服务,就应该清楚,支付宝在付款后采用的就是最大努力通知方案,支付宝会每隔一段时间发送一个通知来告诉开发者订单的支付情况,只有返回了 success
数据后支付宝才会停止通知。
可靠消息
可靠消息仍然只保证数据的最终一致性,且它需要借助消息中间件来完成,当某个服务在事务提交之前,会向消息中间件发送一条消息,再根据本地事务的执行状态发送Commit或者RollBack给消息中间件,消费方根据对应的状态对事务进行对应的处理。
RabbitMQ实现数据的最终一致性
想象一个场景,用户在进行下单操作之后,会有30分钟的时间让用户进行付款,在用户付款之前,商品的库存并没有真正的扣除,而是进行锁定。若是用户在规定时间内付款成功,则需要真正扣减库存;若是用户在规定时间内没有付款,则需要将锁定的商品库存重新解锁,这里涉及到两个模块之间的事务操作:
延时队列
在RabbitMQ中,我们可以实现一个延时队列,消息进入延时队列后不会立马被消费,而是需要等待设定的时间,在实现之前,需要清楚两个概念:
死信 死信路由
死信
在RabbitMQ中,我们可以为消息设置一个存活时间TTL(time to live),当消息超过了存活时间,就可以认为这个消息已经死了,称为 死信
。一个消息在满足如下条件时会进入死信路由:
消息被Consumer拒收,并且reject方法的参数中requeue值为false,即:该消息被拒收后,不会再进入消息队列 消息超过了TTL时间,导致消息过期 消息队列满了,排在前面的消息会被丢弃或者扔到死信路由上
死信路由
在死信概念中,我们一直强调一个词, 死信路由
,其实,它就是一个普通的路由,只是当某个队列绑定了死信路由后,该消息队列中的消息过期了,就会自动触发消息的转发,消息会被扔到死信路由上。通过死信和死信路由,我们就能够实现一个延时队列,如图所示:
x-dead-letter-exchange:xxx 设置死信路由 x-dead-letter-routing-key:xxx 设置死信路由键 x-message-ttl:1800 000 设置消息的存活时间为30分钟
将其类比到具体的业务场景中,就比如订单超时自动解锁库存的需求,其设计如下图所示:
Publisher发送消息给路由order.delay.exchange,路由键为order.delay 路由order.delay.exchange将消息放入绑定关系为order.delay的消息队列order.delay.queue 消息队列order.delay.queue中消息的存活时间为30分钟,当消息过期后,消息会交给路由order.exchange,路由键为order 路由order.exchange将消息放入绑定关系为order的消息队列order.queue Consumer监听消息队列order.queue,保证了消费的消息均是过期的
不过这一过程还可以再简化一下:
代码实现
概念吹得天花乱坠,不动手实现一下始终是无法深刻理解的,所以,我们就通过一个案例来感受一下延迟队列的效果。
创建一个SpringBoot应用,并使用代码创建出路由、消息队列及其它们之间的关系:
@Configuration
public class MyRabbitMQConfig {
@Bean
public Queue orderDelayQueue() {
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "order.delay.exchange");
arguments.put("x-dead-letter-routing-key", "order.release.order");
arguments.put("x-message-ttl", 1000 * 10);
return new Queue("order.delay.queue", true, false, false, arguments);
}
@Bean
public Queue orderReleaseOrderQueue() {
return new Queue("order.release.order.queue", true, false, false);
}
@Bean
public Exchange orderDelayExchange() {
return new TopicExchange("order.delay.exchange", true, false);
}
@Bean
public Binding orderCreateOrderBinding() {
return new Binding("order.delay.queue",
Binding.DestinationType.QUEUE,
"order.delay.exchange",
"order.create.order", null);
}
@Bean
public Binding orderReleaseOrderBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order.delay.exchange",
"order.release.order", null);
}
}
需要注意的是使用这种方式创建需要发送一下消息:
@RestController
public class TestController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/test")
public String test(){
rabbitTemplate.convertAndSend("order.delay.queue","message");
return "test";
}
}
当发送消息到队列 order.delay.queue
时,RabbitMQ便会创建出队列和路由:order.release.order.queue
:
@RabbitListener(queues = "order.release.order.queue")
public void listener(Message message) {
System.out.println("收到消息:" + message);
}
此时我们访问 http://localhost:8080/test ,就会发送一条消息到队列,再经过10秒钟的时间,消息会过期,消息便会进入队列 order.release.order.queue
,而我们监听的又是这个队列,所以总能收到10秒后过期的消息:
收到消息:(Body:'message' ......)
收到消息:(Body:'message' ......)
收到消息:(Body:'message' ......)
通过这样的方式,我们便能够实现数据的最终一致性。
到这里几种常见的分布式解决方案就讲完了,除了本文提到的分布式事务解决方案,你还知道其它的解决方案么?
PS:如果觉得这篇文章对你有帮助,欢迎分享、点赞、在看;如果想持续获取更多技术干货,请关注公众号,感谢支持。
往期干货