通过可靠消息实现最终一致性
- 可靠消息,就是靠普消息,还是基于之前的这个案例
- 比如这个订单服务,无论你是先发送消息,还是先新建订单,它其实都是发送的不可靠消息
- 就是说如果这个消息,像mysql事务那样,只要订单服务不确认,下游就没办法消费
- 如果你这个订单服务挂了,就可以取消这个消息,就不用做这个本地消息表了
- 本地消息表,要有一个这个循环的这么一个查询,高并发的时候,你本地的数据库本身压力就大
- 再弄这么一个查询,一直循环的查,它这个压力也不小,现在看一下基于事务消息,也称为可靠消息
- 生产者就可以理解为这个订单服务,消费者就可以理解为积分服务,还有库存服务
- 先看第一个,生产者先发一个
半消息
给消息队列,消息队列就回我这个半消息成功的信息 - 这是一个半消息,然后拿到这个半消息成功的结果之后,我们往数据库里写一个Transaction事务
- 我们就把本地事务执行了,执行之后,也就是到了第4步, 成功情况下就是确认这个半消息
- 只要到第4步这个commit成功了,消费者就可以消费了,因为你的这个消息已经在这个消息队列中了
- 这个解决方式是解决了只要我们能发出去的这个消息就是可靠的,只要能提交的消息,本地消息就一定是成功的
- 因为你本地事务都已经执行成功了,先发半消息,消息队列回给我,回给我之后,我就能收到了,说明已经成功了
- 然后这个时候你就开始干你本地的事儿,比如订单生产者自己开始建订单,建订单产品表,这都是你的事
- 我本地能成功了之后,1,2,3步是为了能让消费者成功消费的准备工作
- 这个思路就是说只要一提交,那我本地这边全部ok, 然后我这个消息队列, 肯定能保证我我的最终一致性
- 如果在3之前出错,那不会做事务,那相当于准备工作没做好,那下游也不会做相应的这个事情
- 同时,我们还要思考,有没有其他方面的情况会导致问题
- 比如,更复杂的网络传输问题,别人的服务宕机了或有bug了
- 因为微服务在开发的时候,每个小组可以时刻发布自己的服务,它不受控制
- 如果上述分布式服务出问题了,消息队列也会有一个回查事务消息状态的机制
- 我会问你这个生产者,哎,你这个状态是啥?然后生产者就会查询这个本地事务状态
- 第5步和第6步,就是又一次为这个返回事务状态做commit和rollback,就是为下一步的工作做准备
- 就是消息队列,不知道你这个消息要不要投递,也不能知道别人的状态是什么样的
- 那消息队列就会问这个生产者,你这个消息事务是啥状态,在第6步得到查询的事务状态是commit
- 那我消息队列就commit消息投递,一旦消息投递了,这个消费者就可以进行消费了
- 假如说,返回的这个事务状态是rollback,消息队列就可以把消息扔了
- 在这整个链路里,有成功的,让消费者消费消息;也有失败,让这个消息队列去丢弃消息
- 还有中间状态,就是说我不确定这个事务是不是正确的?那询问还是有结果的,就是 commit 或 rollback
- 其实我们说走到第5步,第6步,还有第7步的时候,他就可以再一次确认我们的消息是否要投递还是丢弃
- 到这里,一直没有看到说有锁的存在,在高并发的情况下,消息队列就保证了我们最终的一致性
- 就是说锁的存在,它一定是和高并发是这个对立的,我们尽量不要用锁的方式去考虑我们的并发
积分和库存业务场景的对比
- 再回过头来看一下这个模型,如果你的生产者是订单,而消费者是库存的话
- 如果库存不足,消费者是库存服务,库存不足,虽然成功的发送了到这个RocketMQ里
- 但是库存没有办法成功消费这条消息,这个和其他业务形态上是不一样的
- 比如订单服务,可以说是送积分,从技术角度来说,积分是没有上限的
- 还有一种形态,就是我们说发短信,你成功的购买了某某产品等等
- 这种业务形态, 你的生产者只要把消息放到了消息队列, 消息队列一定是可以保障的
- 就是说你消息队列是集群吧,你在不挂的情况下,是一定能送达到消费者应用队列里的
- 比如说, 积分服务,短信服务,你这边已经是commit了
- 无论你是在第4步commit的,还是说第7步commit的下游是一定能消费到的
- 但是库存服务不一样,如果库存不足了,你这边又没办法返回
- 左边是库存服务和订单服务,右边是消息队列
- 从正向来说,如果服务的提供方发送消息到这个消息队列,也就是生产者发送消息到消息队列
- 只要消息队列集群不挂,那么我们的消费者是一定能收到这个消息的
- 就是实现最终的一致性对于积分服务,短信服务的使用都是没有问题的
- 因为积分理论上是无上限的,我们的短信是一定能发上去的,只要你短信账户里,有足够的余额
- 但是当库存为零的时候,你的这个消息队列仍然收到了我们发送成功的消息
- 但下游库存服务是没有办法消费成功的,我们不可能凭空多出来这么多库存
- 让你去消费,因为没有那么多库存,我们又不能把这个消息队列成功投递库存不足的消息
- 再返回给这个生产者,这个是不可能的,所以,我们能不能先发送一个
归还库存的半消息
- 这个半消息, 对于库存服务来说, 它是见不到的,我们发完半消息之后
- 去调用扣减库存的Srv服务,这就是一个Grpc的这么一个调用,先看这个返回失败的情况
- 如果调用库存失败了,这个时候返回一个rollback,因为一开始就是调用的是归还
- 那你这个时候就调用rollback,如果我们库存执行失败了,那说明我们库存这个数据是没有变的
- 既然你库存调用是失败,订单就不会创建, 因为我本地这个数据库就不会变
- 那么我们两边的这个数据都没变,这个业务也是可以接受的
- 就是说没有造成数据不一致,那你就不要给我发这个消息来告诉我,你要去归还库存了
- 那我们直接rollback这个消息,然后这个消息队列, 就可以把这个归还库存的消息给它扔掉了
- 扔掉之后,看左边这一边就没问题了,数据都没变
- 你执行失败了,我这边又没执行,或者说,这两边的数据都保持一致,这就是OK的
- 好,我们再看下一张执行成功的图,看我们订单服务发送一个半消息
- 然后调用了Grpc扣减那个库存,然后它成功了
- 成功之后, 我们就会执行这个本地 mysql 事务服务, 其实它可能成功,也可能失败
- 因为我们如果在微服务里, 由于网络原因或宕机,Bug,停电等各种问题
- 它都可能导致一个服务的运行失败
- 如果我们先说这个执行本地mysql事务成功,在第4步成功,执行rollback
- 还是从数据的角度来看,库存扣减成功,订单执行成功,我们本地也执行成功
- 那就是说我们两边数据都改变了,这个业务上也是可以接受的
- 订单生成成功了,库存也扣减了,那就相当于交易成功
- 那你就不要给我发送这个归还库存的半消息了,所以是 rollback这个半消息
- 然后,我们把这个消息就扔掉了
- 那我们再看看,如果第4步执行失败了,就是说我们有任何情况
- 订单服务有bug,断电或者其他场景,执行失败,那就执行一个commit
- 因为订单执行失败了,相当于订单数据没有变,那你的库存现在是变了
- 因为之前第三步已经执行成功了,那这个时候就要告诉库存,说给我扣减了,因为我执行失败了
- 失败以后,提交一个commit之后,然后,这个库存服务就可以监听到这个消息队列里
- 因为它 commit 了嘛,这个消息就能看到了,订阅了这个消息之后,就能去归还库存了
- 如果你这个订单服务,还有一些其他问题,怎么办?其实这个消息队列还提供一种机制
- 就是回查这个消息,比如说我现在不确定你这个是要提交还是rollback
- 这个回查机制,就查这个订单的服务,那你还去你这个本地事务的数据库库里去捞数据
- 你捞对了,就rollback,捞错了,还是commit, 你commit之后
- 我还是能调用到这个库存服务,然后你再给我归还
- 这张图就是看到了为了保持这个数据的一致性,我们这边整个业务流程的保证就是这样了
- 再看上面这张图,还有问题,是在原来的基础上增加了第8条发送延迟消息和监听延迟消息。
- 就是说我这个库存有一百个,我这个用户买完以后,就是不支付
- 如果他一周不支付或者一个月不支付,你的库存永远不释放,别人永远买不了
- 那不就把这个商城的库存给锁死了
- 当我库存执行成功,这个本地的事务也执行成功的时候,我就把它发送一条延迟消息。
- 假如我们规定半个小时,时间一到,这个延迟消息就会投递
- 然后,我们就会根据这个消息去看,这个订单是 支付成功了,还是支付失败了
- 如果是未支付或者是支付失败都可以,如果你执行失败了,那我就归还库存
- 因为对商城来说,如果没收到钱,那我就归还库存,半个小时之后,仍然其他的用户就可以买
- 这样就完美的解决了库存,订单和这个订单下单成功后不支付的这么一个场景
- 这里的核心重点是:在一开始发送了一个归还库存的半消息
- 执行commit和rollback的情况是反着来的