深入理解MySQL MVCC:多版本并发控制原理与实战
目录
- 1. MVCC简介
- 2. MVCC核心原理
- 3. Undo Log版本链
- 4. Read View机制
- 5. 事务隔离级别与MVCC
- 6. 当前读vs快照读
- 7. Java实战案例
- 8. 生产环境最佳实践
- 9. 总结
1. MVCC简介
1.1 什么是MVCC?
MVCC(Multi-Version Concurrency Control,多版本并发控制)是MySQL InnoDB存储引擎实现高并发的核心机制。它允许在不加锁的情况下,让多个事务并发读取数据,大幅提升数据库性能。
核心思想:通过保存数据的多个历史版本,让读操作不阻塞写操作,写操作也不阻塞读操作,实现读写并发。
1.2 MVCC解决的问题
- 脏读:读取到未提交的数据
- 不可重复读:同一事务内多次读取数据不一致
- 幻读:同一事务内多次查询结果集行数不同(配合Next-Key Lock)
1.3 MVCC的优势
- 高并发性能:读不加锁,写不阻塞读
- 一致性读:保证事务隔离性
- 无需等待:减少锁等待时间
- 历史查询:可以查询历史版本数据
2. MVCC核心原理
2.1 三大核心组件
MVCC由三个核心组件构成:
1. 隐藏字段
InnoDB为每行数据添加了三个隐藏字段:
-- 实际表结构
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT
);
-- InnoDB内部实际存储
-- id | name | age | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID
隐藏字段说明:
- DB_TRX_ID(6字节):最后修改该记录的事务ID
- DB_ROLL_PTR(7字节):回滚指针,指向Undo Log中的历史版本
- DB_ROW_ID(6字节):隐藏主键,当表没有主键时自动生成
2. Undo Log(回滚日志)
Undo Log用于存储数据的历史版本,主要作用:
- 事务回滚时恢复数据
- 实现MVCC的多版本读取
3. Read View(读视图)
Read View是事务进行快照读时产生的读视图,用于判断版本的可见性。
2.2 Java模拟MVCC核心数据结构
/**
* 模拟InnoDB行记录结构
*/
public class RowRecord {
// 业务字段
private Integer id;
private String name;
private Integer age;
// 隐藏字段
private Long dbTrxId; // 最后修改该记录的事务ID
private Long dbRollPtr; // 回滚指针,指向Undo Log
private Long dbRowId; // 隐藏主键
public RowRecord(Integer id, String name, Integer age, Long dbTrxId) {
this.id = id;
this.name = name;
this.age = age;
this.dbTrxId = dbTrxId;
}
// Getters and Setters...
}
/**
* Undo Log记录(历史版本)
*/
public class UndoLogRecord {
private Long trxId; // 事务ID
private RowRecord oldValue; // 历史版本数据
private Long rollPtr; // 指向更早的版本
public UndoLogRecord(Long trxId, RowRecord oldValue, Long rollPtr) {
this.trxId = trxId;
this.oldValue = oldValue;
this.rollPtr = rollPtr;
}
// Getters and Setters...
}
/**
* Read View结构
*/
public class ReadView {
private List<Long> mIds; // 活跃事务ID列表
private Long minTrxId; // 最小活跃事务ID
private Long maxTrxId; // 下一个要分配的事务ID
private Long creatorTrxId; // 创建此ReadView的事务ID
public ReadView(List<Long> mIds, Long minTrxId, Long maxTrxId, Long creatorTrxId) {
this.mIds = mIds;
this.minTrxId = minTrxId;
this.maxTrxId = maxTrxId;
this.creatorTrxId = creatorTrxId;
}
/**
* 判断版本是否可见
*/
public boolean isVisible(Long trxId) {
// 1. 如果版本的事务ID小于最小活跃事务ID,说明该版本已提交,可见
if (trxId < minTrxId) {
return true;
}
// 2. 如果版本的事务ID大于等于下一个事务ID,说明该版本在ReadView之后才开始,不可见
if (trxId >= maxTrxId) {
return false;
}
// 3. 如果版本的事务ID在活跃列表中,说明该事务还未提交,不可见
if (mIds.contains(trxId)) {
return false;
}
// 4. 如果是当前事务自己的修改,可见
if (trxId.equals(creatorTrxId)) {
return true;
}
// 5. 其他情况可见
return true;
}
}
3. Undo Log版本链
3.1 版本链形成过程
当一行数据被多次修改时,会形成一个版本链:
-- 初始数据 (事务100)
INSERT INTO user VALUES (1, '张三', 25);
-- 事务101修改
UPDATE user SET name = '李四' WHERE id = 1;
-- 事务102修改
UPDATE user SET name = '王五' WHERE id = 1;
-- 事务103修改
UPDATE user SET name = '赵六' WHERE id = 1;
版本链结构:
当前版本 (表中) Undo Log v3 Undo Log v2 Undo Log v1
name='赵六' ←--- name='王五' ←--- name='李四' ←--- name='张三'
TRX_ID=103 TRX_ID=102 TRX_ID=101 TRX_ID=100
3.2 Java模拟版本链查询
public class VersionChainDemo {
/**
* 沿着版本链查找可见版本
*/
public static RowRecord findVisibleVersion(RowRecord current, ReadView readView) {
RowRecord version = current;
// 沿着版本链向前查找
while (version != null) {
Long trxId = version.getDbTrxId();
// 判断当前版本是否可见
if (readView.isVisible(trxId)) {
return version; // 找到可见版本
}
// 继续查找上一个版本
version = getOldVersion(version.getDbRollPtr());
}
return null; // 没有可见版本
}
/**
* 通过回滚指针获取历史版本
*/
private static RowRecord getOldVersion(Long rollPtr) {
// 实际实现中,这里会从Undo Log中读取历史版本
// 这里简化处理
return null;
}
public static void main(String[] args) {
// 模拟当前版本
RowRecord current = new RowRecord(1, "赵六", 25, 103L);
// 模拟ReadView:事务104读取数据
// 活跃事务:[100, 102, 103]
ReadView readView = new ReadView(
Arrays.asList(100L, 102L, 103L), // m_ids
100L, // min_trx_id
104L, // max_trx_id
104L // creator_trx_id
);
// 查找可见版本
RowRecord visible = findVisibleVersion(current, readView);
if (visible != null) {
System.out.println("可见版本: " + visible.getName());
} else {
System.out.println("无可见版本");
}
}
}
4. Read View机制
4.1 Read View结构详解
Read View包含4个核心字段:
| 字段 | 说明 | 示例 |
|---|---|---|
| m_ids | 生成ReadView时活跃的事务ID列表 | [100, 102, 105] |
| min_trx_id | 最小活跃事务ID | 100 |
| max_trx_id | 系统下一个要分配的事务ID | 106 |
| creator_trx_id | 创建此ReadView的事务ID | 103 |
4.2 可见性判断算法
public class VisibilityChecker {
/**
- MVCC可见性判断核心算法
*/
public static boolean isVisible(Long dbTrxId, ReadView readView) {
// 规则1: 如果版本的事务ID小于最小活跃事务ID
// 说明该事务已提交,且在ReadView创建之前
if (dbTrxId < readView.getMinTrxId()) {
return true; // ✓ 可见
}
// 规则2: 如果版本的事务ID大于等于下一个事务ID
// 说明该事务在ReadView创建之后才启动
if (dbTrxId >= readView.getMaxTrxId()) {
return false; // ✗ 不可见,需要查找Undo Log
}
// 规则3: 如果版本的事务ID在活跃列表中
// 说明该事务还在运行中,未提交
if (readView.getMIds().contains(dbTrxId)) {
return false; // ✗ 不可见,需要查找Undo Log
}
// 规则4: 如果是当前事务自己的修改
if (dbTrxId.equals(readView.getCreatorTrxId())) {
return true; // ✓ 可见(自己的修改一定可见)
}
// 规则5: 其他情况
// 说明该事务已提交,且在ReadView创建范围内
return true; // ✓ 可见
}
/**
- 测试可见性判断
*/
public static void main(String[] args) {
// 创建ReadView:事务103读取数据
ReadView readView = new ReadView(
Arrays.asList(100L, 102L, 105L), // 活跃事务
100L, // 最小活跃事务ID
106L, // 下一个事务ID
103L // 当前事务ID
);
// 测试不同版本的可见性
System.out.println("事务99的版本: " + isVisible(99L, readView)); // true
System.out.println("事务100的版本: " + isVisible(100L, readView)); // false
System.out.println("事务101的版本: " + isVisible(101L, readView)); // true
System.out.println("事务102的版本: " + isVisible(102L, readView)); // false
System.out.println("事务103的版本: " + isVisible(103L, readView)); // true
System.out.println("事务106的版本: " + isVisible(106L, readView)); // false
}
}
4.3 Read View生成时机
不同的隔离级别,Read View的生成时机不同:
- READ COMMITTED(RC) :每次SELECT都生成新的Read View
- REPEATABLE READ(RR) :事务开始时(第一次SELECT)生成Read View,整个事务期间复用
5. 事务隔离级别与MVCC
5.1 隔离级别对比
MySQL支持4种隔离级别,InnoDB默认使用REPEATABLE READ:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 不使用MVCC |
| READ COMMITTED | 不会 | 可能 | 可能 | 每次读生成ReadView |
| REPEATABLE READ | 不会 | 不会 | 可能 | 首次读生成ReadView |
| SERIALIZABLE | 不会 | 不会 | 不会 | 加锁,不用MVCC |
5.2 Java设置隔离级别
import java.sql.*;
public class IsolationLevelDemo {
/**
* 演示不同隔离级别的效果
*/
public static void main(String[] args) throws SQLException {
String url = "jdbc:mysql://localhost:3306/test?useSSL=false";
String user = "root";
String password = "123456";
Connection conn = DriverManager.getConnection(url, user, password);
// 查看当前隔离级别
printCurrentIsolationLevel(conn);
// 设置为 READ COMMITTED
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
System.out.println("设置隔离级别为: READ COMMITTED");
// 设置为 REPEATABLE READ(MySQL默认)
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
System.out.println("设置隔离级别为: REPEATABLE READ");
conn.close();
}
/**
* 打印当前隔离级别
*/
private static void printCurrentIsolationLevel(Connection conn) throws SQLException {
int level = conn.getTransactionIsolation();
String levelName;
switch (level) {
case Connection.TRANSACTION_READ_UNCOMMITTED:
levelName = "READ UNCOMMITTED";
break;
case Connection.TRANSACTION_READ_COMMITTED:
levelName = "READ COMMITTED";
break;
case Connection.TRANSACTION_REPEATABLE_READ:
levelName = "REPEATABLE READ";
break;
case Connection.TRANSACTION_SERIALIZABLE:
levelName = "SERIALIZABLE";
break;
default:
levelName = "UNKNOWN";
}
System.out.println("当前隔离级别: " + levelName);
}
}
6. 当前读vs快照读
6.1 两种读取方式对比
快照读(Snapshot Read)
- 定义:读取的是历史版本数据(通过MVCC实现)
- 不加锁:性能高,不阻塞其他事务
- SQL语句: SELECT * FROM user WHERE id = 1;
当前读(Current Read)
- 定义:读取的是最新版本数据
- 加锁:加共享锁或排他锁
- SQL语句: “`sql – 加共享锁(S锁) SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
– 加排他锁(X锁) SELECT * FROM user WHERE id = 1 FOR UPDATE;
– UPDATE、DELETE、INSERT也是当前读 UPDATE user SET age = 26 WHERE id = 1;
### 6.2 Java演示快照读和当前读
```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MVCCReadDemo {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 快照读示例
*/
@Transactional
public void snapshotReadDemo() {
// 普通SELECT是快照读
String sql = "SELECT * FROM user WHERE id = ?";
Map<String, Object> user = jdbcTemplate.queryForMap(sql, 1);
System.out.println("快照读结果: " + user);
// 读取的是事务开始时的数据快照
}
/**
* 当前读示例 - 共享锁
*/
@Transactional
public void currentReadWithSharedLock() {
// 加共享锁的当前读
String sql = "SELECT * FROM user WHERE id = ? LOCK IN SHARE MODE";
Map<String, Object> user = jdbcTemplate.queryForMap(sql, 1);
System.out.println("当前读(共享锁)结果: " + user);
// 读取最新数据,其他事务可以读但不能写
}
/**
* 当前读示例 - 排他锁
*/
@Transactional
public void currentReadWithExclusiveLock() {
// 加排他锁的当前读
String sql = "SELECT * FROM user WHERE id = ? FOR UPDATE";
Map<String, Object> user = jdbcTemplate.queryForMap(sql, 1);
System.out.println("当前读(排他锁)结果: " + user);
// 读取最新数据,其他事务既不能读也不能写
// 修改数据(自动使用当前读)
jdbcTemplate.update("UPDATE user SET age = age + 1 WHERE id = ?", 1);
}
/**
* 演示可重复读
*/
@Transactional
public void repeatableReadDemo() throws InterruptedException {
// 第一次读取(生成ReadView)
String sql = "SELECT age FROM user WHERE id = ?";
Integer age1 = jdbcTemplate.queryForObject(sql, Integer.class, 1);
System.out.println("第一次读取 age = " + age1);
// 等待5秒(模拟其他事务修改数据)
Thread.sleep(5000);
// 第二次读取(使用同一个ReadView)
Integer age2 = jdbcTemplate.queryForObject(sql, Integer.class, 1);
System.out.println("第二次读取 age = " + age2);
// 在REPEATABLE READ级别下,age1 == age2(可重复读)
System.out.println("是否可重复读: " + age1.equals(age2));
}
}
7. Java实战案例
7.1 Spring Boot + MyBatis Plus集成
Maven依赖
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.14</version>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
</dependency>
</dependencies>
配置文件
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志
实体类
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
@Data
@TableName("account")
public class Account {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private Double balance;
@Version // 乐观锁版本号
private Integer version;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
Mapper接口
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
/**
* 当前读 - 加排他锁
*/
@Select("SELECT * FROM account WHERE id = #{id} FOR UPDATE")
Account selectByIdForUpdate(Long id);
/**
* 当前读 - 加共享锁
*/
@Select("SELECT * FROM account WHERE id = #{id} LOCK IN SHARE MODE")
Account selectByIdLockInShareMode(Long id);
}
Service层
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountService extends ServiceImpl<AccountMapper, Account> {
/**
* 转账案例 - 演示MVCC和锁机制
*/
@Transactional(rollbackFor = Exception.class)
public void transfer(Long fromId, Long toId, Double amount) {
// 1. 使用当前读锁定账户(FOR UPDATE)
Account from = baseMapper.selectByIdForUpdate(fromId);
Account to = baseMapper.selectByIdForUpdate(toId);
// 2. 检查余额
if (from.getBalance() < amount) {
throw new RuntimeException("余额不足");
}
// 3. 扣款
from.setBalance(from.getBalance() - amount);
updateById(from);
// 4. 入账
to.setBalance(to.getBalance() + amount);
updateById(to);
System.out.println("转账成功: " + fromId + " -> " + toId + ", 金额: " + amount);
}
/**
* 演示快照读
*/
@Transactional
public void snapshotReadTest(Long accountId) throws InterruptedException {
// 第一次快照读
Account account1 = getById(accountId);
System.out.println("第一次读取余额: " + account1.getBalance());
// 等待(模拟其他事务修改)
Thread.sleep(3000);
// 第二次快照读(RR级别下,结果相同)
Account account2 = getById(accountId);
System.out.println("第二次读取余额: " + account2.getBalance());
// 当前读(读取最新数据)
Account account3 = baseMapper.selectByIdForUpdate(accountId);
System.out.println("当前读取余额: " + account3.getBalance());
}
}
7.2 模拟并发场景
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@SpringBootTest
public class MVCCConcurrencyTest {
@Autowired
private AccountService accountService;
/**
- 测试并发转账 - MVCC + 悲观锁
*/
@Test
public void testConcurrentTransfer() throws InterruptedException {
// 初始化账户
Account account1 = new Account();
account1.setUsername("用户A");
account1.setBalance(1000.0);
accountService.save(account1);
Account account2 = new Account();
account2.setUsername("用户B");
account2.setBalance(1000.0);
accountService.save(account2);
// 并发执行10次转账
int threadCount = 10;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
// A向B转账100元
accountService.transfer(account1.getId(), account2.getId(), 100.0);
} catch (Exception e) {
System.out.println("转账失败: " + e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
// 验证结果
Account finalA = accountService.getById(account1.getId());
Account finalB = accountService.getById(account2.getId());
System.out.println("账户A最终余额: " + finalA.getBalance());
System.out.println("账户B最终余额: " + finalB.getBalance());
System.out.println("总金额: " + (finalA.getBalance() + finalB.getBalance()));
// 预期: A = 0, B = 2000(由于并发控制,只有部分转账成功)
}
}
8. 生产环境最佳实践
8.1 合理选择隔离级别
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("123456");
// 设置连接初始化SQL(设置隔离级别)
dataSource.setConnectionInitSql(
"SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ"
);
return dataSource;
}
}
选择建议:
- 金融系统:使用REPEATABLE READ或SERIALIZABLE
- 一般应用:使用REPEATABLE READ(MySQL默认)
- 高并发读多写少:可考虑READ COMMITTED
8.2 避免长事务
@Service
public class OrderService {
/**
* ❌ 不好的实践:长事务
*/
@Transactional
public void badPractice(Long orderId) {
// 1. 查询订单
Order order = orderMapper.selectById(orderId);
// 2. 调用外部API(耗时操作)
externalApiService.notifyThirdParty(order); // 可能耗时5秒
// 3. 更新订单状态
orderMapper.updateById(order);
// 问题:事务持续时间过长,锁定资源时间长,影响并发
}
/**
* ✓ 好的实践:缩短事务范围
*/
public void goodPractice(Long orderId) {
// 1. 先查询(不在事务中)
Order order = orderMapper.selectById(orderId);
// 2. 调用外部API(不在事务中)
externalApiService.notifyThirdParty(order);
// 3. 只在更新时开启事务
updateOrderStatus(orderId, order.getStatus());
}
@Transactional
public void updateOrderStatus(Long orderId, Integer status) {
orderMapper.updateStatusById(orderId, status);
}
}
8.3 合理使用锁
@Service
public class InventoryService {
/**
* 扣减库存 - 使用悲观锁
*/
@Transactional
public boolean deductStock(Long productId, Integer quantity) {
// 1. 使用FOR UPDATE锁定库存记录
Product product = productMapper.selectByIdForUpdate(productId);
if (product == null) {
return false;
}
// 2. 检查库存
if (product.getStock() < quantity) {
throw new RuntimeException("库存不足");
}
// 3. 扣减库存
product.setStock(product.getStock() - quantity);
productMapper.updateById(product);
return true;
}
/**
* 扣减库存 - 使用乐观锁(MyBatis Plus)
*/
@Transactional
public boolean deductStockOptimistic(Long productId, Integer quantity) {
// 1. 查询库存
Product product = productMapper.selectById(productId);
if (product == null || product.getStock() < quantity) {
return false;
}
// 2. 扣减库存(MyBatis Plus自动处理version字段)
product.setStock(product.getStock() - quantity);
// 3. 更新(如果version不匹配,返回0)
int updated = productMapper.updateById(product);
return updated > 0;
}
}
8.4 监控和调优
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class MySQLMonitor {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 定期检查事务状态
*/
@Scheduled(fixedRate = 60000) // 每分钟执行一次
public void monitorTransactions() {
// 查询长事务
String sql = "SELECT * FROM information_schema.innodb_trx " +
"WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 10";
List<Map<String, Object>> longTransactions = jdbcTemplate.queryForList(sql);
if (!longTransactions.isEmpty()) {
System.out.println("发现长事务: " + longTransactions.size() + "个");
longTransactions.forEach(trx -> {
System.out.println("事务ID: " + trx.get("trx_id"));
System.out.println("持续时间: " + trx.get("trx_started"));
System.out.println("SQL: " + trx.get("trx_query"));
});
}
}
/**
* 查看锁等待情况
*/
public void checkLockWaits() {
String sql = "SELECT * FROM information_schema.innodb_lock_waits";
List<Map<String, Object>> lockWaits = jdbcTemplate.queryForList(sql);
if (!lockWaits.isEmpty()) {
System.out.println("发现锁等待: " + lockWaits.size() + "个");
}
}
}
9. 总结
9.1 MVCC核心要点
- 三大组件:隐藏字段、Undo Log、Read View
- 版本链:通过回滚指针形成历史版本链表
- 可见性判断:基于Read View的四条规则
- 隔离级别:RC和RR的Read View生成时机不同
9.2 最佳实践总结
| 场景 | 建议 |
|---|---|
| 查询操作 | 使用快照读,性能更好 |
| 修改前查询 | 使用当前读(FOR UPDATE) |
| 高并发扣库存 | 使用悲观锁或乐观锁 |
| 事务管理 | 避免长事务,缩小事务范围 |
| 隔离级别 | 一般使用RR,特殊场景考虑RC |
9.3 MVCC的局限性
- 不能完全解决幻读:需要配合Next-Key Lock
- Undo Log开销:长事务会占用大量空间
- 只适用于读多写少:写操作仍需加锁
附录:完整事务执行流程
完整流程:
- BEGIN开启事务
- 分配事务ID
- 执行SQL(SELECT生成ReadView,UPDATE记录Undo Log)
- COMMIT提交(写Redo Log)
- 释放锁和资源
通过理解MVCC原理,我们可以更好地设计高并发系统,避免常见的并发问题,提升系统性能。