十.分布式事务、 幂等性机制
1.分布式事务
1.1CAP和BASE理论
1.1.1CAP理论
cap理论是分布式系统的理论基石
Consistency (一致性):
“all nodes see the same data at the same time”,即更新操作成功并返回客户端后,所有节点在同一时间的数据完全一致,这就是分布式的一致性。一致性的问题在并发系统中不可避免,对于客户端来说,一致性指的是并发访问时更新过的数据如何获取的问题。从服务端来看,则是更新如何复制分布到整个系统,以保证数据最终一致。
Availability (可用性):
可用性指“Reads and writes always succeed”,即服务一直可用,而且是正常响应时间。好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。
Partition Tolerance (分区容错性):
即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。分区容错性要求能够使应用虽然是一个分布式系统,而看上去却好像是在一个可以运转正常的整体。比如现在的分布式系统中有某一个或者几个机器宕掉了,其他剩下的机器还能够正常运转满足系统需求,对于用户而言并没有什么体验上的影响。
如果你你是一个分布式系统,那么你必须要满足一点:分区容错性
CA without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但放弃P的同时也就意味着放弃了系统的扩展性,也就是分布式节点受限,没办法部署子节点,这是违背分布式系统设计的初衷的。
CP without A:如果不要求A(可用),相当于每个请求都需要在服务器之间保持强一致,而P(分区)会导致同步时间无限延长(也就是等待数据同步完才能正常访问服务),一旦发生网络故障或者消息丢失等情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统。设计成CP的系统其实不少,最典型的就是分布式数据库,如Redis、HBase等。对于这些分布式数据库来说,数据的一致性是最基本的要求,因为如果连这个标准都达不到,那么直接采用关系型数据库就好,没必要再浪费资源来部署分布式数据库。
AP wihtout C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。典型的应用就如某米的抢购手机场景,可能前几秒你浏览商品的时候页面提示是有库存的,当你选择完商品准备下单的时候,系统提示你下单失败,商品已售完。这其实就是先在 A(可用性)方面保证系统可以正常的服务,然后在数据的一致性方面做了些牺牲,虽然多少会影响一些用户体验,但也不至于造成用户购物流程的严重阻塞。
1.1.2BASE理论
分布式系统中的一致性是 弱一致性 单数据库 mysql的一致性 强一致性
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。接下来看一下BASE中的三要素:
1、基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性—-注意,这绝不等价于系统不可用。比如:
(1)响应时间上的损失。正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒
(2)系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面
2、软状态
软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
3、最终一致性
最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统的事物ACID特性是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。
1.2两/三阶段提交
常见分布式事务解决方案
- 两阶段提交(2PC, Two-phase Commit)
- TCC 补偿模式
- 基于本地消息表实现最终一致性
- 最大努力通知
- 基于可靠消息最终一致性方案
两阶段提交(2PC)
两阶段提交又称2PC,2PC是一个非常经典的中心化的原子提交协议
。
这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点
(coordinator)和N个参与者节点
(partcipant)。
两个阶段
:第一阶段:投票阶段 和第二阶段:提交/执行阶段。
举例
订单服务A,需要调用 支付服务B 去支付,支付成功则处理购物订单为待发货状态,否则就需要将购物订单处理为失败状态。
那么看2PC阶段是如何处理的
1、第一阶段:投票阶段
第一阶段主要分为3步
1)事务询问
协调者 向所有的 参与者 发送事务预处理请求,称之为Prepare,并开始等待各 参与者 的响应。
2)执行本地事务
各个 参与者 节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向 协调者 报告说:“我这边可以处理了/我这边不能处理”。.
3)各参与者向协调者反馈事务询问的响应
如果 参与者 成功执行了事务操作,那么就反馈给协调者 Yes 响应,表示事务可以执行,如果没有 参与者 成功执行事务,那么就反馈给协调者 No 响应,表示事务不可以执行。
第一阶段执行完后,会有两种可能。1、所有都返回Yes. 2、有一个或者多个返回No。
2、第二阶段:提交/执行阶段(成功流程)
成功条件
:所有参与者都返回Yes。
第二阶段第二阶段主要分为两步
1)所有的参与者反馈给协调者的信息都是Yes,那么就会执行事务提交
协调者 向 所有参与者 节点发出Commit请求.
2)事务提交
参与者 收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源。主要分为两步
1)所有的参与者反馈给协调者的信息都是Yes,那么就会执行事务提交
协调者 向 所有参与者 节点发出Commit请求.
2)事务提交
参与者 收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源。
3、第二阶段:提交/执行阶段(异常流程)
异常条件
:任何一个 参与者 向 协调者 反馈了 No 响应,或者等待超时之后,协调者尚未收到所有参与者的反馈响应。
异常流程第二阶段也分为两步
1)发送回滚请求
协调者 向所有参与者节点发出 RoollBack 请求.
2)事务回滚
参与者 接收到RoollBack请求后,会回滚本地事务。
通过上面的演示,很容易想到2pc所带来的缺陷
1)性能问题
无论是在第一阶段的过程中,还是在第二阶段,所有的参与者资源和协调者资源都是被锁住的,只有当所有节点准备完毕,事务 协调者 才会通知进行全局提交,
参与者 进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。
2)单节点故障
由于协调者的重要性,一旦 协调者 发生故障。参与者 会一直阻塞下去。尤其在第二阶段,协调者 发生故障,那么所有的 参与者 还都处于
锁定事务资源的状态中,而无法继续完成事务操作。(虽然协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
2PC出现单点问题的三种情况
(1)协调者正常,参与者宕机
由于 协调者 无法收集到所有 参与者 的反馈,会陷入阻塞情况。
解决方案:引入超时机制,如果协调者在超过指定的时间还没有收到参与者的反馈,事务就失败,向所有节点发送终止事务请求。
(2)协调者宕机,参与者正常
无论处于哪个阶段,由于协调者宕机,无法发送提交请求,所有处于执行了操作但是未提交状态的参与者都会陷入阻塞情况.
解决方案:引入协调者备份,同时协调者需记录操作日志.当检测到协调者宕机一段时间后,协调者备份取代协调者,并读取操作日志,向所有参与者询问状态。
(3)协调者和参与者都宕机
1发生在第一阶段: 因为第一阶段,所有参与者都没有真正执行commit,所以只需重新在剩余的参与者中重新选出一个协调者,新的协调者在重新执行第一阶段和第二阶段就可以了。
2)发生在第二阶段 并且 挂了的参与者在挂掉之前没有收到协调者的指令。也就是上面的第4步挂了,这是可能协调者还没有发送第4步就挂了。这种情形下,新的协调者重新执行第一阶段和第二阶段操作。
3)发生在第二阶段 并且 有部分参与者已经执行完commit操作。就好比这里订单服务A和支付服务B都收到协调者 发送的commit信息,开始真正执行本地事务commit,但突发情况,Acommit成功,B确挂了。这个时候目前来讲数据是不一致的。虽然这个时候可以再通过手段让他和协调者通信,再想办法把数据搞成一致的,但是,这段时间内他的数据状态已经是不一致的了! 2PC 无法解决这个问题。
1.3TCC分布式事务
一个订单支付之后,我们需要做下面的步骤:
- 更改订单的状态为“已支付”
- 扣减商品库存
- 给会员增加积分
- 创建销售出库单通知仓库发货
好,业务场景有了,现在我们要更进一步,实现一个 TCC 分布式事务的效果。
什么意思呢?也就是说:
[1] 订单服务-修改订单状态
[2] 库存服务-扣减库存
[3] 积分服务-增加积分
[4] 仓储服务-创建销售出库单。
上述这几个步骤,要么一起成功,要么一起失败,必须是一个整体性的事务。
举个例子,现在订单的状态都修改为“已支付”了,结果库存服务扣减库存失败。那个商品的库存原来是 100 件,现在卖掉了 2 件,本来应该是 98 件了。
结果呢?由于库存服务操作数据库异常,导致库存数量还是 100。这不是在坑人么,当然不能允许这种情况发生了!
但是如果你不用 TCC 分布式事务方案的话,就用个 go开发这么一个微服务系统,很有可能会干出这种事儿来。
我们来看看下面的这个图,直观的表达了上述的过程:
所以说,我们有必要使用 TCC 分布式事务机制来保证各个服务形成一个整体性的事务。
上面那几个步骤,要么全部成功,如果任何一个服务的操作失败了,就全部一起回滚,撤销已经完成的操作。
比如说库存服务要是扣减库存失败了,那么订单服务就得撤销那个修改订单状态的操作,然后得停止执行增加积分和通知出库两个操作。
说了那么多,老规矩,给大家上一张图,大伙儿顺着图来直观的感受一下:
落地实现 TCC 分布式事务
那么现在到底要如何来实现一个 TCC 分布式事务,使得各个服务,要么一起成功?要么一起失败呢?
大家稍安勿躁,我们这就来一步一步的分析一下。咱们就以一个 go开发系统作为背景来解释。
TCC 实现阶段一:Try
首先,订单服务那儿,它的代码大致来说应该是这样子的:
type OrderService struct{
CreditSrvClient proto.CreditClient //用户积分
WmsSrvClient proto.WmsClient //记录仓库的变动信息
InventorySrvClient proto.InventoryClient //库存确认扣减
}
func NewOrderService() *OrderService {
return &OrderService{
CreditSrvClient: proto.CreditClient{},
WmsSrvClient: proto.WmsClient{},
InventorySrvClient: proto.InventoryClient{},
}
}
func (o OrderService) UpdateOrderStatus() error {
return nil
}
func (o OrderService) Notify() error {
o.UpdateOrderStatus() //更新订单的状态
o.CreditSrvClient.AddCredit() //增加积分
o.InventorySrvClient.ReduceStock() //库存确认扣减
o.WmsClient.SaleDelivery() //记录仓库变更记录
return nil
}
其实就是订单服务完成本地数据库操作之后,通过grpc 来调用其他的各个服务罢了。
但是光是凭借这段代码,是不足以实现 TCC 分布式事务的啊?!兄弟们,别着急,我们对这个订单服务修改点儿代码好不好。
首先,上面那个订单服务先把自己的状态修改为:TRADE_SUCCESS。
这是啥意思呢?也就是说,在 pay() 那个方法里,你别直接把订单状态修改为已支付啊!你先把订单状态修改为 UPDATING,也就是修改中的意思。
这个状态是个没有任何含义的这么一个状态,代表有人正在修改这个状态罢了。
然后呢,库存服务直接提供的那个 reduce_stock() 接口里,也别直接扣减库存啊,你可以是冻结掉库存。
举个例子,本来你的库存数量是 100,你别直接 100 - 2 = 98,扣减这个库存!
你可以把可销售的库存:100 - 2 = 98,设置为 98 没问题,然后在一个单独的冻结库存的字段里,设置一个 2。也就是说,有 2 个库存是给冻结了。
积分服务的 add_credit() 接口也是同理,别直接给用户增加会员积分。你可以先在积分表里的一个预增加积分字段加入积分。
比如:用户积分原本是 1190,现在要增加 10 个积分,别直接 1190 + 10 = 1200 个积分啊!
你可以保持积分为 1190 不变,在一个预增加字段里,比如说 prepare_add_credit 字段,设置一个 10,表示有 10 个积分准备增加。
仓储服务的 sale_delivery() 接口也是同理啊,你可以先创建一个销售出库单,但是这个销售出库单的状态是“UNKNOWN”。
也就是说,刚刚创建这个销售出库单,此时还不确定它的状态是什么呢!
上面这套改造接口的过程,其实就是所谓的 TCC 分布式事务中的第一个 T 字母代表的阶段,也就是 Try 阶段。
总结上述过程,如果你要实现一个 TCC 分布式事务,首先你的业务的主流程以及各个接口提供的业务含义,不是说直接完成那个业务操作,而是完成一个 Try 的操作。
这个操作,一般都是锁定某个资源,设置一个预备类的状态,冻结部分数据,等等,大概都是这类操作。
咱们来一起看看下面这张图,结合上面的文字,再来捋一捋整个过程:
TCC实现阶段二:Confirm
然后就分成两种情况了,第一种情况是比较理想的,那就是各个服务执行自己的那个 Try 操作,都执行成功了,Bingo!
这个时候,就需要依靠 TCC 分布式事务框架来推动后续的执行了。这里简单提一句,如果你要玩儿 TCC 分布式事务,必须引入一款 TCC 分布式事务框架,比如java国内开源的 seata、ByteTCC、Himly、TCC-transaction。
否则的话,感知各个阶段的执行情况以及推进执行下一个阶段的这些事情,不太可能自己手写实现,太复杂了。
如果你在各个服务里引入了一个 TCC 分布式事务的框架,订单服务里内嵌的那个 TCC 分布式事务框架可以感知到,各个服务的 Try 操作都成功了。
此时,TCC 分布式事务框架会控制进入 TCC 下一个阶段,第一个 C 阶段,也就是 Confirm 阶段。
为了实现这个阶段,你需要在各个服务里再加入一些代码。比如说,订单服务里,你可以加入一个 Confirm 的逻辑,就是正式把订单的状态设置为“已支付”了,大概是类似下面这样子:
func (o OrderService) Pay() error {
gorm.UpdateStatus("TRADE_SUCCESS")
}
库存服务也是类似的,你可以有一个 InventoryServiceConfirm 类,里面提供一个 reduce_stock() 接口的 Confirm 逻辑,这里就是将之前冻结库存字段的 2 个库存扣掉变为 0。
这样的话,可销售库存之前就已经变为 98 了,现在冻结的 2 个库存也没了,那就正式完成了库存的扣减。
积分服务也是类似的,可以在积分服务里提供一个 CreditServiceConfirm 类,里面有一个 addCredit() 接口的 Confirm 逻辑,就是将预增加字段的 10 个积分扣掉,然后加入实际的会员积分字段中,从 1190 变为 1120。
仓储服务也是类似,可以在仓储服务中提供一个 WmsServiceConfirm 类,提供一个 sale_delivery() 接口的 Confirm 逻辑,将销售出库单的状态正式修改为“已创建”,可以供仓储管理人员查看和使用,而不是停留在之前的中间状态“UNKNOWN”了。
好了,上面各种服务的 Confirm 的逻辑都实现好了,一旦订单服务里面的 TCC 分布式事务框架感知到各个服务的 Try 阶段都成功了以后,就会执行各个服务的 Confirm 逻辑。
订单服务内的 TCC 事务框架会负责跟其他各个服务内的 TCC 事务框架进行通信,依次调用各个服务的 Confirm 逻辑。然后,正式完成各个服务的所有业务逻辑的执行。
同样,给大家来一张图,顺着图一起来看看整个过程:
TCC实现创段三:Cancel
好,这是比较正常的一种情况,那如果是异常的一种情况呢?
举个例子:在 Try 阶段,比如积分服务吧,它执行出错了,此时会怎么样?
那订单服务内的 TCC 事务框架是可以感知到的,然后它会决定对整个 TCC 分布式事务进行回滚。
也就是说,会执行各个服务的第二个 C 阶段,Cancel 阶段。同样,为了实现这个 Cancel 阶段,各个服务还得加一些代码。
首先订单服务,它得提供一个 OrderServiceCancel 的类,在里面有一个 pay() 接口的 Cancel 逻辑,就是可以将订单的状态设置为“CANCELED”,也就是这个订单的状态是已取消。
库存服务也是同理,可以提供 reduce_stock() 的 Cancel 逻辑,就是将冻结库存扣减掉 2,加回到可销售库存里去,98 + 2 = 100。
积分服务也需要提供 addCredit() 接口的 Cancel 逻辑,将预增加积分字段的 10 个积分扣减掉。
仓储服务也需要提供一个 sale_delivery() 接口的 Cancel 逻辑,将销售出库单的状态修改为“CANCELED”设置为已取消。
然后这个时候,订单服务的 TCC 分布式事务框架只要感知到了任何一个服务的 Try 逻辑失败了,就会跟各个服务内的 TCC 分布式事务框架进行通信,然后调用各个服务的 Cancel 逻辑。
大家看看下面的图,直观的感受一下:
总结一下,你要玩儿TCC分布式事务的话:
- 首先需要选择某种TCC分布式事务框架,各个服务里就会有这个TCC分布式事务框架在运行。
- 然后你原本的一个接口,要改造为3个逻辑,Try-Confirm-Cancel。
- 先是服务调用链路依次执行Try逻辑
- 如果都正常的话,TCC分布式事务框架推进执行Confirm逻辑,完成整个事务
- 如果某个服务的Try逻辑有问题,TCC分布式事务框架感知到之后就会推进执行各个服务的Cancel逻辑,撤销之前执行的各种操作。
- 这就是所谓的TCC分布式事务。
- TCC分布式事务的核心思想,说白了,就是当遇到下面这些情况时,
- 某个服务的数据库宕机了
- 某个服务自己挂了
- 那个服务的redis、elasticsearch、MQ等基础设施故障了
- 某些资源不足了,比如说库存不够这些
- 先来Try一下,不要把业务逻辑完成,先试试看,看各个服务能不能基本正常运转,能不能先冻结我需要的资源。
- 如果Try都ok,也就是说,底层的数据库、redis、elasticsearch、MQ都是可以写入数据的,并且你保留好了需要使用的一些资源(比如冻结了一部分库存)。
- 接着,再执行各个服务的Confirm逻辑,基本上Confirm就可以很大概率保证一个分布式事务的完成了。
- 那如果Try阶段某个服务就失败了,比如说底层的数据库挂了,或者redis挂了,等等。
- 此时就自动执行各个服务的Cancel逻辑,把之前的Try逻辑都回滚,所有服务都不要执行任何设计的业务逻辑。保证大家要么一起成功,要么一起失败。
终极大招:
- 如果有一些意外的情况发生了,比如说订单服务突然挂了,然后再次重启,TCC分布式事务框架是如何保证之前没执行完的分布式事务继续执行的呢?
- TCC事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘上的日志文件里记录,也可以在数据库里记录。保存下来分布式事务运行的各个阶段和状态。
- 万一某个服务的Cancel或者Confirm逻辑执行一直失败怎么办呢?
- 那也很简单,TCC事务框架会通过活动日志记录各个服务的状态。
- 举个例子,比如发现某个服务的Cancel或者Confirm一直没成功,会不停的重试调用他的Cancel或者Confirm逻辑,务必要他成功!
- 当然了,如果你的代码没有写什么bug,有充足的测试,而且Try阶段都基本尝试了一下,那么其实一般Confirm、Cancel都是可以成功的!
- 如果实在解决不了,那么这个一定是很小概率的事件,这个时候发邮件通知人工处理
- seata、 go-seata
TCC的缺点、优点
优点:
1.解决了跨服务的业务操作原子性问题,例如组合支付,订单减库存等场景非常实用
2.TCC的本质原理是把数据库的二阶段提交上升到微服务来实现,从而避免了数据库2阶段中锁冲突的长事务低性能风险。
3.TCC异步高性能,它采用了try先检查,然后异步实现confirm,真正提交的是在confirm方法中。
缺点:
1.对微服务的侵入性强,微服务的每个事务都必须实现try,confirm,cancel等3个方法,开发成本高,今后维护改造的成本也高。
2.为了达到事务的一致性要求,try,confirm、cancel接口必须实现等幂性操作。
(定时器+重试)
3.由于事务管理器要记录事务日志,必定会损耗一定的性能,并使得整个TCC事务时间拉长,建议采用redis的方式来记录事务日志。
- tcc需要通过锁来确保数据的一致性,会加锁导致性能不高
1.4基于消息表的最终一致性
本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
下面以注册送积分为例来说明 :
下例共有两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。
交互流程如下 :
1、用户注册
用户服务在本地事务新增用户和增加“积分消息日志”。(用户表和消息表通过本地事务保证一致)
下表是伪代码
begin transaction;
// 1.新增用户
// 2.存储积分消息日志
commit transation;
这种情况下,本地数据库操作与存储积分消息日志处于同一事务中,本地数据库操作与记录消息日志操作具备原子性。
2、定时任务扫描日志
如何保证将消息发送给消息队列呢?
经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。
3、消费消息
如何保证消费者一定能消费到消息呢?
这里可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。
积分服务接收到“增加积分”消息,开始增加积分,积分增加成功后消息中间件回应ack,否则消息中间件将重复投递此消息。
由于消息会重复投递,积分服务的“增加积分”功能需要实现幂等性。
总结:上诉的方式是一种非常经典的实现,基本避免了分布式事务,实现了“最终一致性”。但是,关系型数据库的吞吐量和性能方面存在瓶颈,频繁的读写消息会给数据库造成压力。所以,在真正的高并发场景下,该方案也会有瓶颈和限制的。
1.5基于可靠消息的最终一致性
ocketMQ是一个来自阿里巴巴的分布式消息中间件,于2012年开源,并在2017年正式成为Apache顶级项目。据了解,包括阿里云上的消息产品以及收购的子公司在内,阿里集团的消息产品全线都运行在RocketMQ之上,并且最近几年的双十一大促中,RocketMQ都有抢眼表现。Apache RocketMQ 4.3之后的版本正式支持事务消息,为分布式事务实现提供来便利性支持。
RocketMQ事务消息设计则主要是为了解决Producer端的消息发送与本地事务执行的原子性问题,RocketMQ的设计中broker与producer端的双向通信能力,使得broker天生可以作为一个事务协调者存在;而RocketMQ本身提供的存储机制为事务消息提供了持久化能力;RocketMQ的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。
在RocketMQ 4.3后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部,解决Producer端的消息发送与本地事务执行的原子性问题。
执行流程如下 :
为方便理解我们还以注册送积分的例子来描述整个流程。
Producer即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分。
1、Producer发送事务消息
Producer(MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预览状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。
2、MQ Server回应消息发送成功
MQ Server接收到Producer发送给的消息则回应发送成功表示MQ已接收到消息。
3、Producer执行本地事务
Producer端执行业务代码逻辑,通过本地数据库事务控制。
本例中,Producer执行添加用户操作。
4、消息投递
若Producer本地事务执行成功则自动向MQ Server发送commit消息,MQ Server接收到commit消息后将“增加积分消息”状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;
若Producer 本地事务执行失败则自动向MQ Server发送rollback消息,MQ Server接收到rollback消息后将删除“增加积分消息”。
MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。
5、事务回查
如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。
以上主干流程已由RocketMQ实现,对用户则来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。
2.幂等性机制
2.1常见幂等性解决方案
哪些情况下需要考虑幂等性 - 同样的请求发送多次:
a. http请求的类型:
ⅰ. get
1. 获取商品信息, 这个会引起商品的数据的变化吗?
ⅱ. post
1. 比较常见,这种接口需要考虑到幂等性
ⅲ. put
1. 不一定要实现幂等性
a. put 把1号商品的价格改为200,网络返回的时候抖动了,重试
b. 第二次接口还是会把1号商品的价格改为200 - 这种情况下没有幂等的问题
2. 出现幂等性问题的情况:
a. 购物车中的商品,调用一次 这个商品的数量加一
ⅰ. 第一次调用 原本的值 10 之后价格变为11 - 但是返回的时候出现了网络抖动
ⅱ. 第二次发送 原本的值 11 之后价格变为12 - 但是返回的时候出现了网络抖动
ⅲ. 第三次发送 原本的值 12 之后价格变为13- 但是返回的时候出现了网络抖动
ⅳ. delete
1. 一般不具备幂等性的要求
2. 第一次调用 删除数据
3. 第二次调用 还是删除当前的数据
现在假设,你自己开发了一个支付宝,然后别人在二维码支付页面,先支付了,结果由于网络问题,当前页面一直没有刷新,就让人以为我没有支付,然后我就再次扫码支付,这下完蛋了:你扣了两次款。
2.2背景
我们实际系统中有很多操作,是不管做多少次,都应该产生一样的效果或返回一样的结果。
get请求一般没有幂等性需求、delete请求一般也没有幂等性需求,post、update视情况而定例如:
1. 前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果。
2. 我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱;
3. 发送消息,也应该只发一次,同样的短信发给用户,用户会哭的;
4. 创建业务订单,一次业务请求只能创建一个,创建多个就会出大问题。
等等很多重要的情况,这些逻辑都需要幂等的特性来支持。
2.3概念
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数.
更复杂的操作幂等保证是利用唯一交易号(流水号)实现.
我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的
2.4技术方案
1. 查询操作
查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作
2. 删除操作
删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个)
1.唯一索引,防止新增脏数据
比如:新建用户的时候将手机号码设置为唯一索引,那么即使你重试,也只会新建一个用户,不会因为重试导致当前用户注册了两个用户
要点:
唯一索引或唯一组合索引来防止新增数据存在脏数据
(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可)
2.token制制,防止页页重复提交
业务要求:
页面的数据只能被点击提交一次
发生原因:
由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交
解决办法:
集群环境:采用token加redis(redis单线程的,处理需要排队)
处理流程:
1. 数据提交前要向服务的申请token,token放到redis或内存,token有效时间
2. 提交后后台校验token,同时删除token,生成新的token返回
token特点:
要申请,一次有效性,可以限流
注意:redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用
3.悲观锁
获取数据的时候加锁获取
select * from table_xxx where id='xxx' for update;
注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用
//注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的
//悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用
4.乐观锁
乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。
乐观锁的实现方式多种多样可以通过version或者其他状态条件:
1. 通过版本号实现
update table_xxx set name=#name#,version=version+1 where version=#version#
2. 通过条件限制
update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0
要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高
5.分布式锁
还是拿插入数据的例子,如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。
//要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供)
6.select+insert
并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了
//注意:核心高并发流程不要用这种方法
7.对外提供接口的api如何保证幂等
如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号
source+seq在数据库里面做唯一索引,防止多次付款,(并发时,只能处理一个请求)
3.基于rocketmq实现分布式消息
3.1GO操作rocketmq
3.1.1发送普通消息
package main
import (
"context"
"fmt"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/primitive"
"github.com/apache/rocketmq-client-go/v2/producer"
)
func main() {
p, err := rocketmq.NewProducer(producer.WithNameServer([]string{"192.168.66.130:9876"}))
if err != nil {
panic("生成producer失败")
}
err = p.Start()
if err != nil {
panic("启动producer失败")
}
res, err := p.SendSync(context.Background(), primitive.NewMessage("imooc1", []byte("hello,rocketmq")))
if err != nil {
fmt.Printf("发送失败:%s\n", err)
} else {
fmt.Printf("发送成功:%s\n", res.String())
}
if err = p.Shutdown(); err != nil {
panic("关闭producer失败")
}
}
3.1.2消费消息
package main
import (
"context"
"fmt"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/consumer"
"github.com/apache/rocketmq-client-go/v2/primitive"
"time"
)
func main() {
c, _ := rocketmq.NewPushConsumer(
consumer.WithNameServer([]string{"192.168.66.130:9876"}),
consumer.WithGroupName("mxshop"),
)
段都成功了以后,就会执行各个服务的 Confirm 逻辑。
订单服务内的 TCC 事务框架会负责跟其他各个服务内的 TCC 事务框架进行通信,依次调用各个服务的 Confirm 逻辑。然后,正式完成各个服务的所有业务逻辑的执行。
同样,给大家来一张图,顺着图一起来看看整个过程:
[外链图片转存中...(img-QARcZmHf-1698745720830)]
**TCC实现创段三:Cancel**
好,这是比较正常的一种情况,那如果是异常的一种情况呢?
举个例子:在 Try 阶段,比如积分服务吧,它执行出错了,此时会怎么样?
那订单服务内的 TCC 事务框架是可以感知到的,然后它会决定对整个 TCC 分布式事务进行回滚。
也就是说,会执行各个服务的第二个 C 阶段,Cancel 阶段。同样,为了实现这个 Cancel 阶段,各个服务还得加一些代码。
首先订单服务,它得提供一个 OrderServiceCancel 的类,在里面有一个 pay() 接口的 Cancel 逻辑,就是可以将订单的状态设置为“CANCELED”,也就是这个订单的状态是已取消。
库存服务也是同理,可以提供 reduce_stock() 的 Cancel 逻辑,就是将冻结库存扣减掉 2,加回到可销售库存里去,98 + 2 = 100。
积分服务也需要提供 addCredit() 接口的 Cancel 逻辑,将预增加积分字段的 10 个积分扣减掉。
仓储服务也需要提供一个 sale_delivery() 接口的 Cancel 逻辑,将销售出库单的状态修改为“CANCELED”设置为已取消。
然后这个时候,订单服务的 TCC 分布式事务框架只要感知到了任何一个服务的 Try 逻辑失败了,就会跟各个服务内的 TCC 分布式事务框架进行通信,然后调用各个服务的 Cancel 逻辑。
大家看看下面的图,直观的感受一下:
[外链图片转存中...(img-eXvjXsdL-1698745720832)]
总结一下,你要玩儿TCC分布式事务的话:
1. 首先需要选择某种TCC分布式事务框架,各个服务里就会有这个TCC分布式事务框架在运行。
2. 然后你原本的一个接口,要改造为3个逻辑,Try-Confirm-Cancel。
- 先是服务调用链路依次执行Try逻辑
- 如果都正常的话,TCC分布式事务框架推进执行Confirm逻辑,完成整个事务
- 如果某个服务的Try逻辑有问题,TCC分布式事务框架感知到之后就会推进执行各个服务的Cancel逻辑,撤销之前执行的各种操作。
- 这就是所谓的TCC分布式事务。
- TCC分布式事务的核心思想,说白了,就是当遇到下面这些情况时,
1. 某个服务的数据库宕机了
2. 某个服务自己挂了
3. 那个服务的redis、elasticsearch、MQ等基础设施故障了
4. 某些资源不足了,比如说库存不够这些
- 先来Try一下,不要把业务逻辑完成,先试试看,看各个服务能不能基本正常运转,能不能先冻结我需要的资源。
- 如果Try都ok,也就是说,底层的数据库、redis、elasticsearch、MQ都是可以写入数据的,并且你保留好了需要使用的一些资源(比如冻结了一部分库存)。
- 接着,再执行各个服务的Confirm逻辑,基本上Confirm就可以很大概率保证一个分布式事务的完成了。
- 那如果Try阶段某个服务就失败了,比如说底层的数据库挂了,或者redis挂了,等等。
- 此时就自动执行各个服务的Cancel逻辑,把之前的Try逻辑都回滚,所有服务都不要执行任何设计的业务逻辑。保证大家要么一起成功,要么一起失败。
**终极大招:**
- 如果有一些意外的情况发生了,比如说订单服务突然挂了,然后再次重启,TCC分布式事务框架是如何保证之前没执行完的分布式事务继续执行的呢?
- TCC事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘上的日志文件里记录,也可以在数据库里记录。保存下来分布式事务运行的各个阶段和状态。
- 万一某个服务的Cancel或者Confirm逻辑执行一直失败怎么办呢?
- 那也很简单,TCC事务框架会通过活动日志记录各个服务的状态。
- 举个例子,比如发现某个服务的Cancel或者Confirm一直没成功,会不停的重试调用他的Cancel或者Confirm逻辑,务必要他成功!
- 当然了,如果你的代码没有写什么bug,有充足的测试,而且Try阶段都基本尝试了一下,那么其实一般Confirm、Cancel都是可以成功的!
- 如果实在解决不了,那么这个一定是很小概率的事件,这个时候发邮件通知人工处理
- seata、 go-seata
**TCC的缺点、优点**
**优点:**
1.解决了跨服务的业务操作原子性问题,例如组合支付,订单减库存等场景非常实用
2.TCC的本质原理是把数据库的二阶段提交上升到微服务来实现,从而避免了数据库2阶段中锁冲突的长事务低性能风险。
3.TCC异步高性能,它采用了try先检查,然后异步实现confirm,真正提交的是在confirm方法中。
**缺点:**
1.对微服务的侵入性强,微服务的每个事务都必须实现try,confirm,cancel等3个方法,开发成本高,今后维护改造的成本也高。
2.为了达到事务的一致性要求,try,confirm、cancel接口必须实现等幂性操作。
(定时器+重试)
3.由于事务管理器要记录事务日志,必定会损耗一定的性能,并使得整个TCC事务时间拉长,建议采用redis的方式来记录事务日志。
1. tcc需要通过锁来确保数据的一致性,会加锁导致性能不高
##### 1.4基于消息表的最终一致性
本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
下面以注册送积分为例来说明 :
下例共有两个微服务交互,用户服务和积分服务,用户服务负责添加用户,积分服务负责增加积分。
[外链图片转存中...(img-WeIsblsi-1698745720834)]
交互流程如下 :
**1、用户注册**
用户服务在本地事务新增用户和增加“积分消息日志”。(用户表和消息表通过本地事务保证一致)
下表是伪代码
```plain
begin transaction;
// 1.新增用户
// 2.存储积分消息日志
commit transation;
这种情况下,本地数据库操作与存储积分消息日志处于同一事务中,本地数据库操作与记录消息日志操作具备原子性。
2、定时任务扫描日志
如何保证将消息发送给消息队列呢?
经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。
3、消费消息
如何保证消费者一定能消费到消息呢?
这里可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重试向消费者来发送消息。
积分服务接收到“增加积分”消息,开始增加积分,积分增加成功后消息中间件回应ack,否则消息中间件将重复投递此消息。
由于消息会重复投递,积分服务的“增加积分”功能需要实现幂等性。
总结:上诉的方式是一种非常经典的实现,基本避免了分布式事务,实现了“最终一致性”。但是,关系型数据库的吞吐量和性能方面存在瓶颈,频繁的读写消息会给数据库造成压力。所以,在真正的高并发场景下,该方案也会有瓶颈和限制的。
[外链图片转存中…(img-hOTEKZo4-1698745720835)]
1.5基于可靠消息的最终一致性
ocketMQ是一个来自阿里巴巴的分布式消息中间件,于2012年开源,并在2017年正式成为Apache顶级项目。据了解,包括阿里云上的消息产品以及收购的子公司在内,阿里集团的消息产品全线都运行在RocketMQ之上,并且最近几年的双十一大促中,RocketMQ都有抢眼表现。Apache RocketMQ 4.3之后的版本正式支持事务消息,为分布式事务实现提供来便利性支持。
RocketMQ事务消息设计则主要是为了解决Producer端的消息发送与本地事务执行的原子性问题,RocketMQ的设计中broker与producer端的双向通信能力,使得broker天生可以作为一个事务协调者存在;而RocketMQ本身提供的存储机制为事务消息提供了持久化能力;RocketMQ的高可用机制以及可靠消息设计则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。
在RocketMQ 4.3后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部,解决Producer端的消息发送与本地事务执行的原子性问题。
[外链图片转存中…(img-ujg4Fhax-1698745720837)]
执行流程如下 :
为方便理解我们还以注册送积分的例子来描述整个流程。
Producer即MQ发送方,本例中是用户服务,负责新增用户。MQ订阅方即消息消费方,本例中是积分服务,负责新增积分。
1、Producer发送事务消息
Producer(MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预览状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。
2、MQ Server回应消息发送成功
MQ Server接收到Producer发送给的消息则回应发送成功表示MQ已接收到消息。
3、Producer执行本地事务
Producer端执行业务代码逻辑,通过本地数据库事务控制。
本例中,Producer执行添加用户操作。
4、消息投递
若Producer本地事务执行成功则自动向MQ Server发送commit消息,MQ Server接收到commit消息后将“增加积分消息”状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;
若Producer 本地事务执行失败则自动向MQ Server发送rollback消息,MQ Server接收到rollback消息后将删除“增加积分消息”。
MQ订阅方(积分服务)消费消息,消费成功则向MQ回应ack,否则将重复接收消息。这里ack默认自动回应,即程序执行正常则自动回应ack。
5、事务回查
如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其他Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。
以上主干流程已由RocketMQ实现,对用户则来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。
2.幂等性机制
2.1常见幂等性解决方案
哪些情况下需要考虑幂等性 - 同样的请求发送多次:
a. http请求的类型:
ⅰ. get
1. 获取商品信息, 这个会引起商品的数据的变化吗?
ⅱ. post
1. 比较常见,这种接口需要考虑到幂等性
ⅲ. put
1. 不一定要实现幂等性
a. put 把1号商品的价格改为200,网络返回的时候抖动了,重试
b. 第二次接口还是会把1号商品的价格改为200 - 这种情况下没有幂等的问题
2. 出现幂等性问题的情况:
a. 购物车中的商品,调用一次 这个商品的数量加一
ⅰ. 第一次调用 原本的值 10 之后价格变为11 - 但是返回的时候出现了网络抖动
ⅱ. 第二次发送 原本的值 11 之后价格变为12 - 但是返回的时候出现了网络抖动
ⅲ. 第三次发送 原本的值 12 之后价格变为13- 但是返回的时候出现了网络抖动
ⅳ. delete
1. 一般不具备幂等性的要求
2. 第一次调用 删除数据
3. 第二次调用 还是删除当前的数据
**现在假设,你自己开发了一个支付宝,然后别人在二维码支付页面,先支付了,结果由于网络问题,当前页面一直没有刷新,就让人以为我没有支付,然后我就再次扫码支付,这下完蛋了:你扣了两次款。**
##### 2.2背景
```go
我们实际系统中有很多操作,是不管做多少次,都应该产生一样的效果或返回一样的结果。
get请求一般没有幂等性需求、delete请求一般也没有幂等性需求,post、update视情况而定例如:
1. 前端重复提交选中的数据,应该后台只产生对应这个数据的一个反应结果。
2. 我们发起一笔付款请求,应该只扣用户账户一次钱,当遇到网络重发或系统bug重发,也应该只扣一次钱;
3. 发送消息,也应该只发一次,同样的短信发给用户,用户会哭的;
4. 创建业务订单,一次业务请求只能创建一个,创建多个就会出大问题。
等等很多重要的情况,这些逻辑都需要幂等的特性来支持。
2.3概念
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“getUsername()和setTrue()”函数就是一个幂等函数.
更复杂的操作幂等保证是利用唯一交易号(流水号)实现.
我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的
2.4技术方案
1. 查询操作
查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作
2. 删除操作
删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个)
1.唯一索引,防止新增脏数据
比如:新建用户的时候将手机号码设置为唯一索引,那么即使你重试,也只会新建一个用户,不会因为重试导致当前用户注册了两个用户
要点:
唯一索引或唯一组合索引来防止新增数据存在脏数据
(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可)
2.token制制,防止页页重复提交
业务要求:
页面的数据只能被点击提交一次
发生原因:
由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交
解决办法:
集群环境:采用token加redis(redis单线程的,处理需要排队)
处理流程:
1. 数据提交前要向服务的申请token,token放到redis或内存,token有效时间
2. 提交后后台校验token,同时删除token,生成新的token返回
token特点:
要申请,一次有效性,可以限流
注意:redis要用删除操作来判断token,删除成功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用
3.悲观锁
获取数据的时候加锁获取
select * from table_xxx where id='xxx' for update;
注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用
//注意:id字段一定是主键或者唯一索引,不然是锁表,会死人的
//悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用
4.乐观锁
乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高。
乐观锁的实现方式多种多样可以通过version或者其他状态条件:
1. 通过版本号实现
update table_xxx set name=#name#,version=version+1 where version=#version#
2. 通过条件限制
update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0
要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高
5.分布式锁
还是拿插入数据的例子,如果是分布是系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。
//要点:某个长流程处理过程要求不能并发执行,可以在流程执行之前根据某个标志