一、MySQL 简介
MySQL 是目前使用最广泛的开源关系型数据库之一,由 Oracle 公司维护,采用客户端/服务端架构,支持标准 SQL,具备跨平台、性能稳定、生态完善等特点,被大量应用于 Web 应用、电商、金融、日志分析等场景。
从整体结构上看,MySQL 可以分为以下几个层次:
- 连接层:负责客户端连接管理、身份认证、权限校验和线程复用。
- 服务层:包含 SQL 解析器、查询优化器、执行器以及内置函数,是 SQL 语句真正被理解和调度的位置。
- 存储引擎层:负责数据的实际存取,采用插件式设计,常见引擎有 InnoDB、MyISAM、Memory 等。
- 文件系统层:负责把数据、日志、索引落到磁盘文件上。
需要特别强调的是,事务能力由存储引擎提供。MySQL 默认使用 InnoDB 引擎,它支持事务、行级锁、外键和崩溃恢复;而 MyISAM 不支持事务,因此在实际项目中,涉及事务的业务表几乎都会选择 InnoDB。
二、什么是数据库事务
事务(Transaction)是一组逻辑上不可分割的数据库操作序列。这组操作要么全部成功,要么全部失败回滚,不允许出现"只做了一半"的中间状态。
最典型的例子是转账:A 账户扣款 100 元,B 账户增加 100 元。这两步必须作为一个整体执行,如果扣款成功而加款失败,就会造成资金凭空消失。事务正是用来保证这类业务一致性的机制。
一句话理解:事务就是把多条 SQL 打包成一个"原子操作",让数据库从一种一致状态安全地过渡到另一种一致状态。
三、事务的四大特性(ACID)
| 特性 | 含义 | InnoDB 的实现方式 |
|---|---|---|
| 原子性(Atomicity) | 事务中的操作要么全部提交,要么全部回滚 | 通过 undo log 记录反向操作,回滚时撤销已执行的修改 |
| 一致性(Consistency) | 事务执行前后,数据都必须满足业务约束和完整性规则 | 由原子性、隔离性、持久性共同保障,并依赖约束、触发器等机制 |
| 隔离性(Isolation) | 并发事务之间互不干扰,一个事务的中间状态对其他事务不可见 | 通过锁机制和 MVCC(多版本并发控制)实现 |
| 持久性(Durability) | 事务一旦提交,修改就永久保存,即使数据库崩溃也不丢失 | 通过 redo log 与刷盘策略(如 innodb_flush_log_at_trx_commit)保证 |
其中,一致性是目的,原子性、隔离性、持久性是手段。理解这一点,有助于在排查问题时判断应该关注日志、锁还是隔离级别。
四、事务的隔离级别
并发事务会带来三类典型问题:
- 脏读:读到了其他事务尚未提交的数据。
- 不可重复读:同一事务内两次读取同一行,结果不一致(其他事务提交了更新)。
- 幻读:同一事务内两次执行相同查询,返回的行数不一致(其他事务插入或删除了数据)。
SQL 标准定义了四种隔离级别,MySQL 全部支持:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | 可能 | 可能 | 可能 |
| READ COMMITTED(读已提交) | 不可能 | 可能 | 可能 |
| REPEATABLE READ(可重复读,MySQL 默认) | 不可能 | 不可能 | InnoDB 下基本不可能 |
| SERIALIZABLE(串行化) | 不可能 | 不可能 | 不可能 |
MySQL 的默认隔离级别是 REPEATABLE READ 。InnoDB 在该级别下通过 MVCC 保证快照读的一致性,并通过 Next-Key Lock(记录锁 + 间隙锁) 在很大程度上避免了幻读,这也是 MySQL 与部分其他数据库的一个差异点。
查看与设置隔离级别:
sql
-- 查看当前会话与全局隔离级别
SELECT @@transaction_isolation;
SELECT @@global.transaction_isolation;
-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局隔离级别(新连接生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
五、事务的基本用法
5.1 显式事务
MySQL 默认开启自动提交(autocommit = 1),每条 SQL 都是一个独立事务。要手动控制事务,可以使用以下语句:
sql
-- 方式一:显式开启事务
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 全部成功则提交
COMMIT;
-- 出现异常则回滚
-- ROLLBACK;
也可以使用 BEGIN 代替 START TRANSACTION,两者在 InnoDB 中效果基本一致。此外还可以设置事务的隔离级别和读写属性:
sql
-- 开启一个只读事务,适合报表类查询
START TRANSACTION READ ONLY;
-- 开启事务并指定隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
5.2 保存点(SAVEPOINT)
当事务较长时,可以设置保存点,实现部分回滚:
sql
START TRANSACTION;
INSERT INTO orders (id, user_id, amount) VALUES (1001, 1, 199.00);
SAVEPOINT sp1;
INSERT INTO order_items (order_id, sku, qty) VALUES (1001, 'SKU-A', 2);
SAVEPOINT sp2;
-- 假设明细数据有误,只回滚到 sp1,保留订单主表记录
ROLLBACK TO SAVEPOINT sp1;
COMMIT;
注意:ROLLBACK TO SAVEPOINT 只是回滚到某个保存点,事务本身仍然处于开启状态,需要最终 COMMIT 或 ROLLBACK 结束。
5.3 自动提交控制
sql
-- 关闭自动提交,后续语句需手动 COMMIT
SET autocommit = 0;
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;
-- 恢复自动提交
SET autocommit = 1;
在实际开发中,更推荐使用显式的 START TRANSACTION,而不是长期关闭自动提交,避免连接池复用时出现"忘记提交"的隐患。
六、编程语言中的事务用法
6.1 Java(JDBC)
java
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class TransferService {
public void transfer(long fromId, long toId, int amount) throws SQLException {
String url = "jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai";
try (Connection conn = DriverManager.getConnection(url, "root", "password")) {
// 关闭自动提交,开启事务
conn.setAutoCommit(false);
try (PreparedStatement deduct = conn.prepareStatement(
"UPDATE account SET balance = balance - ? WHERE id = ? AND balance >= ?");
PreparedStatement add = conn.prepareStatement(
"UPDATE account SET balance = balance + ? WHERE id = ?")) {
deduct.setInt(1, amount);
deduct.setLong(2, fromId);
deduct.setInt(3, amount);
int rows = deduct.executeUpdate();
if (rows != 1) {
throw new SQLException("余额不足或账户不存在");
}
add.setInt(1, amount);
add.setLong(2, toId);
add.executeUpdate();
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
} finally {
conn.setAutoCommit(true);
}
}
}
}
关键点有三个:关闭自动提交 、成功时 commit 、异常时 rollback 。同时要注意连接池归还连接前必须恢复 autocommit,否则会影响后续使用者。
6.2 Spring 声明式事务
java
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TransferService {
private final AccountMapper accountMapper;
public TransferService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
@Transactional(rollbackFor = Exception.class)
public void transfer(long fromId, long toId, int amount) {
int rows = accountMapper.deduct(fromId, amount);
if (rows != 1) {
throw new IllegalStateException("余额不足");
}
accountMapper.add(toId, amount);
}
}
使用 @Transactional 时需要注意几个常见坑:
- 默认只对
RuntimeException和Error回滚,受检异常需显式指定rollbackFor。 - 同类内部方法调用不会经过代理,事务注解会失效。
- 方法必须是
public,且类需要被 Spring 管理。 - 事务方法内应避免远程调用、长时间等待等耗时操作,防止长时间持有锁。
七、事务与锁、MVCC 的关系
理解事务行为,离不开锁和 MVCC:
- 快照读 :普通
SELECT在 REPEATABLE READ 下读取 undo log 构建的一致性视图,不加锁。 - 当前读 :
SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、UPDATE、DELETE读取最新数据并加锁。 - 行锁:InnoDB 基于索引加锁,若查询条件没有命中索引,可能退化为锁全表扫描范围,导致并发性能骤降。
- 间隙锁:在 REPEATABLE READ 下防止其他事务在区间内插入数据,从而抑制幻读。
sql
-- 当前读,锁定该行,防止并发修改
START TRANSACTION;
SELECT * FROM account WHERE id = 1 FOR UPDATE;
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;
因此,事务中的 SQL 应尽量走索引、缩小锁定范围,并保持事务短小。
八、事务使用的最佳实践
- 事务尽量短:只包裹必要的数据库操作,避免在事务中做 HTTP 调用、文件读写、消息发送等耗时动作。
- 控制事务粒度:不要一个事务处理成百上千条记录,必要时分批提交。
- 统一加锁顺序:多表或多行更新时按固定顺序加锁,降低死锁概率。
- 合理使用索引:避免无索引更新导致锁范围扩大。
- 捕获并处理死锁:InnoDB 检测到死锁会回滚其中一个事务,应用层应具备重试机制。
- 避免长事务:长事务会阻塞 undo 清理,导致 undo 表空间膨胀,也会加剧锁等待。
- 明确隔离级别:根据业务一致性要求选择,不要盲目追求最高隔离级别。
查看当前运行中的事务:
sql
-- MySQL 8.0
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx
ORDER BY trx_started;
九、常见问题与排查思路
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 更新语句长时间不返回 | 行锁被其他事务占用 | 查看 innodb_trx、performance_schema.data_locks,定位阻塞源 |
| 报错 Deadlock found | 两个事务交叉持有锁 | 查看 SHOW ENGINE INNODB STATUS 中的 LATEST DETECTED DEADLOCK |
| 数据出现"半更新" | 未使用事务或异常未回滚 | 检查代码是否 commit/rollback,是否吞掉异常 |
| 同一事务两次查询结果不同 | 隔离级别为 READ COMMITTED | 确认隔离级别,必要时改为 REPEATABLE READ 或使用当前读 |
| undo 表空间持续增长 | 存在长事务 | 排查 innodb_trx 中运行时间过长的事务 |
十、总结
MySQL 事务是保证数据一致性的核心机制。InnoDB 通过 undo log 实现原子性,通过 redo log 实现持久性,通过锁与 MVCC 实现隔离性,三者共同支撑起一致性这一最终目标。
在实际使用中,掌握 START TRANSACTION、COMMIT、ROLLBACK、SAVEPOINT 这些基础语句只是起点,更重要的是理解隔离级别带来的行为差异,保持事务短小、SQL 走索引、加锁顺序一致,并在应用层做好异常回滚与死锁重试。只有把事务当作一种需要设计的资源来对待,才能真正发挥它的价值。