InnoDB 默认使用可重复读作为隔离级别,但把它直接搬到高并发写入场景并不总是最优选择。可重复读为了保证事务内多次读取结果一致,会在索引扫描时引入间隙锁或临键锁,锁范围的扩大往往导致并发事务在不可见的锁队列上排队,表现为吞吐下降和死锁增多。读已提交隔离级别通过改变 ReadView 的生成时机和锁的加取策略,把并发竞争压缩到一个更小的范围。下面先理解它的运行机制,再给出配置方式和性能边界。

一、RC 隔离级别为什么更适合高并发写入
读已提交与可重复读最核心的差异在于 MVCC 可见性快照的生成时机。可重复读在事务第一次执行快照读时创建 ReadView,整个事务存续期间一直沿用这份快照;读已提交则是每条 SQL 语句执行前重新创建 ReadView。也就是说,在 RC 下,只要其他事务已经提交,当前语句就能看到最新的数据版本。这个特性让 RC 在长事务中不会长期持有旧快照,配合锁机制可以明显减少间隙锁。
在加锁行为上,RC 下的 UPDATE、DELETE 和 SELECT ... FOR UPDATE 只会对实际命中的索引记录加锁,不会对索引扫描时经过的间隙添加 range lock。例如使用普通索引字段执行 UPDATE,可重复读会锁定扫描区间内的索引记录和间隙,而读已提交只锁需要修改的行。高并发写入时,这种差异直接减少了锁覆盖面积,多个事务修改不同行时不再因为落在同一个间隙内而互相阻塞。
另外,读已提交还支持 InnoDB 的半一致性读机制。当 UPDATE 语句通过唯一索引等值定位到一行,但该行不满足最终的过滤条件时,InnoDB 可以在检查聚簇索引后提前释放这行记录上的锁,而不必等到语句结束。这个优化对热点行更新场景很有价值,能在保证正确性的前提下进一步降低锁持有时间。
| 对比项 | 可重复读 | 读已提交 |
|---|---|---|
| ReadView 生成 | 事务首次快照读时 | 每条 SQL 语句执行时 |
| 间隙锁 | 存在 | 一般不添加 |
| 不可重复读 | 不会出现 | 可能出现 |
| 幻读 | 不会出现 | 可能出现 |
| 锁冲突概率 | 相对较高 | 相对较低 |
二、在 MySQL 中配置 RC 隔离级别的几种方式
切换隔离级别最直接的方式是使用 SQL 命令。会话级设置只会影响当前连接,适合先在业务会话中做测试;全局设置影响后续新建的连接,但不会改变已建立连接的会话状态,因此生产环境通常需要两者配合。查询当前隔离级别可以使用系统变量 transaction_isolation,在 MySQL 8.0 之前的版本中对应变量为 tx_isolation。
-- 查看全局与当前会话的隔离级别 SELECT @@GLOBAL.transaction_isolation, @@SESSION.transaction_isolation; -- 会话级切换为读已提交 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 全局级切换为读已提交 SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
如果希望 MySQL 重启后配置仍然生效,可以写入配置文件。在 Linux 环境的 my.cnf 或 Windows 的 my.ini 中,在 [mysqld] 段下增加事务隔离级别配置。同时建议将二进制日志格式调整为 ROW,这是 RC 在主从复制场景下保持数据一致的重要前提。
[mysqld] transaction-isolation = READ-COMMITTED binlog_format = ROW
对于 Java 应用,连接池初始化时可以设置会话隔离级别。HikariCP 的 connection-init-sql 会在每个物理连接创建后执行一次,可以统一将会话切到 RC。如果使用 Spring 声明式事务,还可以在事务注解上指定隔离级别,这样只有特定方法走 RC,其余方法仍可保留默认级别。
spring:
datasource:
hikari:
connection-init-sql: SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
@Transactional(isolation = Isolation.READ_COMMITTED)
public void createOrder(Order order) {
orderMapper.insert(order);
stockMapper.decreaseStock(order.getSkuId(), order.getQuantity());
}配置文件生效后,可以通过新建一个连接执行 SELECT @@transaction_isolation; 验证。如果返回值不是 READ-COMMITTED,需要检查连接池是否覆盖了参数,或者应用代码中是否在事务开启前再次修改了隔离级别。
三、RC 的性能优势与代价
读已提交带来的第一个直接收益是锁等待减少。由于不加间隙锁,多个事务修改相邻但不同的行时不再互相阻塞。尤其在订单流水、日志记录、审计明细这类主键有序或唯一索引写入密集的系统中,RR 的间隙锁经常会把并行的插入和更新串行化,而 RC 允许更高的写入并发。锁等待时间缩短后,连接占用时间下降,整体吞吐会得到改善。
第二个收益是死锁概率更低。很多死锁的根源是多个事务在间隙锁上形成互相等待,RC 去掉了间隙锁,这类死锁场景会大幅度减少。对于死锁频繁导致服务重试风暴的业务,切到 RC 通常能快速缓解。但 RC 并不是没有代价:事务内部多次普通 SELECT 可能读到不同的数据,例如第一次查询余额为 100,第二次变成 80,如果业务逻辑依赖同一次事务内的可重复读取,就会出现校验错误或状态判断异常。
另一个必须关注的点是主从复制一致性。可重复读配合基于语句的复制可以通过加锁保证一些场景下主从一致,但读已提交在 STATEMENT 格式下可能造成主从数据不一致,因为没有间隙锁保护,部分语句的执行顺序在主从库上不同。因此使用 RC 时必须将 binlog_format 设置为 ROW,让 binlog 记录每一行的实际变更,从而保证复制安全。同时需要接受 ROW 格式带来的 binlog 体积增长。
四、高并发落地 RC 的实践清单
并非所有高并发系统都适合 RC。一个合适的场景通常具备以下特征:写入以主键或唯一索引为主,业务上允许出现逻辑幻读,事务时间短,且热点数据变更分散。反过来,如果业务强依赖事务内快照一致性,例如财务对账、余额变动明细需要严格可重复读,则不应盲目把隔离级别降到 RC。适合的方式是按方法或按数据源拆分,对允许幻读的模块单独使用 RC。
落地之后需要增加监控。通过 performance_schema.data_lock_waits 观察锁等待,通过 SHOW ENGINE INNODB STATUS 查看最近一次死锁信息,并统计业务的错误重试率。切到 RC 后,如果死锁没有明显下降,说明瓶颈可能不在间隙锁,而在行锁或应用层排序,需要进一步分析 SQL 执行计划。
最后,RC 的性能优势依赖于索引设计。如果 UPDATE 语句缺少有效索引,InnoDB 仍然会扫描大量记录,行锁数量依然可能很高。此时应先优化索引,而不是只调整隔离级别。将 RC 与合理的索引、ROW 格式复制、较短事务边界和连接池初始化参数组合使用,才能在高并发环境中获得稳定收益。