事务
我们设想一个场景,这个场景中我们需要插入多条相关联的数据到数据库,不幸的是,这个过程可能会遇到下面这些问题:
- 数据库中途突然因为某些原因挂掉了。
- 客户端突然因为网络原因连接不上数据库了。
- 并发访问数据库时,多个线程同时写入数据库,覆盖了彼此的更改。
- ......
数据库事务
大多数情况下,我们在谈论事务的时候,如果没有特指分布式事务,往往指的就是数据库事务。
数据库事务在我们日常开发中接触的最多了。如果你的项目属于单体架构的话,你接触到的往往就是数据库事务了。
那数据库事务有什么作用呢?
简单来说,数据库事务可以保证多个对数据库的操作(也就是SQL语句)构成一个逻辑上的整体。构成这个逻辑 上的整体的这些数据库操作遵循:要么全部执行成功,要么全部不执行。
# 开启一个事务
START TRANSACTION;
# 多条 SQL 语句
SQL1,SQL2...
## 提交事务
COMMIT;
另外,关系型数据(例如:MySQL、SQL Server、Oracle 等)事务都有ACID特性:
- 原子性 (
Atomicity
):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; - 一致性(
Consistency
): 执行事务前后数据保持一致,例如转账业务中无论事务是否成功,转账者和收款人的总额应该是不变的; - 隔离性(
Isolation
): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; - 持久性(
Durabilily
): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
这里要额外补充一点:只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!
想必大家也和我一样,被ACID 这个概念被误导了很久! 我也是看周志明老师的公开课《周志明的软件架构课》才搞清楚的(多看好书!!!)。
数据事务的实现原理呢?
我们这里以MySQL的InnoDB引擎为例来简单说一下。
MySQL InnoDB
引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。
MySQL InnoDB引擎通过 锁机制、MVCC 等手段来保证事务的隔离性(默认支持的隔离级别是REPEATABLE-READ)。
分布式事务
微服务架构下,一个系统被拆分为多个小的微服务。每个微服务都可能存在不同的机器上,并且每个微服务可能都有一个单独的数据库供自己使用。这种情况下,一组操作可能会涉及到多个微服务以及多个数据库。
举个例子:电商系统中,你创建一个订单往往会涉及到订单服务(订单数加一)、库存服务(库存减一)等等服务,这些服务会有供自己单独使用的数据库。
那么如何保证这一组操作要么都执行成功,要么都执行失败呢?
这个时候单单依靠数据库事务就不行了!我们就需要引入 分布式事务 这个概念了!
实际上,只要跨数据库的场景都需要用到引入分布式事务。比如说单个数据库的性能达到瓶颈或者数据量太大的时候,我们需要进行 分库。分库之后,同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了。
一言蔽之,分布式事务的终极目标就是保证系统中多个相关联的数据库中的数据的一致性!
那既然分布式事务也属于事务,理论上就应该准守事务的 ACID 四大特性。但是,考虑到性能、可用性等各方面因素,我们往往是无法完全满足 ACID 的,只能选择一个比较折中的方案。
针对分布式事务,又诞生了一些新的理论。
分布式事务基础理论
CAP理论和BASE理论
CAP 理论和 BASE 理论是分布式领域非常非常重要的两个理论。不夸张地说,只要问到分布式相关的内容,面试官几乎是必定会问这两个分布式相关的理论。
不论是你面试也好,工作也罢,都非常有必要将这两个理论搞懂,并且能够用自己的理解给别人讲出来。
我这里就不多提这两个理论了,不了解的小伙伴,可以看我前段时间写过的一篇相关的文章:《CAP 和 BASE 理论了解么?可以结合实际案例说下不?》
一致性的 3 种级别
我们可以把对于系统一致性的要求分为下面3种级别:
- 强一致性:系统写入了什么,读出来的就是什么。
- 弱一致性:不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只是会尽量保证某个时刻达到数据一致的状态。
- 最终一致性:弱一致性的升级版。系统会保证在一定时间内达到数据一致的状态
除了上面这 3 个比较常见的一致性级别之外,还有读写一致性、因果一致性等一致性模型,具体可以参考《Operational Characterization of Weak Memory Consistency Models》这篇论文。因为日常工作中这些一致性模型很少见,我这里就不多做阐述。
业界比较推崇是 最终一致性,但是某些对数据一致要求十分严格的场景比如银行转账还是要保证强一致性。
柔性事务
互联网应用最关键的就是要保证高可用, 计算式系统几秒钟之内没办法使用都有可能造成数百万的损失。在此场景下,一些大佬们在 CAP 理论和 BASE 理论的基础上,提出了 柔性事务 的概念。 柔性事务追求的是最终一致性。
实际上,柔性事务就是 BASE 理论 +业务实践。 柔性事务追求的目标是:我们根据自身业务特性,通过适当的方式来保证系统数据的最终一致性。 像 TCC、Saga、MQ 事务 、本地消息表 就属于柔性事务。
刚性事务
与柔性事务相对的就是 刚性事务 了。前面我们说了,柔性事务追求的是最终一致性。那么,与之对应,刚性事务追求的就是 强一致性。
像2PC 、3PC 就属于刚性事务。
分布式事务解决方案
分布式事务的解决方案有很多,比如:2PC、3PC、TCC、本地消息表、MQ 事务(Kafka 和 RocketMQ 都提供了事务相关功能)、Saga 等等。这些方案的适用场景有所区别,我们需要根据具体的场景选择适合自己项目的解决方案。
2PC(两阶段提交协议)
2PC(Two-Phase Commit)这三个字母的含义:
● 2 -> 指代事务提交的 2 个阶段
● P-> Prepare (准备阶段)
● C ->Commit (提交阶段)
2PC 将事务的提交过程分为 2 个阶段:准备阶段 和 提交阶段 。
准备阶段(Prepare)
准备阶段的核心是“询问”事务参与者执行本地数据库事务操作是否成功。
- 事务协调者/管理者 向所有参与者发送消息询问:“你是否可以执行事务操作呢?”,并等待其答复。
- 事务参与者 接收到消息之后,开始执行本地数据库事务预操作比如写 redo log/undo log 日志。但是,此时并不会提交事务!
- 事务参与者 如果执行本地数据库事务操作成功,那就回复:“就绪”,否复:“未就绪”。
提交阶段(Commit)
提交阶段的核心是“询问”事务参与者提交事务是否成功。
当所有事务参与者都是“就绪”状态的话:
- 事务协调者/管理者 向所有参与者发送消息:“你们可以提交事务啦!”(commit 消息)
- 事务参与者 接收到 commit 消息 后执行 提交本地数据库事务 操作,执行完成之后 释放整个事务期间所占用的资源。
- 事务参与者 回复:“事务已经提交”(ack 消息)
- 事务协调者/管理者 收到所有 事务参与者 的 ack 消息 之后,整个分布式事务过程正式结束。
总结
简单总结一下 2PC 两阶段中比较重要的一些点:
- 准备阶段 的主要目的是测试 事务参与者 能否执行 本地数据库事务 操作 (!!!注意:这一步并不会提交事 务)。
- 提交阶段 中 事务协调者/管理者 会根据 准备阶段 中 事务参与者 的消息来决定是执行事务提交还是回滚操 作。
- 提交阶段 之后一定会结束当前的分布式事务
优点与存在的问题
2PC 的优点:
● 实现起来非常简单,各大主流数据库比如MySQL、Oracle 都有自己实现。
● 针对的是数据强一致性。不过,仍然可能存在数据不一致的情况。
2PC 存在的问题:
● 同步阻塞:事务参与者会在正式提交事务之前会一直占用相关的资源。比如用户小明转账给小红,那其他事务也要操作用户小明或小红的话,就会阻塞。
● 数据不一致:由于网络问题或者事务协调者/管理者宕机都有可能会造成数据不一致的情况。比如在第2阶段(提交阶段),部分网络出现问题导致部分参与者收不到commit/rollback 消息的话,就会导致数据不一致。
● 单点问题: 事务协调者/管理者在其中也是一个很重要的角色,如果事务协调者/管理者在准备(Prepare)阶段完成之后挂掉的话,事务参与者就会一直卡在提交(Commit)阶段。
3PC(三阶段提交协议)
3PC 是人们在 2PC 的基础上做了一些优化得到的。3PC 把 2PC 中的 准备阶段(Prepare) 做了进一步细 化,分为 2 个阶段:
- 询问阶段(CanCommit):这一步 不会执行事务操作,只会询问事务参与者能否执行本地数据库事操作。
- 准备阶段(PreCommit):当所有事物参与者都返回“可执行”之后, 事务参与者才会执行本地数据库事务预操作比如写 redo log/undo log 日志。
TCC补偿事务
简单来说,TCC 是 Try、Confirm、Cancel 三个词的缩写,它分为三个阶段:
-
1.Try (尝试)阶段:尝试执行。完成业务检查,并预留好必需的业务资源。
-
2.Confirm(确认)阶段:确认执行。当所有事务参与者的 Try 阶段执行成功就会执行 Confirm ,Confirm 阶段会处理 Try 阶段预留的业务资源。否则,就会执行 Cancel 。
-
3.Cancel (取消) 阶段:取消执行,释放 Try 阶段预留的业务资源。
我们拿转账场景来说:
- 1.Try (尝试) 阶段:在转账场景下,Try要做的事情是就是检查账户余额是否充足,预留的资源就是转账资金。
- 2.Confirm(确认)阶段:如果 Try 阶段执行成功的话,Confirm 阶段就会执行真正的扣钱操作。
- 3.Cancel(取消)阶段:释放 Try 阶段预留的转账资金。
一般情况下,当我们使用 TCC 模式的时候,需要自己实现 try , confirm , cancel 这三个方法,来达到最终一致性。也就是说,正常情况下会执行 try , confirm ,如下图所示。
出现异常的话会执行try, cancel ,如下图所示。
因此,TCC 模式不需要依赖于底层数据资源的事务支持,但是需要我们手动实现更多的代码,属于 侵入 业务代码 的一种分布式解决方案。
针对 TCC 的实现,业界也有一些不错的开源框架。不同的框架对于 TCC 的实现可能略有不同,不过大致思想都一样。
- ByteTCC : ByteTCC 是基于 Try-Confirm-Cancel (TCC) 机制的分布式事务管理器的实现。相关阅读:关于如何实现一个 TCC 分布式事务框架的一点思考
- Seata :Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
- Hmily:金融级分布式事务解决方案。