深入理解MySQL MVCC:多版本并发控制原理与实战

71 阅读13分钟

深入理解MySQL MVCC:多版本并发控制原理与实战

目录


1. MVCC简介

1.1 什么是MVCC?

MVCC(Multi-Version Concurrency Control,多版本并发控制)是MySQL InnoDB存储引擎实现高并发的核心机制。它允许在不加锁的情况下,让多个事务并发读取数据,大幅提升数据库性能。

核心思想:通过保存数据的多个历史版本,让读操作不阻塞写操作,写操作也不阻塞读操作,实现读写并发。

1.2 MVCC解决的问题

  • 脏读:读取到未提交的数据
  • 不可重复读:同一事务内多次读取数据不一致
  • 幻读:同一事务内多次查询结果集行数不同(配合Next-Key Lock)

1.3 MVCC的优势

  1. 高并发性能:读不加锁,写不阻塞读
  2. 一致性读:保证事务隔离性
  3. 无需等待:减少锁等待时间
  4. 历史查询:可以查询历史版本数据

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最小活跃事务ID100
max_trx_id系统下一个要分配的事务ID106
creator_trx_id创建此ReadView的事务ID103

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核心要点

  1. 三大组件:隐藏字段、Undo Log、Read View
  2. 版本链:通过回滚指针形成历史版本链表
  3. 可见性判断:基于Read View的四条规则
  4. 隔离级别:RC和RR的Read View生成时机不同

9.2 最佳实践总结

场景建议
查询操作使用快照读,性能更好
修改前查询使用当前读(FOR UPDATE)
高并发扣库存使用悲观锁或乐观锁
事务管理避免长事务,缩小事务范围
隔离级别一般使用RR,特殊场景考虑RC

9.3 MVCC的局限性

  1. 不能完全解决幻读:需要配合Next-Key Lock
  2. Undo Log开销:长事务会占用大量空间
  3. 只适用于读多写少:写操作仍需加锁

附录:完整事务执行流程

完整流程

  1. BEGIN开启事务
  2. 分配事务ID
  3. 执行SQL(SELECT生成ReadView,UPDATE记录Undo Log)
  4. COMMIT提交(写Redo Log)
  5. 释放锁和资源

通过理解MVCC原理,我们可以更好地设计高并发系统,避免常见的并发问题,提升系统性能。