在数据库系统中,事务隔离级别是保证数据一致性的核心机制。RR(Repeatable Read)和RC(Read Committed)是两种常见的事务隔离级别,它们通过不同的锁机制和版本控制策略来实现事务间的数据隔离。
RR(可重复读)隔离级别保证在同一个事务内,多次读取同一数据会得到相同的结果。这种隔离级别通过快照机制实现,事务开始时创建数据快照,后续读取都基于这个快照版本。MySQL的InnoDB引擎默认采用RR隔离级别。
RC(读已提交)隔离级别则只保证读取到已提交的数据。每次读取都会获取最新的已提交数据版本,因此同一事务内多次读取可能得到不同结果。这种隔离级别下,读操作不会阻塞写操作,但可能出现不可重复读现象。
关键区别:RR通过事务级快照保证可重复读,RC通过语句级快照只保证读取已提交数据
数据库系统需要解决三个主要并发问题:
RR隔离级别理论上解决了脏读和不可重复读,但在标准实现中仍可能出现幻读。InnoDB通过Next-Key Locking机制在RR级别下也解决了幻读问题。
多版本并发控制(MVCC)是现代数据库实现隔离级别的核心技术。InnoDB通过Undo日志和ReadView实现MVCC,避免了纯锁机制带来的性能问题。
InnoDB为每行记录维护多个版本:
ReadView是事务在读取数据时创建的"可见性快照",决定哪些版本的数据对当前事务可见。它包含四个关键组件:
判断数据版本是否可见的规则:
RR隔离级别下,ReadView在事务第一次读取时创建,后续所有读取都使用同一个ReadView。这保证了可重复读特性。
RC隔离级别下,每个SQL语句执行时都会创建新的ReadView。因此能读取到其他事务最新提交的数据,但可能导致不可重复读。
InnoDB中的读取操作分为快照读和当前读两种模式,它们与隔离级别紧密相关。
快照读基于MVCC实现,读取数据的历史版本而不加锁。普通SELECT语句默认使用快照读。
在RR级别下,快照读使用事务开始时创建的ReadView;在RC级别下,每次快照读使用最新的ReadView。
快照读的特点:
当前读总是读取数据的最新版本,并对读取的记录加锁。以下语句使用当前读:
当前读的实现机制:
不同隔离级别下读操作的差异:
| 特性 | RR隔离级别 | RC隔离级别 |
|---|---|---|
| 快照读的ReadView创建 | 事务第一次读取时创建 | 每个语句执行时创建 |
| 当前读的加锁范围 | 使用Next-Key Lock防幻读 | 只锁记录不防幻读 |
| 一致性保证 | 可重复读+防幻读 | 只防止脏读 |
RR级别适用场景:
RC级别适用场景:
MVCC带来的潜在问题及解决方案:
长事务导致Undo日志膨胀:
快照读与当前读冲突:
RC级别下的不可重复读:
关键监控指标:
innodb_trx表:查看当前活跃事务information_schema.INNODB_TRX:获取事务详细信息show engine innodb status:查看锁等待情况诊断ReadView问题的步骤:
SELECT @@tx_isolationinformation_schema.INNODB_TRXshow engine innodb status中的History list lengthperformance_schema.events_waits_currentInnoDB在RR级别下通过Next-Key Locking解决幻读问题。Next-Key Lock是记录锁和间隙锁的组合,锁定索引记录及其前面的间隙。
案例演示:
sql复制-- 事务1
BEGIN;
SELECT * FROM users WHERE age > 20 FOR UPDATE; -- 对age>20的所有记录及间隙加锁
-- 事务2尝试插入
INSERT INTO users(name, age) VALUES('new', 25); -- 将被阻塞直到事务1提交
RC级别可能出现写倾斜(Write Skew),即两个事务基于彼此不可见的修改做出决策,导致逻辑不一致。
典型案例:
sql复制-- 事务1
BEGIN;
SELECT balance FROM accounts WHERE user_id = 1; -- 读到100
-- 事务2同时执行
BEGIN;
SELECT balance FROM accounts WHERE user_id = 2; -- 读到100
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
COMMIT;
-- 事务1继续
UPDATE accounts SET balance = balance - 100 WHERE user_id = 2;
COMMIT; -- 可能导致总余额为负
解决方案:
在同一事务中混合使用两种读模式可能导致逻辑错误:
sql复制BEGIN;
-- 快照读
SELECT * FROM products WHERE stock > 0; -- 假设返回id=1, stock=1
-- 其他事务购买了id=1的产品,stock变为0
-- 当前读
SELECT * FROM products WHERE id = 1 FOR UPDATE; -- 读到stock=0
-- 基于第一次查询结果继续处理,可能导致逻辑错误
最佳实践: