1. MVCC(多版本并发控制)概述
MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种数据库事务并发控制机制,主要用于提高数据库的读写性能。它通过维护数据的多个版本,使得读操作无需加锁,同时保证一致性,减少了事务之间的阻塞。
在 MySQL 的 InnoDB 存储引擎中,MVCC 主要用于**可重复读(REPEATABLE READ)和读已提交(READ COMMITTED)**这两种事务隔离级别。
2. MVCC 的实现原理
MVCC 通过隐藏删除和修改的行,加上一些额外的信息来实现多版本控制。主要依赖于 UNDO日志 和 事务 ID(Transaction ID)。
(1) 数据隐藏 & 版本控制
InnoDB 的数据行结构中包含两个隐藏的字段:
trx_id
(事务 ID):表示最近对该行进行修改的事务 ID。roll_pointer
(回滚指针):指向该行的 旧版本(即 undo log 记录),从而支持回滚和版本链。
当数据被修改时:
- 更新(UPDATE):不会直接修改数据,而是将旧版本保存到
undo log
,然后生成新的数据版本,并更新trx_id
。 - 删除(DELETE):不会立即删除,而是生成一个新版本,标记该行已删除,并记录
undo log
。 - 插入(INSERT):只插入最新版本的数据,不会产生历史版本(因此插入数据不受 MVCC 影响)。
(2) Undo Log 及回滚指针
Undo Log 主要用于:
- 回滚事务:当事务失败或回滚时,可以通过 Undo Log 恢复数据。
- MVCC 读取旧版本数据:Undo Log 形成一个版本链,事务可以基于
trx_id
获取合适的旧版本数据,而不会影响其他事务。
(3) MVCC 的可见性规则
当一个事务读取数据时,它需要判断哪些数据版本对自己可见。InnoDB 通过 Read View(读取视图)来管理可见性。
可见性规则如下:
- 当前事务 ID (
trx_id
) < Read View 的最小活跃事务 ID (min_trx_id
):- 该数据版本已经提交,对当前事务可见。
- 当前事务 ID (
trx_id
) > Read View 的最大活跃事务 ID (max_trx_id
):- 该数据版本是在当前事务之后创建的,不可见。
- 当前事务 ID (
trx_id
) 介于min_trx_id
和max_trx_id
之间:- 若
trx_id
属于活跃事务列表,则表示该事务还未提交,不可见。 - 若
trx_id
不在活跃事务列表中,则可见。
- 若
3. MVCC 在不同事务隔离级别下的表现
隔离级别 | MVCC 读取的版本 |
---|---|
读已提交(Read Committed) | 每次 SELECT 都创建新的 Read View,读取最近提交的数据版本 |
可重复读(Repeatable Read) | 事务开始时创建 Read View,整个事务期间保持一致 |
串行化(Serializable) | 不使用 MVCC,需要加锁 |
未提交读(Read Uncommitted) 不使用 MVCC,而是直接读取最新的数据版本,因此可能会读取到未提交的数据(脏读)。
4. MVCC 的优缺点
优点
- 非阻塞读:读取数据不需要加锁,提高并发性能。
- 减少事务冲突:多个事务可以同时操作不同版本的数据,避免不必要的锁竞争。
- 提高可重复读性能:避免了
SELECT
过程中加锁的开销。
缺点
- 需要额外存储:Undo Log 需要存储多个版本的数据,可能会导致存储空间增长。
- 版本回收问题:老版本数据需要定期清理,否则会影响性能。
- 不适用于高并发写入:因为写入仍然需要锁定行,多个事务同时写入相同数据时,仍然需要等待。
5. 总结
- MVCC 主要依赖
trx_id
和undo log
来维护数据的多个版本,允许事务在不同时间点读取合适的数据版本。 - 不同事务隔离级别下 MVCC 的行为不同,
Read Committed
每次查询创建新 Read View,而Repeatable Read
事务开始时创建 Read View 并保持不变。 - MVCC 使得大部分读操作无需加锁,提高了数据库的并发能力,但也带来存储开销和版本管理的挑战。
在实际应用中,MVCC 适用于读多写少的场景,对于高并发写入,可能需要结合锁机制或优化索引来提升性能。
理解 Read View 和 事务可见性规则
当事务在 READ COMMITTED
或 REPEATABLE READ
隔离级别下执行 SELECT
语句时,InnoDB 不会直接读取最新的数据版本,而是通过 Read View 来决定 哪个数据版本对当前事务可见。
Read View 的关键字段
在 MVCC 机制下,每个事务在读取数据时都会维护一个 Read View,它主要包含:
trx_id
:每个事务都有一个唯一递增的事务 ID,越新的事务 ID 值越大。m_ids
(活跃事务列表):当 Read View 生成时,当前正在执行但未提交的事务 ID 列表。min_trx_id
(最小活跃事务 ID):m_ids
中最小的事务 ID。max_trx_id
(下一个将要分配的事务 ID):比当前所有活跃事务 ID 都大的值,代表未来新事务的起始 ID。
数据可见性规则
当事务 T
读取一行数据时,该数据的 trx_id
(创建它的事务 ID)将与 T
的 Read View 进行比较,以决定该版本是否可见。
数据版本的 trx_id 与 Read View 比较 | 可见性 | 解释 |
---|---|---|
trx_id < min_trx_id | ✅ 可见 | 该数据版本是比 Read View 生成时更早的事务创建的,并且该事务已经提交。 |
trx_id > max_trx_id | ❌ 不可见 | 该数据版本是比 Read View 生成时更晚的事务创建的,因此不可见。 |
min_trx_id ≤ trx_id < max_trx_id 且 trx_id ∈ m_ids | ❌ 不可见 | 该数据版本是由某个活跃未提交的事务创建的,不可见。 |
min_trx_id ≤ trx_id < max_trx_id 且 trx_id ∉ m_ids | ✅ 可见 | 该数据版本的事务已提交,但其事务 ID 仍然在 Read View 生成时的范围内,因此可见。 |
直观示例
假设有如下事务操作:
- T1 开启事务,写入数据 A,但未提交 (
trx_id = 10
)。 - T2 开启事务,创建 Read View (
min_trx_id = 10, max_trx_id = 15
)。 - T3 开启事务,并更新数据 A (
trx_id = 12
),但也未提交。 - T4 开启事务 (
trx_id = 15
),并提交更新数据 A 的事务。
此时:
- T2 读取数据 A 时,
trx_id = 10
在m_ids
内,未提交,不可见。 - T2 也看不到
trx_id = 12
(未提交)。 - T2 只能看到
trx_id < 10
的数据,即 T1 之前的版本。 - T4 提交后,T2 依然看不到
trx_id = 15
创建的数据,因为 Read View 在创建时已经固定了事务状态。
如何理解 Read View 的作用
- 保证一致性:
- 在
REPEATABLE READ
级别下,同一事务内多次SELECT
看到的数据是一致的,因为 Read View 在事务开始时创建,不会变化。 - 在
READ COMMITTED
级别下,每次SELECT
都会生成新的 Read View,所以可以看到最新已提交的数据。
- 在
- 避免幻读:
REPEATABLE READ
级别的 Read View 确保了事务期间看到的行数据不会随其他事务的提交而变化,但对INSERT
仍然可能出现幻读(需借助Next-Key Lock
解决)。
- 提升并发性能:
- 通过 Read View 让事务读取旧版本数据,而不需要加锁,避免阻塞其他事务。
创建 Read View
T1 开启事务时是否创建 Read View 取决于 事务的隔离级别 和 执行的 SQL 语句。
什么时候创建 Read View?
操作 | Read View 何时创建? |
---|---|
READ COMMITTED | 每次执行 SELECT 语句时都会创建一个新的 Read View。 |
REPEATABLE READ | 第一次执行 SELECT 语句时 创建 Read View,并在整个事务生命周期内保持不变。 |
SERIALIZABLE | 由于需要加锁,不依赖 MVCC Read View,而是直接使用锁进行事务隔离。 |
T1 开启事务时是否创建 Read View?
- 如果 T1 只是执行
START TRANSACTION;
,此时并不会创建 Read View,只是开启一个事务,还没有执行任何查询。 - 只有当 T1 执行
SELECT
语句时,才会创建 Read View(前提是隔离级别需要 Read View,如READ COMMITTED
或REPEATABLE READ
)。 - 在
READ COMMITTED
级别下,每次查询都会创建一个新的 Read View。 - 在
REPEATABLE READ
级别下,T1 第一次查询时 会创建 Read View,后续查询都会使用这个 Read View,即使其他事务提交了新数据,T1 也不会看到(除非显式提交)。
除非显示提交的含义
这里的 “除非显式提交” 是指 T1 事务提交 (COMMIT
) 后,再次开启新的事务并执行查询,此时会生成一个新的 Read View,从而看到其他事务提交的数据。
1. 提交事务的方式
在 MySQL 中,提交事务的方式主要有两种:
① 显式提交(Explicit Commit)
指 手动执行 COMMIT
语句 来提交事务:
BEGIN;
SELECT * FROM users; -- 事务 T1 创建 Read View
-- 此时 Read View 固定,无法看到其他事务提交的数据
COMMIT; -- 显式提交事务
COMMIT
之后,事务结束,Read View 被销毁。
如果 T1 之后再执行SELECT
,需要开启新事务,此时会创建新的 Read View,可以看到最新数据。
② 隐式提交(Implicit Commit)
MySQL 在某些情况下会自动提交事务,比如执行以下 SQL 语句时:
- DDL 语句(
CREATE
,ALTER
,DROP
,TRUNCATE
) SET AUTOCOMMIT = 1
(默认开启自动提交)LOCK TABLES
(会自动提交当前事务)
示例:
BEGIN;
SELECT * FROM users; -- 创建 Read View
ALTER TABLE users ADD COLUMN age INT; -- DDL 语句触发隐式提交
SELECT * FROM users; -- 这时 Read View 重新创建
执行
ALTER TABLE
之后,事务被 MySQL 隐式提交,Read View 也重新生成。
2. “除非显式提交” 的含义
在
REPEATABLE READ
级别,T1 第一次查询时创建的 Read View 不会变化,即使其他事务 (T2
) 提交了新数据,T1 也看不到。
只有当 T1 执行COMMIT
之后,再次查询时才会创建新的 Read View,并看到最新数据。
(1) 事务 T1 只读取,不提交
-- 事务 T1 开启
BEGIN;
SELECT * FROM users; -- 创建 Read View (RV1)
此时 Read View (RV1) 记录了活跃事务列表。
-- 事务 T2 执行更新
BEGIN;
INSERT INTO users VALUES (2, 'Bob');
COMMIT;
事务 T2 提交了新数据。
-- T1 继续查询
SELECT * FROM users; -- 仍然使用 Read View (RV1),看不到 Bob
由于 T1 没有提交,它的 Read View 没变,所以看不到
T2
提交的数据。
(2) T1 提交后,再次查询
COMMIT; -- 显式提交事务
BEGIN;
SELECT * FROM users; -- 重新创建 Read View (RV2)
T1 提交事务后,Read View (RV1) 销毁。
新事务创建新的 Read View (RV2),此时能看到 Bob!
3. 事务提交的不同方式总结
提交方式 | 触发时机 | 是否创建新 Read View |
---|---|---|
显式提交 (COMMIT ) | 事务手动提交 | ✅ 是,提交后开启新事务会创建新的 Read View |
隐式提交(DDL、LOCK TABLES 等) | 特定 SQL 语句执行时自动提交 | ✅ 是,事务自动提交,Read View 重新创建 |
未提交 (ROLLBACK 或未执行 COMMIT ) | 事务未结束 | ❌ 否,事务继续使用旧的 Read View |
4. 结论
🔹 “除非显式提交” 的意思是:
- 在
REPEATABLE READ
级别下,T1 不会看到其他事务的新提交数据,除非 T1 自己先COMMIT
事务。 COMMIT
之后,Read View 被销毁,新的查询会创建新的 Read View,这时就能看到最新数据了!
示例
假设数据库初始状态:
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(50));
INSERT INTO users VALUES (1, 'Alice');
场景 1:REPEATABLE READ
-- T1 事务
BEGIN;
SELECT * FROM users; -- (此时创建 Read View)
这时 Read View 记录了当前活跃事务列表和
min_trx_id
。
-- T2 事务
BEGIN;
INSERT INTO users VALUES (2, 'Bob');
COMMIT;
-- T1 继续查询
SELECT * FROM users; -- 仍然只能看到 Alice,因为 Read View 不变
T1 的 Read View 在事务开始时确定,不会看到
Bob
。
场景 2:READ COMMITTED
-- T1 事务
BEGIN;
SELECT * FROM users; -- (此时创建 Read View)
Read View 记录了当前事务状态。
-- T2 事务
BEGIN;
INSERT INTO users VALUES (2, 'Bob');
COMMIT;
-- T1 再次查询
SELECT * FROM users; -- 这次能看到 Bob,因为 Read View 在每次查询时重新生成
READ COMMITTED
级别每次查询都会创建新的 Read View,因此 T1 在第二次查询时能看到T2
提交的数据。
Read View 绑定性
1. 事务只能看到自己创建的 Read View
- Read View 是事务内部的快照,用于决定当前事务能看到哪些数据版本。
- 不同事务的 Read View 互不影响,事务 A 无法访问事务 B 的 Read View。
- 每个事务只能使用自己创建的 Read View 来查询数据。
2. 示例:不同事务的 Read View 是独立的
-- 事务 A 开始
BEGIN;
SELECT * FROM users; -- 创建 Read View A
此时 Read View A 记录了当前活跃事务列表。
-- 事务 B 开始
BEGIN;
SELECT * FROM users; -- 创建 Read View B
此时 Read View B 也创建了,可能与 Read View A 不同。
-- 事务 A 继续查询
SELECT * FROM users; -- 仍然使用 Read View A
事务 A 只能使用自己的 Read View A,而不会使用事务 B 的 Read View。
-- 事务 B 继续查询
SELECT * FROM users; -- 仍然使用 Read View B
事务 B 只能使用自己的 Read View B,而不会使用事务 A 的 Read View。
所以,每个事务的 Read View 就像是自己专属的“数据时间快照”,它决定了事务能看到的数据版本 🎯。
默认事务
MySQL 默认是自动提交 (AUTOCOMMIT = 1
),即每条 SQL 语句都会作为一个独立的事务执行,并在执行后立即提交。
- 在默认情况下,每条 SQL 语句(如
INSERT
、UPDATE
、DELETE
)都会立即提交,不会等待COMMIT
。 - 只有显式关闭
AUTOCOMMIT
或使用BEGIN;
/START TRANSACTION;
才能开启手动提交模式。
示例 1:默认情况下,每条语句都会自动提交
SELECT @@AUTOCOMMIT; -- 查询当前自动提交模式
-- 结果:1 (表示自动提交开启)
INSERT INTO users VALUES (1, 'Alice');
-- 这条 INSERT 语句在执行后立即提交,不需要手动 COMMIT。
如果想要手动管理事务(即不让 SQL 语句自动提交),可以通过以下两种方式:
方法 1:使用 SET AUTOCOMMIT = 0
SET AUTOCOMMIT = 0; -- 关闭自动提交
BEGIN; -- 开启事务
UPDATE users SET name = 'Bob' WHERE id = 1;
COMMIT; -- 手动提交事务
注意:
SET AUTOCOMMIT = 0
的作用是对当前会话生效,也就是说,当前连接的所有操作都会使用手动提交,直到手动COMMIT
或ROLLBACK
。- 但是 如果连接断开,
AUTOCOMMIT
也会恢复为默认值1
。
方法 2:使用 START TRANSACTION;
/ BEGIN;
START TRANSACTION; -- 开启事务(不会立即提交)
UPDATE users SET name = 'Charlie' WHERE id = 1;
COMMIT; -- 手动提交事务
或者:
BEGIN; -- 也是开启事务
UPDATE users SET name = 'David' WHERE id = 1;
ROLLBACK; -- 撤销事务(如果不想提交)
区别:
START TRANSACTION;
或BEGIN;
只是针对当前事务关闭自动提交,并不会影响整个会话。- 当事务提交 (
COMMIT
) 或回滚 (ROLLBACK
) 后,自动提交模式 (AUTOCOMMIT=1
) 仍然有效。
事务提交模式总结
方式 | 作用 | 影响范围 | 恢复方式 |
---|---|---|---|
AUTOCOMMIT = 1 (默认) | 每条 SQL 语句都会立即提交 | 整个会话(每个 SQL 语句) | 无需恢复 |
SET AUTOCOMMIT = 0 | 关闭自动提交,手动 COMMIT 事务 | 仅当前会话 | 连接断开后恢复为 1 |
START TRANSACTION; / BEGIN; | 仅当前事务手动提交 | 只影响当前事务 | COMMIT / ROLLBACK 后恢复自动提交 |
结论
✅ MySQL 默认是自动提交 (AUTOCOMMIT=1
),每条 SQL 语句都会自动提交。
✅ 如果要手动管理事务,可以用 SET AUTOCOMMIT = 0
或 START TRANSACTION;
/ BEGIN;
。
✅ 手动管理事务时,需要显式 COMMIT
或 ROLLBACK
,否则事务可能会被锁住,影响并发性能。
如果想在事务中批量执行 SQL 并手动控制提交,建议使用 START TRANSACTION;
,这样不会影响整个会话的自动提交行为! 🚀
MyBatis 默认行为
如果在 Java 代码中使用 MyBatis 执行 SQL 语句,而没有显式管理事务(比如没有调用 commit()
或 rollback()
),那么默认情况下,MySQL 的 AUTOCOMMIT=1
会生效,每条 SQL 语句都会自动提交。
- MyBatis 不主动管理事务,而是依赖数据库的默认行为(自动提交)。
- 这意味着即使执行
INSERT
、UPDATE
、DELETE
语句,也不需要显式COMMIT
,数据会立即生效。
不手动管理事务(默认自动提交)
SqlSession sqlSession = sqlSessionFactory.openSession(); // 默认不手动控制事务
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
userMapper.insertUser(new User(1, "Alice")); // 这条 SQL 立即提交
sqlSession.close(); // 关闭连接
这里的 insertUser()
方法执行后,SQL 语句会立即提交,因为 MySQL 默认是 AUTOCOMMIT=1
。
如果想手动控制事务(比如执行多个 SQL 语句后统一提交),需要:
- 关闭自动提交(
openSession(false)
)。 - 手动
commit()
或rollback()
事务。
(2)手动管理事务
SqlSession sqlSession = sqlSessionFactory.openSession(false); // 关闭自动提交
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
userMapper.insertUser(new User(2, "Bob"));
userMapper.updateUser(new User(2, "Bob Updated"));
sqlSession.commit(); // 统一提交事务
} catch (Exception e) {
sqlSession.rollback(); // 发生异常则回滚
} finally {
sqlSession.close(); // 关闭连接
}
这里 openSession(false)
关闭了自动提交,所以:
insertUser()
和updateUser()
不会立即提交。- 只有
sqlSession.commit()
执行后,数据才会真正写入数据库。 - 如果发生异常,会执行
rollback()
,撤销事务中的所有操作。
Spring + MyBatis 事务管理
在实际的 Spring 项目中,事务通常是由 Spring 事务管理器(@Transactional
)来控制的,而不是手动调用 commit()
或 rollback()
。
(3)使用 Spring 事务管理
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Transactional // 由 Spring 统一管理事务
public void createUser() {
userMapper.insertUser(new User(3, "Charlie"));
userMapper.updateUser(new User(3, "Charlie Updated"));
// 发生异常时,Spring 会自动回滚事务
}
}
Spring 事务管理的特点:
@Transactional
让 Spring 统一管理事务,默认是 非自动提交 的(AUTOCOMMIT=0
)。- 方法执行成功时,Spring 自动提交 事务。
- 发生异常时,Spring 自动回滚 事务。
结论
✅ MyBatis 默认使用 MySQL 的 AUTOCOMMIT=1
,每条 SQL 语句执行后都会自动提交。
✅ 如果需要手动管理事务,可以使用 openSession(false)
关闭自动提交,并手动 commit()
或 rollback()
。
✅ 在 Spring 项目中,通常使用 @Transactional
让 Spring 统一管理事务,而不会直接调用 MyBatis 的 commit()
。
所以,如果 MyBatis 代码里没有 commit()
,而数据仍然成功写入数据库,那就是 MySQL 的自动提交功能在起作用! 🚀
SpringBoot 中的事务提交
在 Spring Boot 中,事务的提交方式主要取决于 是否使用了 Spring 事务管理(@Transactional
),并不是单纯依赖 MySQL 的 AUTOCOMMIT=1
。
1. Spring Boot 默认的事务行为
Spring Boot 默认不会自动提交事务,它的事务管理主要依赖于 Spring 事务管理器,而 Spring 事务管理默认是 AUTOCOMMIT=0
(手动提交)。
在 Spring Boot 项目中,通常有两种情况:
- 没有
@Transactional
(默认自动提交,每个 SQL 语句执行后都会立即提交,依赖 MySQLAUTOCOMMIT=1
)。 - 使用
@Transactional
(Spring 事务管理,事务方法结束后才会提交,默认AUTOCOMMIT=0
)。
2. 如果没有 @Transactional
,会使用 MySQL 自动提交
如果 没有 @Transactional
,那么 MyBatis 或 JPA 在执行 SQL 语句时,都会立即提交事务,这时依赖 MySQL 的 AUTOCOMMIT=1
。
3. 如果使用了 @Transactional
,Spring 事务管理接管提交
🚀 在 @Transactional
作用下:
- Spring 事务管理会关闭 MySQL 自动提交 (
AUTOCOMMIT=0
)。 - 所有 SQL 语句都会等到方法执行结束后才提交。
- 如果方法中抛出异常,Spring 会自动回滚事务,之前的 SQL 操作也不会生效。
4. 如何验证 Spring Boot 是否在使用事务?
可以在 application.properties
中查看 spring.datasource
配置:
spring.datasource.hikari.auto-commit=false # Hikari 数据源默认关闭自动提交
- 如果
auto-commit=false
,那么即使没有@Transactional
,数据库操作也不会立即提交,而是等待commit()
。 - 如果
auto-commit=true
,则默认会使用 MySQL 的AUTOCOMMIT=1
,每条 SQL 执行后立即提交。
此外,还可以手动检查数据库的 AUTOCOMMIT
状态:
SELECT @@autocommit;
如果返回 1
,说明 MySQL 处于自动提交模式,SQL 执行后会立即提交。
✅ 在 Spring Boot 代码中看不到 commit()
,是因为 Spring 事务管理器帮我们自动管理了事务提交,而不是依赖 MySQL 自动提交。
Spring Boot 代码中不需要手动
commit()
,并不一定是因为 MySQL 自动提交,而是 Spring 事务管理器负责了事务的提交和回滚! 🚀