01-事务和分布式事务浅谈

61 阅读12分钟

事务是什么

事务就是一系列操作,官方一点就是事务是一个逻辑工作单元,它包含了一系列的操作,这些操作要么全部成功执行,要么全部不执行。在数据库系统中,事务是访问并可能更新数据库中各种数据项的一个程序执行单元。

在数据库里,传统意义上讲只要是对数据库的访问操作都是一个事务。

事务有什么特性呢?

想必大家都知道:ACID(转账的例子)

  • 原子性(Atomicity)

  • 事务是一个不可分割的工作单位,事务中的操作要么全部执行,要么全部不执行。就像银行转账的例子,如果在从账户 A 扣款成功后,系统出现故障,无法完成向账户 B 的存款操作,那么系统应该回滚,将账户 A 的扣款操作撤销,以保证数据的一致性。

  • 一致性(Consistency)

  • 事务必须使数据库从一个一致性状态变换到另一个一致性状态。在转账事务中,系统要保证总金额不变。即转账前账户 A 和账户 B 的金额总和等于转账后两个账户金额的总和。

  • 隔离性(Isolation)

  • 多个事务并发执行时,一个事务的执行不能被其他事务干扰。比如有两个用户同时对账户 A 进行操作,一个是查询余额,另一个是进行转账操作。查询余额的事务应该不受转账事务的影响,看到的是转账操作前或者转账操作后完整状态下的余额,而不是中间混乱的状态。

  • 持久性(Durability)

  • 一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。即使系统出现故障,如断电、软件崩溃等情况,已经提交的事务的数据修改也不会丢失。例如,转账成功后,即使银行系统服务器重启,账户 A 和账户 B 的金额也已经更新,不会回到转账前的状态。

事务的实现机制

  • 在数据库管理系统中,事务是通过日志文件和锁机制来实现的。日志文件记录了事务对数据库的所有操作,用于事务的回滚和重做。锁机制则用于控制多个事务对数据库资源的并发访问,避免数据不一致的情况发生。例如,当一个事务对某一行数据进行更新操作时,会对该行数据加锁,其他事务如果要访问该行数据,就需要等待锁释放。

事务谁先谁后呢?

根据逻辑时间戳,Oracle 的话是SCN,MySQL (InnoDB)是Trx_id事务id,本身也是逻辑时间戳。

事务处理:

业务发生错误进行回滚 (日志记录反向的操作,方便错误时进行回滚)

系统down机进行恢复 (recovery)

这些其实都是通过日志进行恢复。

死锁产生的原因是什么呢?

  • 两个线程
  • 不同的方向
  • 相同的资源

如何避免死锁呢? ①尽可能不死锁,降低隔离级别

②采用碰撞检测 (用的较多)

③等锁超时 (较少)

事务调优有哪些原则呢?

  • 在不影响业务的情况下,减少锁的范围。例如从MyIsam的表锁到InnoDB的行锁。

  • 增加锁上可执行的线程数

  • 读写锁分离

  • 多线程并行读取

  • 允许更多人读取

  • 选取正确的锁类型

  • 悲观锁:针对写多读少

  • 乐观锁:读多写少(更多场景)

事务隔离级别(重点)

