浅浅了解分布式事务
; 3355 words
目录
关系型数据库中存在事务机制,可以保证每个独立节点的操作满足ACID。
分布式事务产生的原因主要是存储和服务的拆分。存储层拆分如数据库分库分表,服务层拆分即业务的服务化,因为解耦合,许多核心业务如服务、商品、订单、库存等变成了独立的服务,业务逻辑的执行散落在不同服务器上。
因此问题也会出现,下单操作同时依赖订单、库存、支付服务,这三个服务如果有一个失败,下单操作则失败,而这种事务性,就需要分布式事务,保证所有节点的数据写操作要么全都执行,要么全都不执行。
解决方案
分布式事务的解决方案有两阶段和三阶段提交协议、TCC分段提交、基于消息队列的最终一致性设计。
对于分布式一致性算法,都是通过leader进程进行协调,2PC和3PC也是同样的思想。
two-phase commit protocol
two-phase即Commit-request和Commit。
对于2PC算法,基于以下假设:
- 存在一个节点作为协调者Coordinator,其他节点作为参与者Participants,节点之间可以进行网络通信。
- 所有节点采取预写式日志,日志被写入后保存在可靠的存储设备,节点宕机也不会导致日志的丢失。
- 所有节点不会永久损坏,损坏后仍可以恢复
提交请求阶段
Coordinator通知Participants准备提交事务,进入表决过程。在表决过程中,Participants告诉Coordinator自己的决策:同意(Participants本地事务执行成功)或取消(Participants本地事务故障)
提交阶段
Coordinator基于第一阶段的投票结果进行决策:提交或取消事务,必需当且仅当所有Participants同意提交,Coordinator才会通知Participants提交事务,否则会取消事务。
Participants收到通知后,在本地进行Commit或Rollback。
问题
- 资源被同步阻塞
执行过程,所有参与节点都是事务独占状态,当参与者占用公共资源,第三房节点访问公共资源会被阻塞
- Coordinator单点故障
一旦Coordinator出现故障,Participants会一直阻塞
- Commit阶段数据不一致
提交阶段,Coordinator发送的事务通知如果由于网络问题仅被一部分Participants收到,没有收到的Participants会一直阻塞,数据出现不一致。
three-phase commit protocol
为了解决2PC的同步阻塞问题,3PC引入了超时机制,把2PC的提交请求阶段拆分为两步:询问、锁资源,然后进入提交阶段。
3PC的三个阶段分别是 CanCommit、PreCommit、DoCommit。
CanCommit
类似于2PC的请求提交阶段,Coordinator向Participants发送Can-Commit请求,Participants返回yes 或 no。
PreCommit
- 所有Participants的反馈都是yes,就会进行事务的预执行。
- Coordinator向Participants发送pre-commit请求,进入prepared阶段。
- Participants接收pre-commit请求,执行事务操作。
- Participants成功执行事务后,返回ACK响应,等待最终指令。
- 如果存在Participants向Coordinator发送no,或等待超时都没有接收到Participants的响应,中断事务。
- 发送中断请求,Coordinator向Participants发送abort请求。
- 中断事务,Participants执行事务的中断。
DoCommit
- 执行提交
- 发送提交请求。Coordinator接收到Participants发送的 ACK 响应后,从预提交状态进入到提交状态,并向所有Participants发送 doCommit 请求。
- 事务提交。Participants接收到 doCommit 请求之后,执行正式的事务提交,并在完成事务提交之后释放所有事务资源。
- 响应反馈。事务提交完之后,向Coordinator发送 ACK 响应。
- 完成事务。Coordinator接收到所有Participants的 ACK 响应之后,完成事务。
- 中断事务 Coordinator没有接收到Participants发送的 ACK 响应,可能是因为接受者发送的不是 ACK 响应,也有可能响应超时了,那么就会执行中断abort事务。
3.超时提交 Participants如果没有收到Coordinator的通知,超时之后会执行 Commit 操作。
改进
- 引入超时机制。2PC只有Coordinator拥有超时机制。
- 添加pre-commit阶段,作为缓冲,保证最后提交阶段各Participants节点状态一致。
问题
在do-commit阶段,如果participants接收到pre-commit消息仍会进行事务的提交,导致数据不一致。
Try-Confirm-Cancel
TCC 的核心思想是针对每个业务操作,添加一个与其对应的确认和补偿操作,同时把相关的处理,从数据库转移到业务中,以此实现跨数据库的事务。
Try
Try操作一般都是锁定某个资源,设置预备状态,尝试执行业务,完成所有业务检查,预留业务资源。
Confirm/Cancel
两阶段是互斥的,只能进入其中一个阶段,并且阶段都满足幂等性,允许失败重试。
- Confirm:对业务系统做确认提交,确认执行业务操作
- Cancel:业务执行错误需要回滚时,执行业务取消,释放预留资源
应用
TCC的本质是把数据库的二阶段提交上升到微服务,TCC对业务进行拆解。依然拿支付过程演示
- Try 操作
订单服务添加预备状态,修改为 UPDATING,冻结当前订单的操作,而不是直接修改为支付成功。
库存服务设置冻结库存,可以扩展字段,也可以额外添加新的库存冻结表。积分服务和库存一样,添加一个预增加积分,比如本次订单积分是 100,添加一个额外的存储表示等待增加的积分,账户余额服务等也是一样的操作。
- Confirm 操作
Confirm 操作就是把前边的 Try 操作锁定的资源提交,类比数据库事务中的 Commit 操作。在支付的场景中,包括订单状态从准备中更新为支付成功;库存数据扣减冻结库存,积分数据增加预增加积分。
- Cancel 操作
Cancel 操作执行的是业务上的回滚处理,类比数据库事务中的 Rollback 操作。首先订单服务,撤销预备状态,还原为待支付状态或者已取消状态,库存服务删除冻结库存,添加到可销售库存中,积分服务也是一样,将预增加积分扣减掉。
业务请求过来,执行 Try 操作,如果 TCC 分布式事务框架感知到各个服务的 Try 阶段都成功了以后,就会执行各个服务的 Confirm 逻辑。如果 Try 阶段有操作不能正确执行,比如订单失效、库存不足等,就会执行 Cancel 的逻辑,取消事务提交。
对比XA
- 第一阶段
在 XA(分布式事务处理规范) 事务中,各个 RM (资源管理器)准备提交各自的事务分支,即准备提交资源的更新操作(insert、delete、update 等);而在 TCC 中,是主业务操作请求各个子业务服务预留资源。
- 第二阶段
XA 事务根据第一阶段每个 RM 是否都 prepare 成功,判断是要提交还是回滚。如果都 prepare 成功,那么就 commit 每个事务分支,反之则 rollback 每个事务分支。
在 TCC 中,如果在第一阶段所有业务资源都预留成功,那么进入 Confirm 步骤,提交各个子业务服务,完成实际的业务处理,否则进入 Cancel 步骤,取消资源预留请求。
- 2PC/XA 是数据库或者存储资源层面的事务,实现的是强一致性,在两阶段提交的整个过程中,一直会持有数据库的锁。
- TCC 关注业务层的正确提交和回滚,在 Try 阶段不涉及加锁,是业务层的分布式事务,关注最终一致性,不会一直持有各个业务资源的锁。
基于消息补偿的最终一致性
由本地消息表和第三方可靠信息队列组成。本地消息表核心思想是将分布式事务拆分成本地事务处理,通过消息日志的方式异步执行。消息生产方即rpc上游,额外建立一个事务消息表,记录消息发送状态,消息消费方需要处理这个消息,并完成自己的业务逻辑,异步机制定期扫描未完成的消息,确保最终一致性。
如最初提出的下单减库存业务,简单模拟过程:
(1)系统收到下单请求,将订单业务数据存入到订单库中,同时存储该订单对应的消息数据,如商品ID 和数量等,消息数据与订单库为同一库,更新订单和存储消息为一个本地事务,要么都成功,要么都失败。(2)库存服务通过消息中间件收到库存更新消息,调用库存服务进行业务操作,同时返回业务处理结果。
(3)消息生产方,即订单服务,收到处理结果后,将本地消息表的数据删除或者设置为已完成。
(4)设置异步任务,定时去扫描本地消息表,发现有未完成的任务则重试,保证最终一致性。