事务隔离级别
事务隔离级别
复习定位
隔离级别定义了一个事务在提交之前对并发执行的其他事务的可观性能。四种级别——READ UNCOMMITTED(允许脏读)、READ COMMITTED(不允许脏读——不可重复读可能)、REPEATABLE READ(防止不可重复读但幻读存在)、SERIALIZABLE(最强隔离——像串行执行一样)。减少隔离级别提高并发性能——但可能造成数据不一致。
四种隔离级别
READ UNCOMMITTED——最低隔离级别——一个事务可以读到其他事务未提交的数据(脏读)。很少实际采用——因为即使读一个近似值也不需要读到未提交的更改。
READ COMMITTED——大多数数据库(PostgreSQL、Oracle)的默认级别——一个事务只能读到其他事务已经提交的数据。解决了脏读但是不可重复读——在同一事务中两次相同的SELECT可能返回不同的结果——因为在两个SELECT的间隔中另一个事务修改并提交了数据。
REPEATABLE READ——MySQL InnoDB的默认级别——一个事务执行中多次读取同一结果集得到相同的结果——解决了不可重复读。但纯标准中幻读可能发生——两次范围查询可能得到不同行数的结果——因为另一个事务在范围内插入了新行并提交。InnoDB通过Next-Key Lock在RR级别上实现了幻读防御——实际使用中InnoDB的RR比标准更强一步。
SERIALIZABLE——最高隔离级别——它保证并发事务的执行效果与这些事务按某个顺序串行执行的效果完全相同。脏读/不可重复读/幻读都不会发生。但并发性能急剧下降——因为需要范围锁或冲突检测。
隔离级别的实现机制——锁 vs MVCC
基于锁的(悲观)实现——每个事务在读取或修改时对数据加锁——不同隔离级别的锁持有时间不同——实现了不同的可见性限制。
基于MVCC(多版本并发控制)的实现——PostgreSQL和MySQL InnoDB使用多版本机制。不阻塞读——读到的是数据在事务开始时的快照(或语句开始时的快照)——写事务创建新版本——读事务看到旧版本——两者不冲突。
| 级别 | MVCC实现 | 可见性 |
|---|---|---|
| READ COMMITTED | 每条语句执行时生成ReadView | 读到最新已提交版本 |
| REPEATABLE READ | 事务开始时生成ReadView | 整个事务看到一致快照 |
| SERIALIZABLE | ReadView+冲突检测 | 所有事务串行化效果 |
工程选择
互联网高并发系统——默认选择READ COMMITTED(PostgreSQL默认)——因为快照读不会阻塞写——写也不会阻塞读——性能最高。对于需要保证一致性读的报表——可以用REPEATABLE READ或RR模式。对于需要完全避免异常的金融核心——使用SERIALIZABLE或程序上附加的悲观锁。
复习检查
READ COMMITTED下——一个事务执行两个SELECT语句分别查询同一行——如果另一个事务在这两次查询之间修改并提交——同一事务两次读到的结果为何不同?解释MVCC下每条SQL语句重新生成ReadView的行为。
REPEATABLE READ为什么防止了不可重复读但标准上认为幻读仍然可能(不考虑InnoDB额外实现)?使用MVCC快照隔离为什么确保两次行读相同但范围查询可能不同?
MySQL InnoDB的可重复读实际上用了Next-Key Lock——这个机制如何阻止幻读——是通过锁住范围不让人插入完成的?
串行化隔离级别(SERIALIZABLE)的共享锁和排他锁在范围扫描中是如何完全锁定整个被查询的范围——及如何使用锁机制实现这种"完全串行化"的效果?
在工程中如何根据数据一致性和系统并发量权衡隔离级别的选择——如果能接受秒杀系统偶尔多卖一件——用READ COMMITTED;反之如果银行转账必须精确到分——就需要SERIALIZABLE级别的强保证。
四种隔离级别的详细对比表
| 级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 | 并发性能 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 无锁或脏读 | 最高 |
| READ COMMITTED | 防止 | 可能 | 可能 | 语句级快照/写锁 | 高 |
| REPEATABLE READ | 防止 | 防止 | 可能(MySQL InnoDB例外) | 事务级快照/Next-Key Lock | 中 |
| SERIALIZABLE | 防止 | 防止 | 防止 | 范围锁/冲突检测 | 低 |
四种隔离级别从低到高——一致性逐渐增强——但并发性能逐渐降低。选择隔离级别就是在这两者之间做权衡。
脏读的具体示例
线程A: BEGIN;
UPDATE account SET balance=balance-100 WHERE id=1; (balance=400, 未提交)
线程B: SELECT balance FROM account WHERE id=1; (读到400——脏读)
线程A: ROLLBACK; (balance重新变为500)
线程B此时使用了400作为决策依据——但400实际上从未被提交(回滚了)脏读是隔离级别最低(READ UNCOMMITTED)时发生的问题——大多数数据库的默认隔离级别已经高于此——所以现代应用中很少直接遇到脏读。但了解脏读的机制有助于理解隔离级别向上逐步解决什么层面的一致性问题。
不可重复读的具体示例
事务A: SELECT balance FROM account WHERE id=1; (读到500)
事务B: UPDATE account SET balance=400 WHERE id=1; COMMIT;
事务A: SELECT balance FROM account WHERE id=1; (读到400——两次读到的结果不同)
事务A在同一事务内——对同一行执行了两次查询——结果不同——这就是不可重复读。不可重复读发生在READ COMMITTED(及更低)级别——因为每次SELECT读取的是当前已提交的最新数据——而两次SELECT之间可能被其他事务的提交修改了数据。REPEATABLE READ通过复用事务开始时的ReadView来保证同一事务内的多次查询结果一致。
幻读的具体示例
事务A: SELECT * FROM orders WHERE price>1000; (返回3行)
事务B: INSERT INTO orders VALUES(4, 1500); COMMIT;
事务A: SELECT * FROM orders WHERE price>1000; (返回4行——新插入的行)幻读和不可重复读的区别——不可重复读是同一个行(同一条记录)的值被修改——幻读是查询结果集的行数发生了变化(增加了新行)——这是MVCC快照隔离不能天然防止幻读的原因:因为MVCC只记录了已存在的行的可见性——对新插入的行没有版本信息可供快照读取——所以第二次查询发现多了一行。InnoDB的Next-Key Lock通过锁住索引区间阻止新行的插入来解决幻读。
隔离级别的MVCC实现细节
MVCC通过为每个事务维护数据的多个版本来实现读不阻塞写、写不阻塞读:
- 每一行记录额外存储
DB_TRX_ID(最近修改该行的事务的ID)和DB_ROLL_PTR(指向回滚段中该行上一个版本的指针) - 事务开始时或每条语句执行时——生成一个ReadView——记录当时系统中所有活跃(未提交)事务的ID列表
- 读取行时——通过行的
DB_TRX_ID与ReadView比较——判断该行的该版本对当前事务是否可见
READ COMMITTED——每条语句执行前生成新的ReadView——语句之间如果有其他事务提交——新ReadView会包含更少的活跃事务——因此可能看到新提交的数据。
REPEATABLE READ——事务的首次SELECT时生成ReadView并复用整个事务生命周期——不论其他事务如何提交——ReadView不会变化——所以看到的数据保持一致。
复习检查(续)
MVCC中READ COMMITTED和REPEATABLE READ的ReadView生成时机有何不同——READ COMMITTED每条SELECT语句生成新ReadView——REPEATABLE READ第一次SELECT时生成并复用——保证了事务内的一致读。
脏读和不可重复读的本质区别——脏读读到的是未提交的数据——不可重复读读到的是已提交的但时间点不同的数据——但两者在两次读取间数据都发生了变化(只是脏读读取的数据来源是未提交事务,不可重复读来源是已提交事务的新版本)。
REPEATABLE READ下的幻读是如何发生的——MVCC快照读到的是某时刻的已存在行的版本——新插入的行在另一个事务的范围内不属于快照的读取范围——因此第二次范围查询时可以从数据库中观察到新插入的已提交行。
SERIALIZABLE级别如何防止幻读——通过范围锁(或PostgreSQL的SSI——可串行化快照隔离——基于冲突检测)阻止其他事务在锁区间内插入满足查询条件的新行。
工程中选择隔离级别的考虑——READ COMMITTED适合高并发场景(如电商浏览)——SERIALIZABLE适合金融、账务等一致性要求上不容妥协的场景——REPEATABLE READ适合中间场景(如多步报表查询需要一致快照)。
隔离级别对并发程序行为的实际影响
在Java/Go等后端语言开发数据库应用时——事务隔离级别的选择直接影响程序逻辑行为:
READ COMMITTED下:
- 同一个事务内两次读取同一行可能返回不同的值
- 需要用SELECT ... FOR UPDATE(行锁)来保证读取不被其他事务更改
- 适合读多写少、对实时数据不苛刻的服务(新闻/博客)
REPEATABLE READ下:
- 同一事务内多次读取同一行或用同一条件查询始终得到同样的结果
- 不需要额外的锁来保证重复读等场景的一致性
- 适合需要按事务执行前的快照做逻辑判断(如对账单、定期统计)应用开发者应该理解隔离级别对程序逻辑的约束——而不是简单地"越严越好"——因为SERIALIZABLE虽然最安全——但并发度的急剧下降可能导致应用无法满足高并发需求。
不同数据库的默认隔离级别
| 数据库 | 默认级别 | 实现 | 说明 |
|---|---|---|---|
| PostgreSQL | READ COMMITTED | MVCC | 读不阻塞写 |
| MySQL InnoDB | REPEATABLE READ | MVCC + Next-Key Lock | Next-Key Lock也防止幻读——RR级别实际上达到了接近SQL标准的SERIALIZABLE |
| Oracle | READ COMMITTED | MVCC | 回滚段提供快照——写不阻塞读 |
| SQL Server | READ COMMITTED | 锁/MVCC可配置 | 默认使用锁——可启用读提交快照(RCSI) |
| MongoDB | 取决于副本策略 | 无传统事务隔离 | 4.0+支持多文档事务——隔离级别类似READ COMMITTED |
了解数据库的默认隔离级别对跨数据库移植至关重要——相同的SQL事务逻辑在不同默认级别下可能产生不同的结果。
复习检查(续二)
MySQL InnoDB在REPEATABLE READ下使用什么机制阻止幻读——Next-Key Lock(记录锁+间隙锁)——锁定访问的索引区间——防止其他事务在区间中插入新行。
PostgreSQL的默认隔离级别——READ COMMITTED——为什么PostgreSQL选择更低级别作为默认——因为大多数应用不需要REPEATABLE READ——高并发的READ COMMITTED对大部分业务已经足够——且性能更好。
不同数据库之间的默认隔离级别差异对应用移植的影响——相同SQL在PostgreSQL(READ COMMITTED)下可能得出不同结果——需要根据目标数据库的特性调整事务隔离级别设置。
Oracle中通过回滚段实现写不阻塞读的机制——读操作在回滚段中寻找需要的旧版本——写操作创建新版本写数据块——两者不冲突——所以Oracle在READ COMMITTED下也能保证写不阻塞读、读不阻塞写。
高并发应用(如秒杀/高并发下单)在READ COMMITTED隔离级别下出现超卖时应该如何解决——使用乐观锁或
UPDATE ... WHERE stock>0条件更新——不依赖隔离级别来保证逻辑一致性——而是在业务层面用单行确保准确扣减。
隔离级别在分布式数据库中的特殊行为
在分布式数据库中(如Google Spanner、CockroachDB、TiDB)——事务可能跨多个物理节点——隔离级别的实现复杂度远高于单机数据库。这些系统通常只提供SNAPSHOT ISOLATION或SERIALIZABLE两种隔离级别。SNAPSHOT ISOLATION(快照隔离)类似于MVCC的REPEATABLE READ——读操作看到一个一致快照——写操作在提交时检测是否有写冲突——如果检测到冲突——提交失败——事务回滚。分布式SERIALIZABLE实现通常基于OCC(乐观并发控制)结合TrueTime(Spanner的全球时钟)或HLC(混合逻辑时钟)——通过精确的提交时间戳排序来确保全局可串行化——但相比单机的SERIALIZABLE有更高的延迟和更低的吞吐。
数据库并行性能与隔离级别的工程权衡决策树
应用对数据一致性要求?
├─ 可以接受近似值 → READ UNCOMMITTED(极少日志系统、计数器缓存)
├─ 需要精确行值但不介意行变化 → READ COMMITTED(Web后端、社交动态)
├─ 需要事务内一致快照 → REPEATABLE READ(财务报表、数据迁移对比)
└─ 需要绝对串行化 → SERIALIZABLE(账务核心、电子钱包基础流程)按照这个决策树方向思考——多数Web应用的默认选择是READ COMMITTED——它提供了足够的隔离性(不会脏读)——同时在高并发下保持高性能。需要快照一致性的报表类应用可以临时提升到REPEATABLE READ。核心账务类操作应提升到SERIALIZABLE——或在应用层使用悲观锁替代。
复习检查(续三)
隔离级别选择的核心提醒
隔离级别的选择是在数据一致性和系统并发性能之间的权衡——不是越严格越好。在互联网高并发场景下——默认使用数据库的默认隔离级别(通常是READ COMMITTED或REPEATABLE READ)和补充应用层的乐观锁或条件更新来保证业务逻辑正确——比将隔离级别提高到SERIALIZABLE更实用——因为SERIALIZABLE的锁范围和冲突检测在大规模并发下会严重拖慢系统的吞吐能力。
READ COMMITTED应用层处理超卖的常见做法——使用
UPDATE SET stock=stock-1 WHERE id=X AND stock>0——通过条件更新确保不会超卖——不依赖于隔离级别防止并发写入的损失更新。分布式数据库快照隔离(SI)和单机MVCC RR的区别——分布式SI在写提交时通过冲突检测防止写偏斜(Write Skew)——而单机MVCC RR通常不做全局写冲突检测——允许并发的两个写事务提交——即使它们基于同一个不一致快照做出了错误决策。
读已提交的不可重复读对业务的实际影响——一个订单查询系统在两次查询间被其他事务插入了新订单——可能对分页查询造成影响——但绝大多数业务可以接受这种轻微的不一致。
金融核心系统选择SERIALIZABLE的原因——在账务操作中——不可重复读或幻读都可能导致资金计算错误——串行化虽然大幅牺牲了并发能力——但绝对的正确性在此类场景下优先级超过性能。
InnoDB在RR级别下使用Next-Key Lock如何在性能与防幻读之间做到平衡——只在索引扫描过的范围内加间隙锁——不影响范围外的插入——比起全表间隙锁处理更加高效。