数据一致性问题剖析与实践(二)——单机事务的一致性问题
一、前言
对问题定义进行了扩展,是一个综合性问题,也会涉及竞态条件竞争,冗余数据存储。
事务的起源来自于数据库,其最重要的定义就是,要么全部成功执行,要么全部不执行,保证安全的状态转化。
之前我们讨论了几种场景的一致性问题
- 冗余数据存储中的一致性问题
- 分布式共识中的一致性问题
本文将围绕单机事务中的一致性问题展开讨论。
二、事务是什么?
2.1 本质
数据库事务的本质是一组数据库操作的集合,这些操作要么全部成功执行,要么全部不执行,以保证数据库从一个一致性状态转换到另一个一致性状态。
事务这个概念在业务逻辑中非常常见,有非常多的实际场景需要。
2.2 事务特性 ——ACID
单机事务的 ACID 特性,是保障数据正确性与一致性的基础准则
-
原子性(Atomicity):事务被视为一个不可分割的整体,所有操作要么全部成功提交,要么全部失败回滚。例如在银行转账中,从账户 A 扣款与向账户 B 入账必须同时完成,若扣款成功但入账失败,整个事务需回滚,确保资金不会凭空消失或增加 。
-
隔离性(Isolation):多个并发事务之间相互隔离,互不干扰。不同隔离级别(读未提交、读已提交、可重复读、串行化)定义了事务间对数据访问的可见范围,避免脏读、不可重复读和幻读等问题 。
其本质是控制数据的访问范围,在并发场景下,实现不同隔离级别下的“对外一致性”。
-
持久性(Durability):一旦事务提交成功,其对数据的修改将永久保存,即使系统出现故障(如断电、宕机)也不会丢失。银行交易完成后,账户余额的变更会持久化存储,确保数据的可靠性 。
-
一致性(Consistency):事务执行前后,数据需从一个合法状态转换到另一个合法状态。如电商下单时,库存数量必须与订单数量保持逻辑一致,不能出现超卖现象,以维持业务规则的正确性 。
只有保证了原子性、隔离性、持久性的前提下,才能实现一致性,一致性是事务的目的。
三、单机事务中的一致性问题
以mysql为例
3.1 隔离性
隔离性是确保不同事务之间相互隔离、互不干扰,在并发执行时,每个事务都能如同在单线程环境下一样独立地运行,不受其他事务的影响。
其本质是控制数据的访问范围,在并发场景下,实现不同隔离级别下的“对外一致性”。
3.1.1 产生场景
- 脏读:当一个事务读取到另一个未提交事务修改的数据时,就会发生脏读。例如,事务 A 对某条数据进行了修改,但尚未提交,此时事务 B 读取了这条被修改但未提交的数据,若事务 A 随后回滚了修改,那么事务 B 读取到的数据就是无效的,这就是脏读现象。在银行系统中,如果一个转账事务正在处理(未提交),另一个查询事务读取了转账过程中的临时余额,就可能出现脏读问题。
- 不可重复读:在同一事务中,多次读取同一数据却得到不同的结果,原因是其他事务在此期间对该数据进行了修改并提交。比如,事务 A 先读取了某客户的账户余额,然后其他事务 B 对该账户进行了存款操作并提交,当事务 A 再次读取该账户余额时,得到的结果与第一次不同,这就产生了不可重复读的问题。
- 幻读:在一个事务中执行查询操作时,由于其他事务插入或删除了符合查询条件的数据,导致该事务再次执行相同查询时得到了不同的结果集。例如,事务 A 查询某类商品的库存数量,然后事务 B 插入了一些该类商品的库存记录并提交,当事务 A 再次查询时,发现库存数量增加了,就好像出现了 “幻觉”,这就是幻读现象。
3.1.2 Mysql中的事务隔离级别
- 读未提交(Read Uncommitted)级别最低,几乎不提供隔离保证,容易出现脏读、不可重复读和幻读;
- 读已提交(Read Committed)可以避免脏读,但可能会出现不可重复读和幻读;
- 可重复读(Repeatable Read)是 MySQL 的默认隔离级别,它可以避免脏读和不可重复读,但无法解决幻读;
- 串行化(Serializable)是最高的隔离级别,它通过强制事务串行执行,完全避免了脏读、不可重复读和幻读,但会严重影响并发性能。
3.1.3 解决范式
- 读屏障:通过控制读取的逻辑(比如读版本快照、还是读现在的数据)来实现对外的一致性。
MySQL 提供了不同的事务隔离级别来解决上述问题,其中最主要的机制就是MVCC(Multiversion Concurrency Control),本质是一种快照读,通过自增的事务id来判断读哪个版本的快照数据,并通过redo log “回溯”返回对应的数据快照。 - 使用锁机制控制并发:本质是细粒度维度(如数据行、数据范围)去串行化写操作,来保证数据写的正确性
在mysql中,如共享锁(S 锁)、排他锁(X 锁)和间隙锁。- 共享锁允许其他事务读取数据,但阻止其他事务修改数据;
- 排他锁则完全阻止其他事务对数据的读写操作。例如,当一个事务需要对数据进行修改时,可以先获取排他锁,确保在修改期间其他事务无法干扰,从而保证数据的一致性。
- 间隙锁,间隙锁主要用于解决幻读问题,它锁住的不是具体的数据行,而是数据的范围。当一个事务使用间隙锁锁定了某个数据范围后,其他事务无法在该范围内插入新的数据,从而避免了幻读现象的发生。
3.2 原子性
要么一起成功,要么一起失败!
3.2.1 产生场景
- 系统崩溃:在事务执行过程中,如果系统突然崩溃(如硬件故障、操作系统崩溃等),可能导致事务中的部分操作已经执行,而部分操作未执行。例如,在一个数据库更新事务中,已经完成了对某些表的插入操作,但在执行更新另一些表的操作时系统崩溃,这就破坏了事务的原子性。
- 软件错误:应用程序中的错误(如代码逻辑错误、异常未处理等)可能导致事务无法完整执行。比如,在一个涉及多个数据库操作的事务中,由于代码中的一个逻辑错误,导致在执行某个操作时抛出异常,使得后续操作无法继续进行,从而破坏了事务的原子性。
- 资源不足:当事务执行需要的资源(如数据库连接、内存等)不足时,可能无法完成所有操作。例如,在一个批量插入数据的事务中,由于数据库连接池耗尽,导致部分数据插入成功,部分数据插入失败,破坏了事务的原子性。
- 业务主动回滚:在某些业务场景下,根据特定的业务规则或条件判断,即使事务尚未执行完毕,也需要主动进行回滚操作。例如,在电商系统的订单创建事务中,当系统检测到用户的账户余额不足支付订单金额时,即使订单创建过程中的部分操作(如生成订单号、记录订单基本信息)已经完成,也需要主动回滚整个事务,取消订单创建,以保证业务逻辑的正确性和数据的一致性。
3.2.2 解决范式
常规解法有两种思路——回滚&尽最大努力交付,但是在单机事务这个场景下,一般都采用回滚的思路。
- 回滚机制
以 MySQL 为例,MySQL 中的 InnoDB 存储引擎通过事务日志(重做日志 redo log 和回滚日志 undo log)来实现事务的回滚。
- 尽最大努力交付
3.3 持久性
3.3.1 背景
在单机情况下,持久化主要指内存和磁盘之间的同步策略,本质是性能和可靠性的权衡。
3.3.2 解决范式
核心思路就是把数据写入磁盘,但难点在于性能和可靠性之间的平衡,其中常见的解决策略就是lsm-tree(Log-Structured-Merge-Tree),通过将数据的变化转化为日志的变化,从而实现顺序写磁盘,提升磁盘写入性能。
在Mysql中,
- WAL(Write-Ahead Logging):InnoDB使用WAL来保证数据的持久性。所有修改操作首先写入到WAL日志中,然后才更新内存中的数据结构。
- Change Buffer:InnoDB使用Change Buffer来减少磁盘I/O。当有多个修改操作发生时,它们会先被记录在Change Buffer中,然后定期合并到SSTable中。
- Buffer Pool:InnoDB使用Buffer Pool来存储最近访问的数据页,从而减少磁盘I/O。
总结
单机事务中的一致性问题,通过原子性、隔离性和持久性这三大特性的协同保障得以实现。
原子性确保事务操作的完整性,避免部分成功部分失败的情况;
隔离性控制并发事务之间的相互影响,通过不同的隔离级别和并发控制手段,实现数据访问的一致性;
持久性则致力于将事务提交的数据可靠地存储到磁盘,在性能和可靠性之间寻求平衡。
以 MySQL 为代表的单机数据库系统,通过 MVCC、各种锁机制、事务日志、WAL、Change Buffer 和 Buffer Pool 等一系列技术手段,有效地解决了单机事务中的一致性问题,为各类应用提供了稳定、可靠的数据处理基础。