为什么会出现事务隔离级别? 并发带来的数据不一致问题

  • 在数据库系统中,多个事务可能会同时运行,这种并发操作如果不加以控制,就会导致数据不一致的情况。例如,假设有两个事务 T1 和 T2 同时对同一个账户余额进行操作。T1 是查询余额操作,T2 是更新余额操作(比如取款)。

  • 脏读(Dirty Read)

  • 如果 T2 在修改余额但尚未提交事务时,T1 读取了这个被修改后的余额,然后 T2 因为某种原因回滚了操作。那么 T1 读取的数据就是 “脏数据”,这种情况就是脏读。例如,银行系统中,事务 T2 从账户中扣除 100 元准备转账,但未提交,事务 T1 此时查询余额并显示扣除后的余额,随后 T2 转账失败回滚,余额应该恢复原状,但 T1 读取到的却是错误的余额。

  • 不可重复读(Non - Repeatable Read)

  • 事务 T1 在读取数据的过程中,事务 T2 对同一数据进行了修改并提交。导致 T1 两次读取同一数据却得到不同的结果。例如,事务 T1 先读取账户余额为 1000 元,然后事务 T2 进行取款操作并提交,将余额修改为 900 元。当 T1 再次读取余额时,发现余额变为 900 元,这种在一个事务中多次读取同一数据却不一致的情况就是不可重复读。

  • 幻读(Phantom Read)

  • 事务 T1 按照某个条件查询数据后,事务 T2 插入或删除了满足该条件的一些数据,然后 T1 再次按照相同条件查询时,发现结果集的数量发生了变化。例如,事务 T1 查询账户余额大于 1000 元的账户数量,得到有 5 个账户。然后事务 T2 插入了一个余额大于 1000 元的新账户并提交。当 T1 再次查询余额大于 1000 元的账户数量时,发现有 6 个账户,这种情况就是幻读。幻读主要是针对插入和删除操作导致的数据不一致。

解决方案:事务隔离级别应运而生

  • 为了解决上述并发事务导致的数据不一致问题,数据库系统引入了事务隔离级别。不同的隔离级别可以在一定程度上允许或限制并发事务之间的相互影响,从而平衡数据一致性和系统性能。

  • 读未提交(Read Uncommitted)

  • 这是最低的隔离级别。在这个级别下,一个事务可以读取另一个事务未提交的数据。这种隔离级别可能会出现脏读、不可重复读和幻读等问题,但它的优点是性能相对较高,因为对并发事务的限制较少。例如,在一些对数据实时性要求极高但对数据准确性要求稍低的场景下,如某些实时数据监控系统,可能会采用这种隔离级别。

  • 读已提交(Read Committed)

  • 这个隔离级别可以避免脏读。一个事务只能读取另一个事务已经提交的数据。不过,它仍然可能出现不可重复读和幻读的问题。在大多数数据库应用场景中,这种隔离级别比较常用,因为它在保证数据相对准确性的同时,也有较好的性能。例如,在普通的订单处理系统中,用户查询订单状态时,不希望看到未提交的、可能会回滚的订单状态变化,就可以采用读已提交的隔离级别。

  • 可重复读(Repeatable Read)

  • 可重复读隔离级别可以避免脏读和不可重复读。在一个事务的执行过程中,它多次读取同一数据会得到相同的结果。但是,它仍然可能出现幻读的情况。例如,在一些需要对数据进行多次读取且要求数据稳定性的统计分析系统中,可采用这种隔离级别。

  • 串行化(Serializable)

  • 这是最高的隔离级别,它可以避免脏读、不可重复读和幻读。在这个隔离级别下,事务是串行执行的,就像单线程一样,一个事务完全执行完后,另一个事务才开始执行。虽然它能完全保证数据一致性,但会严重影响系统性能,因为并发程度很低。例如,在对数据一致性要求极高的金融核心系统的某些关键操作中,可能会采用串行化隔离级别。

