前言
回顾之前做的b2c项目,在订单服务那块,涉及到分布式事务的问题,情景是这样的:提交订单的时候,需要创建一笔订单,并且还需要调用远程库存服务进行库存锁定,等到支付成功之后将这个锁定的库存减掉。这里就涉及到远程调用的时候就可能会出现两个事务状态不一致的情况
本地事务和分布式事务
简单一说
本地事务: 它是在一个服务内的事务
分布式事务: 在微服务下,涉及到多个服务之间的调用,这时候需要保持多个服务下的事务一致性,这就是分布式事务的范畴了。
本地事务
我们先来复习下本地事务的有关知识点:
事务的特性: (ACID)
- A:原子性,即一个事务里的操作,要么全部执行成功,要么全部失败
- C:一致性,即事务前后的操作整体数据是一致的
- I :隔离性,即事务与事务的操作是互不影响的
- D:持久性,即事务一旦完成,就会被持久化到磁盘中永久保存,不会丢失
事务的隔离级别:
设置 | 描述 |
---|---|
Serializable | 可避免脏读、不可重复读、虚读情况的发生。(串行化) |
Repeatable read | 可避免脏读、不可重复读情况的发生。(可重复读) |
Read committed | 可避免脏读情况发生(读已提交)。 |
Read uncommitted | 最低级别,以上情况均无法保证。(读未提交) |
事务的传播形为:
在Spring种,@Transactional(propagation = Propagation.REQUIRED) 设置事务的传播级别,这个就是表示,在内部方法调用的时候,共用一个事务
下面这个则是创建一个新事务。
但是共用一个事务这里有一个bug,就是Spring在处理事务的时候,是采用代理的方式,所以调用方法的时候需要经过这个代理类,否则就不是一个事务。
分布式事务
为什么会有分布式事务呢?
微服务模块下,多个服务之间进行远程调用是常有的事,伴随而来的,也会常出现异常:机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失等等,这时候就需要分布式事务来保证全局事务的一致性了。
CAP理论和BASE理论
CAP:
- C:数据一致性
- A:集群可用性
- P:分区容错性
最多只能实现这三点中的两点,三者不可兼得。一般我们必须保证分区容错性,C/A看情况采取。
BASE: - BA:基本可用:在分布式系统中出现故障的时候,允许损失部分可用性,比如响应时间、功能上的可用性
- S:软状态:是指允许系统存在中间状态,这些中间状态不会影响系统的可用性
- E:最终一致性:是指系统中所有的数据副本经过一定时间后,最终能够到达一致的状态。
分布式事务解决方案:
- 2PC模式:一个总的事务管理,多个本地资源管理器。在进行数据更新的时候,事务管理器会先发一个准备发送数据信息,各个资源管理器再回发确认,只要有一个没有,这条数据操作的事务就不能进行。
- 柔性事务-TCC:在事务回滚的时候,恢复数据是通过日志回滚的。(这块不确定,后续再学学)
- 柔性事务-可靠消息+最终一致性方案(异步确保型),我项目中的解决方案就是这种,就是在业务处理之后事务提交之前,向事实消息服务发送消息,实时服务只记录消息数据,而不是真正的发送。业务服务在事务提交之后,向实时消息服务确认发送,只有在得到确认发送指令后,实时消息服务才会真正发送。
项目解决方案
提交订单流程:
RabbitMQ延时队列(实现定时任务)
利用消息的TTL和死信Exchange就可以实现一个延时队列
什么是消息的TTL? 就是消息的存活时间
什么是死信Exchange? 一个消息在满足条件以后会进入死信路由,其实这里的死信Exchange跟普通的交换机没有什么不同,只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,被转发到这个交换机里
这样就实现了一个延时队列。
我的思路就是,在点击提交订单按钮之后,发送请求,创建订单,在订单创建成功以后,给MQ发消息,再去远程锁库存,锁定完库存之后,在库存服务下再给MQ发消息,剩下的就是监听了,在30min后,锁库存消息过期,在收到解锁库存的消息,我就去解库存,再过一段时间,订单消息过期,我收到订单关闭的消息,就去解锁库存
接下来我们要理清一下什么情况下需要解锁库存:
我们获取到锁库存消息过期的消息,这个消息包括:工作单ID,锁库存详情。我们可以通过工作单ID获取到订单编号,再去远程调用订单模块,查看这个订单是否存在?如果订单不存在,那库存肯定是需要解锁的,如果存在,我们需要看下订单的状态,我需要对已取消的状态进行解锁库存。已支付的不需要解锁。在解锁的过程中,我们还要看锁库存详情表里的锁定状态,如果这个库存已经被解锁,那我们也是不需要解锁的,被锁定状态才需要解锁。
对于订单过期未支付的我们也要监听这个消息,也是需要去解锁的。
但是上面的思路还是有一定的问题,当我们在解锁订单时,有网络延迟等状况时导致解锁库存发生在了订单解锁之前,那么有一部分订单的库存消息将会丢失不能被解锁。这种情况下,我们在库存服务里也监听了订单关闭的消息,收到这个订单关闭的消息,我们去解锁库存,然后将库存中未被解锁的库存进行解锁。
好了,以上我们通过消息过期时采取的一些策略包括: 我们针对订单过期未支付的消息进行消费,对订单进行关闭服务操作,针对库存服务过期发现没订单、或订单状态为已取消的消息进行解库存操作,最后还保证了避免这种情况的发生:防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理,导致卡顿的订单,永远都不能解锁库存问题。
解决了最终一致性,怎么保证消息可靠呢?
消息不可靠,任何操作都是浮云。假如我们在网络传输消息的时候消息丢失了怎么办?消息丢失主要有以下几种情况:
- 消息发出去,由于网络原因没有到达服务器:这种情况是不可避免地,我们需要做好日志信息存到数据库当中,设计失败后具有重发机制,定期去扫描数据库中的这些未发送成功的消息。
- 消息抵达Broker,Broker要将消息持久化到磁盘中才算成功,假如还没有持久化成功,broker宕机了,消息就会丢失,这个时候我们需要将publisher必须确认回调机制,确认成功的消息,修改数据库消息状态。
- 自动ACK状态下,消费者收到消息,但没来得及确认回复,宕机了,也会导致消息丢失,这个时候我们需要开启手动ack,消息成功移除,否则消息就重回队列。
针对消息重复的话,我们可以在数据库中设计一个防重表,只有没被操作的信息才会被消费,又或者是我们在业务中设计成幂等的。
针对消息积压:可以上线更多的消费者;也可以上线一个专门处理消息的队列,将这些消息监听下来批量存储到数据当中,然后在离线编写的程序去处理数据库中的这些消息。
总结
总之,这块在做的过程中,非常的难,需要考虑各种情况,也许我想到的这些之下还有可能会出现问题,这需要慢慢的积累总结经验,针对查询数据库的,我们可以加缓存,解决一下缓存数据不一致的问题,提升一下系统吞吐量,而针对修改数据库的消息,一定要慎之又慎。