SQL事务隔离是数据库并发控制的核心机制,它通过定义事务之间互相可见的程度,解决多个会话同时读写数据时的脏读、不可重复读和幻读问题。不同隔离级别在性能与一致性之间做出权衡,开发者需要理解其底层实现,才能在具体业务里正确配置。

一、事务隔离级别的基础概念
SQL标准定义了四种隔离级别,从低到高分别是读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。级别越低,事务之间约束越弱,并发性能越好,但数据异常风险越高;级别越高,数据一致性越强,但锁冲突和等待时间也会显著增加。
脏读指一个事务读到了另一个未提交事务修改的数据,若后者回滚,前者便读到无效值。不可重复读指同一事务内两次读同一行,结果因其他事务提交更新而不同。幻读则针对范围查询,其他事务插入新行导致再次查询出现“幻影”记录。隔离级别正是围绕这三类中止现象来设计的。
1.1 各级别异常允许对照
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 允许 | 允许 | 允许 |
| 读已提交 | 禁止 | 允许 | 允许 |
| 可重复读 | 禁止 | 禁止 | 通常禁止 |
| 串行化 | 禁止 | 禁止 | 禁止 |
上表是主流关系型数据库对标准的大致实现,其中MySQL InnoDB在可重复读级别通过间隙锁解决了幻读,而Oracle读已提交仍存在幻读可能。了解这些差异,是控制隔离的第一步。
二、控制隔离级别的详细步骤
在应用中控制隔离级别通常有两层手段:数据库会话级设置与编程语言驱动参数。以MySQL为例,可以通过SQL语句直接设置当前会话的隔离级别,这种方式适合临时调试或特定批处理脚本。
-- 查看当前会话隔离级别 SELECT @@transaction_isolation; -- 设置当前会话为读已提交 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- 开启事务并执行操作 START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE id = 1; COMMIT;
上述代码先查看再设置,最后在显式事务中完成扣款。注意SET SESSION只影响当前连接,连接池中的其他连接不受影响。如果使用全局设置,需替换为SET GLOBAL,但会影响所有新连接,生产环境应谨慎。
2.1 在Java代码中控制
使用JDBC时,可以在获取连接后调用API设置,也可以在连接串中指定。代码方式更灵活,能够根据业务方法动态切换。
import java.sql.*;
public class IsolationDemo {
public static void main(String[] args) throws Exception {
Connection conn = DriverManager.getConnection(
"jdbc:mysql://127.0.0.1:3306/test", "user", "pass");
// 设置为可重复读
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
conn.setAutoCommit(false);
try (Statement st = conn.createStatement()) {
ResultSet rs = st.executeQuery("SELECT balance FROM account WHERE id=1");
if (rs.next()) {
int balance = rs.getInt(1);
// 业务逻辑处理
}
conn.commit();
} catch (Exception e) {
conn.rollback();
}
}
}
这段代码在连接上显式声明了可重复读,并关闭自动提交,保证查询与后续写操作在同一事务快照内。若方法抛出异常则回滚,避免部分更新。实际框架如Spring可通过@Transactional(isolation=...)注解声明,底层同样转化为连接设置。
三、完整应用场景拆解
电商库存扣减是最典型的需要精细控制隔离的场景。假设商品仅剩一件,两个用户同时下单,若隔离不当就会出现超卖。下面以读已提交加乐观锁方案演示如何安全实现。
-- 表结构含版本号字段
CREATE TABLE stock (
id INT PRIMARY KEY,
count INT NOT NULL,
version INT NOT NULL
);
-- 扣减时带版本条件
UPDATE stock
SET count = count - 1, version = version + 1
WHERE id = 1 AND count >= 1 AND version = 旧版本值;
该语句在更新时校验版本,若两次请求拿到相同旧版本,只有一个能成功,另一个影响行数为0,应用层据此提示“手慢了”。读已提交保证我们能读到已提交的库存数,而乐观锁把并发冲突检测下推到单条SQL,避免长事务锁表。
3.1 财务对账的串行化方案
与秒杀不同,财务日终对账要求绝对准确,哪怕慢也要防止任何幻读。此时应将隔离级别提升至串行化,或显式加表级锁。
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION; -- 锁定对账范围,防止其他事务插入新流水 SELECT * FROM bill WHERE biz_date = '2023-05-01' FOR UPDATE; -- 执行对账计算与写入 INSERT INTO check_result SELECT ... FROM bill WHERE biz_date = '2023-05-01'; COMMIT;
串行化下,上述FOR UPDATE不仅锁行还锁间隙,其他会话无法插入同日账单,从根本上消除幻读。虽然并发度下降,但日终批处理本就允许暂停实时写入,属于合理取舍。
四、常见误区与注意事项
不少团队误以为提升隔离级别就能解决所有并发bug,结果导致接口大面积超时。实际上,多数读多写少场景用可重复读配合合理索引已足够,盲目使用串行化反而放大锁等待。另外,ORM框架默认隔离常是数据库默认值,迁移数据库后行为可能变化,需要统一配置。
控制隔离不是越高越好,而是看清业务容忍度:读多写少重吞吐,钱账相关重一致。
最后建议通过监控长事务与锁等待视图,定期审查事务边界。将大事务拆小、在必要时才提升级别,才能让SQL事务隔离真正服务于稳定系统。