值得一提的是,MySQL的隔离级别是RR,也就是可重复读,但是他也解决了幻读的问题,主要借助于Next - Key Locking 机制。

  • Next - Key Locking 是行锁和间隙锁的组合。它会锁定一个范围,这个范围包括索引记录本身以及索引记录之间的间隙。在可重复读隔离级别下,当一个事务执行一个范围查询(例如,SELECT * FROM users WHERE age BETWEEN 20 AND 30)时,MySQL 会使用 Next - Key Locking 来锁定这个范围。

  • 具体来说,对于上述查询,MySQL 会对年龄为 20 岁的用户记录加行锁,同时对 20 - 30 岁之间的间隙加间隙锁,并且对年龄为 30 岁的用户记录也加行锁。这样,其他事务就无法在这个范围内插入新的用户记录(会被间隙锁阻止),也无法修改这个范围内已有的用户记录(会被行锁阻止),从而在一定程度上解决了幻读问题。

  • 行锁:行锁是对数据库表中的某一行数据进行锁定。当一个事务对某一行进行操作(如更新、删除)时,会对该行加行锁,防止其他事务对这一行进行冲突操作。例如,事务 A 要更新用户表中用户 ID 为 1 的用户信息,会对用户 ID 为 1 这一行加行锁,此时事务 B 如果也想更新这一行就需要等待事务 A 释放行锁。

  • 间隙锁:间隙锁是对索引记录中的间隙进行锁定。间隙是指索引记录之间的范围,比如在一个按照年龄排序的用户索引表中,年龄为 20 岁和 21 岁的用户记录之间就存在一个间隙。间隙锁的作用是防止其他事务在这个间隙中插入新的数据。例如,事务 A 查询年龄在 20 - 30 岁之间的用户信息,它会对这个年龄区间的行加上行锁,同时对 20 岁以下和 30 岁以上的相邻间隙加上间隙锁。

MVCC机制,这里也是讲了一些,主要基于版本快照机制。undo log 实现可重复读

分布式事务

分布式带来的是:

  • 扩展能力
  • 安全能力
  • 高可用能力

但同时带来了问题

  • 数据共享问题

  • 不可达问题

  • 更多的延迟等

  • 数据一致性问题更复杂

  • 由于数据分布在不同的节点上,保证各个节点数据的一致性更加困难。在分布式事务中,可能会出现部分节点事务提交成功,部分节点事务提交失败的情况。比如在上述电商系统中,订单记录插入成功,但是库存减少失败,就会导致数据不一致。

  • 网络通信问题

  • 分布式系统中的节点之间需要通过网络进行通信。网络的不可靠性,如延迟、丢包、网络分区等情况,会对分布式事务产生影响。例如,在分布式事务执行过程中,一个节点向另一个节点发送的提交事务的请求因为网络延迟而没有及时到达,就可能导致事务无法正常完成。

  • 协调和管理复杂

  • 分布式事务需要协调多个节点的事务执行,需要一个全局的事务管理器来管理事务的开始、提交和回滚等操作。而且不同节点可能使用不同的数据库管理系统或者服务框架,这增加了协调的难度。

分布式事务的解决方案

  • 两阶段提交协议(2PC)

  • 第一阶段是准备阶段,事务管理器向所有参与者发送准备请求,参与者执行事务操作并记录日志,但不提交事务。参与者将执行结果(同意或拒绝)返回给事务管理器。第二阶段是提交阶段,如果事务管理器收到所有参与者的同意消息,就向所有参与者发送提交请求,参与者提交事务;如果有一个参与者拒绝,事务管理器就向所有参与者发送回滚请求。不过 2PC 存在单点故障(事务管理器故障)和阻塞问题(部分参与者等待事务管理器指令而无法释放资源)。

  • 三阶段提交协议(3PC)

  • 在 2PC 的基础上进行了改进,增加了一个预提交阶段,用于在提交阶段之前再次确认参与者的状态,减少了参与者的等待时间,一定程度上解决了 2PC 的阻塞问题。但是 3PC 也有其复杂性和性能开销

  • 分布式事务框架(如 Seata)

  • 这些框架提供了更高级的抽象和功能来处理分布式事务。它们可以自动处理事务的协调、回滚和提交等操作,并且能够更好地适应复杂的分布式系统环境,通过对业务进行侵入式或者非侵入式的改造,实现分布式事务的管理。

文章部分来源:

慕课网中在线分布式事务实践,其实讲的还是比较粗犷的,整体上讲了一下事务和分布式事务,主要是还是讲的单机事务多一点。讲的还是比较通俗,针对事务的特性还有事务处理以及死锁等做了说明,在深入单机事务里重点讲了ACID,在事务隔离级别里面其实个人觉得MVCC机制讲的不够深入,可能也是作者的初衷,带大家初步的有个整体的概括,更加细节的可以参考专栏里的MySQL或者掘金小册里的MySQL(非常详细,深入源码级别,我没看完 )。