【面试题】数据库事务隔离与传播属性是什么?

数据库事务隔离与MVCC深度剖析

一、事务隔离问题详解

1. 脏读(Dirty Read)

定义 :一个事务读取了另一个未提交事务修改的数据。

核心问题:读到了"临时"的、可能被回滚的数据,破坏了数据一致性。

场景示例

sql 复制代码
-- 事务A(转账操作,但未提交)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 余额从1000改为900

-- 事务B(读取数据)
BEGIN;
SELECT balance FROM accounts WHERE id = 1;  -- 读到900(脏数据)
-- 此时页面显示用户余额为900

-- 事务A因异常回滚
ROLLBACK;
-- 实际余额仍为1000,但事务B以为余额是900

危害

  • 业务决策基于错误数据
  • 可能导致"幽灵数据"问题
  • 财务系统等对一致性要求高的场景绝对不能接受

2. 不可重复读(Non-repeatable Read)

定义:同一事务内,多次读取同一数据行,结果不一致(被其他已提交事务修改)。

核心问题 :事务内的读一致性被破坏。

场景示例

sql 复制代码
-- 事务A(统计报表事务)
BEGIN;
-- 第一次读取
SELECT balance FROM accounts WHERE id = 1;  -- 返回1000

-- 事务B(更新操作并提交)
BEGIN;
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;

-- 事务A继续
-- 第二次读取(同一事务内)
SELECT balance FROM accounts WHERE id = 1;  -- 返回900
-- 事务A同一数据行读取结果不一致,影响报表准确性
COMMIT;

与脏读的区别

  • 脏读:读取未提交的数据
  • 不可重复读:读取已提交的数据,但同一事务内前后不一致

3. 幻读(Phantom Read)

定义 :同一事务内,多次执行相同查询,返回的行数不同(被其他已提交事务插入/删除)。

核心问题 :影响范围查询的一致性。

场景示例

sql 复制代码
-- 事务A(统计部门人数)
BEGIN;
SELECT COUNT(*) FROM employees WHERE dept_id = 1;  -- 返回5人

-- 事务B(新增员工并提交)
BEGIN;
INSERT INTO employees(name, dept_id) VALUES('新员工', 1);
COMMIT;

-- 事务A再次统计
SELECT COUNT(*) FROM employees WHERE dept_id = 1;  -- 返回6人
-- 好像出现了"幻影行",统计结果不一致
COMMIT;

不可重复读 vs 幻读

  • 不可重复读:针对已存在行变化
  • 幻读:针对结果集行数变化(新增或删除行)

二、事务隔离级别详解

SQL标准定义的四个级别(从宽松到严格):

隔离级别 脏读 不可重复读 幻读 实现机制 性能 适用场景
READ UNCOMMITTED ❌ 可能 ❌ 可能 ❌ 可能 无锁/直接读 最高 数据仓库分析、不关心一致性的统计
READ COMMITTED ✅ 避免 ❌ 可能 ❌ 可能 MVCC+行锁 Oracle默认,Web应用常用
REPEATABLE READ ✅ 避免 ✅ 避免 ❌ 可能* MVCC+行锁+间隙锁 MySQL默认,需要读一致性
SERIALIZABLE ✅ 避免 ✅ 避免 ✅ 避免 严格锁/序列化 最低 金融交易、票务系统

*注:MySQL的REPEATABLE READ通过Next-Key Locking解决了大部分幻读问题

各数据库默认级别:

sql 复制代码
-- MySQL (默认: REPEATABLE READ)
SELECT @@transaction_isolation;  -- REPEATABLE-READ

-- PostgreSQL (默认: READ COMMITTED)
SHOW transaction_isolation;  -- read committed

-- Oracle (默认: READ COMMITTED)
-- SQL Server (默认: READ COMMITTED)

设置隔离级别示例:

sql 复制代码
-- 会话级别设置
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 全局设置
SET GLOBAL TRANSACTION ISLOLATION LEVEL REPEATABLE READ;

-- 在事务开始时指定
START TRANSACTION WITH CONSISTENT SNAPSHOT;

三、MVCC(多版本并发控制)深度剖析

什么是MVCC?

MVCC(Multi-Version Concurrency Control)是一种无锁并发控制技术,通过保存数据的多个版本来实现读写并发,避免读写冲突。

MVCC核心原理

1. 版本链机制

sql 复制代码
-- 每行数据隐藏的系统字段:
-- DB_TRX_ID: 创建/最后一次修改该行的事务ID
-- DB_ROLL_PTR: 回滚指针,指向undo log中的旧版本
-- DB_ROW_ID: 隐藏的自增ID(如果表没有主键)

版本链示例

复制代码
当前行 → 版本1 (事务10修改) → 版本2 (事务20修改) → 版本3 (事务30修改)
        ↑                   ↑                   ↑
    回滚指针             回滚指针             回滚指针

2. ReadView机制

每个事务开始时或执行查询时,会创建一个ReadView,包含:

  • trx_list: 当前活跃事务ID列表
  • up_limit_id: 活跃事务中最小ID
  • low_limit_id: 下一个将要分配的事务ID
  • creator_trx_id: 创建该ReadView的事务ID

3. 可见性判断规则

对于版本链中的每个版本:

  1. 如果 DB_TRX_ID < up_limit_id,说明在ReadView创建前已提交 → 可见
  2. 如果 DB_TRX_ID >= low_limit_id,说明在ReadView创建后才开始 → 不可见
  3. 如果 up_limit_id ≤ DB_TRX_ID < low_limit_id
    • 如果 DB_TRX_IDtrx_list 中,说明未提交 → 不可见
    • 否则已提交 → 可见

MVCC在不同隔离级别的表现

1. READ COMMITTED(读已提交)

sql 复制代码
-- 每次查询都生成新的ReadView
-- 只能看到已提交的数据
事务A: SELECT * FROM users;  -- 生成ReadView1
事务B: INSERT INTO users ... COMMIT;  -- 已提交
事务A: SELECT * FROM users;  -- 生成ReadView2,能看到B的修改

实现机制:每次SELECT都重新生成ReadView

2. REPEATABLE READ(可重复读)

sql 复制代码
-- 事务第一次查询时生成ReadView,后续复用
-- 保证同一事务内看到的数据一致
事务A: BEGIN;
事务A: SELECT * FROM users;  -- 生成ReadView(事务开始时)
事务B: INSERT INTO users ... COMMIT;
事务A: SELECT * FROM users;  -- 使用同一个ReadView,看不到B的插入

实现机制:事务开始时生成ReadView并复用

MVCC的Undo Log实现

sql 复制代码
-- 更新操作示例
UPDATE users SET name = 'Bob' WHERE id = 1;

-- MVCC执行流程:
1. 将当前行拷贝到Undo Log(保存旧版本)
2. 修改当前行,更新DB_TRX_ID为当前事务ID
3. 设置DB_ROLL_PTR指向Undo Log中的旧版本

-- 读操作:
通过版本链和ReadView找到合适的可见版本

MVCC的优缺点

优点:

  1. 读写不阻塞:读操作不会阻塞写操作,写操作不会阻塞读操作
  2. 高并发:避免锁竞争,提升并发性能
  3. 回滚高效:通过版本链快速回滚

缺点:

  1. 存储开销:需要存储多个版本的数据
  2. 清理机制:需要定期清理过期版本(purge操作)
  3. 写冲突:写操作之间仍可能冲突

MVCC与锁的配合

sql 复制代码
-- 实际是MVCC+锁的混合机制
SELECT * FROM users WHERE id = 1;  -- MVCC,无锁快照读
SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 当前读,加锁
UPDATE users SET name = '...' WHERE id = 1;  -- 当前读,加锁

四、Spring事务传播属性详解

传播属性是什么?

事务传播属性 定义了多个事务方法相互调用时,事务应该如何传播。它解决的是"事务边界"问题------当一个事务方法调用另一个事务方法时,这两个事务应该如何互动。

7种传播行为深度解析

1. REQUIRED(默认) - 需要事务

行为:如果当前存在事务,则加入该事务;如果当前没有事务,则新建一个事务。

使用场景:大多数业务方法,确保操作在事务中执行。

java 复制代码
@Service
public class OrderService {
    @Transactional(propagation = Propagation.REQUIRED)
    public void placeOrder(Order order) {
        // 如果调用方有事务,加入;否则新建事务
        orderDao.save(order);
        inventoryService.deductStock(order);  // 也会在同一个事务中
    }
}

2. REQUIRES_NEW - 新建事务

行为:创建一个新的事务,如果当前存在事务,则挂起当前事务。

使用场景:日志记录、审计操作等,需要独立提交,不受主事务影响。

java 复制代码
@Service
public class AuditService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logOperation(String action) {
        // 独立事务,即使主事务回滚,日志仍然保留
        auditDao.save(new AuditLog(action));
    }
}

// 调用示例
@Transactional
public void businessMethod() {
    try {
        // 业务操作
        orderService.process();
    } catch (Exception e) {
        // 即使业务回滚,审计日志仍然提交
        auditService.logOperation("业务异常: " + e.getMessage());
        throw e;
    }
}

3. NESTED - 嵌套事务

行为:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则新建事务。

关键特性 :使用保存点(Savepoint) 机制,可以部分回滚。

java 复制代码
@Service
public class ComplexService {
    @Transactional(propagation = Propagation.NESTED)
    public void updateUserProfile(User user, Profile profile) {
        userDao.update(user);
        profileDao.update(profile);
        // 如果失败,只回滚这个方法,不影响外部事务
    }
}

// 外层事务
@Transactional
public void completeUserRegistration(User user) {
    userService.register(user);           // 主事务的一部分
    complexService.updateUserProfile(...); // 嵌套事务,可独立回滚
    notificationService.sendWelcome(user); // 主事务的一部分
}

4. SUPPORTS - 支持事务

行为:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。

使用场景:查询方法,可以接受事务但不强求。

java 复制代码
@Service
public class QueryService {
    @Transactional(propagation = Propagation.SUPPORTS)
    public User getUserById(Long id) {
        // 有事务就加入,没有也无妨
        return userDao.findById(id);
    }
}

5. NOT_SUPPORTED - 不支持事务

行为:以非事务方式执行,如果当前存在事务,则挂起该事务。

使用场景:不需要事务支持的操作,如复杂计算、调用外部API。

java 复制代码
@Service
public class ReportService {
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public Report generateMonthlyReport() {
        // 复杂统计计算,不需要事务
        // 也不会受外部事务影响
        return reportDao.complexQuery();
    }
}

6. NEVER - 绝不使用事务

行为:以非事务方式执行,如果当前存在事务,则抛出异常。

使用场景:确保方法不在事务上下文中执行。

java 复制代码
@Service
public class UtilityService {
    @Transactional(propagation = Propagation.NEVER)
    public void clearCache() {
        // 缓存清理,绝对不能有事务
        // 如果调用方有事务,会抛出异常
        cacheManager.clearAll();
    }
}

7. MANDATORY - 强制存在事务

行为:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

使用场景:必须在事务中执行的关键操作。

java 复制代码
@Service
public class PaymentService {
    @Transactional(propagation = Propagation.MANDATORY)
    public void processPayment(Payment payment) {
        // 必须在事务中执行,否则报错
        // 确保数据一致性
        accountDao.deduct(payment.getAmount());
        paymentDao.save(payment);
    }
}

传播属性组合使用策略

分层架构中的传播属性设计:

java 复制代码
// Controller层 - 不管理事务
@RestController
public class UserController {
    @Autowired
    private UserFacade userFacade;
}

// Facade/Service层 - 开启事务
@Service
public class UserFacade {
    @Transactional(propagation = Propagation.REQUIRED)
    public UserDTO registerUser(UserRequest request) {
        // 协调多个Service,统一事务管理
        User user = userService.createUser(request);
        profileService.initProfile(user.getId());
        auditService.logRegistration(user);
        return convertToDTO(user);
    }
}

// 业务Service层 - 根据需要使用不同传播属性
@Service
public class UserService {
    // 主业务方法,使用默认REQUIRED
    @Transactional
    public User createUser(UserRequest request) {
        return userRepository.save(convertToEntity(request));
    }
}

@Service  
public class AuditService {
    // 审计日志,独立事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logRegistration(User user) {
        auditRepository.save(new AuditLog("USER_REGISTER", user.getId()));
    }
}

@Service
public class ProfileService {
    // 嵌套事务,可部分回滚
    @Transactional(propagation = Propagation.NESTED)
    public void initProfile(Long userId) {
        profileRepository.createDefaultProfile(userId);
    }
}

常见陷阱与解决方案:

java 复制代码
// 陷阱1:自调用导致@Transactional失效
@Service
public class OrderService {
    public void processOrder(Order order) {
        // 自调用,@Transactional失效!
        this.updateInventory(order);  
    }
    
    @Transactional
    public void updateInventory(Order order) {
        // 不会开启事务
    }
}

// 解决方案1:使用代理对象
@Service
public class OrderService {
    @Autowired
    private OrderService selfProxy;  // 注入自身代理
    
    public void processOrder(Order order) {
        selfProxy.updateInventory(order);  // 通过代理调用
    }
}

// 解决方案2:使用AopContext
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
    // 配置类启用代理暴露
}

public void processOrder(Order order) {
    OrderService proxy = (OrderService) AopContext.currentProxy();
    proxy.updateInventory(order);
}

// 陷阱2:异常被捕获不抛出
@Transactional
public void saveWithRollback() {
    try {
        userRepository.save(user);
        throw new RuntimeException();  // 触发回滚的异常
    } catch (Exception e) {
        // 异常被捕获,事务不会回滚!
        log.error("Error occurred", e);
    }
}

// 解决方案:手动回滚或重新抛出
@Transactional
public void saveWithRollback() {
    try {
        userRepository.save(user);
        throw new RuntimeException();
    } catch (Exception e) {
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        throw e;  // 或者重新抛出
    }
}

隔离级别与传播属性的组合实践

java 复制代码
@Service
public class FinancialService {
    
    // 资金转移:最高隔离级别,需要事务
    @Transactional(
        isolation = Isolation.SERIALIZABLE,
        propagation = Propagation.REQUIRED,
        timeout = 30,
        rollbackFor = {BusinessException.class, RuntimeException.class}
    )
    public void transferFunds(TransferRequest request) {
        // 1. 检查账户(需要一致性读)
        Account from = accountService.getAccount(request.getFromAccountId());
        Account to = accountService.getAccount(request.getToAccountId());
        
        // 2. 扣款(强一致性要求)
        accountService.deduct(from, request.getAmount());
        
        // 3. 存款
        accountService.deposit(to, request.getAmount());
        
        // 4. 记录交易日志(独立事务)
        auditService.logTransaction(request);
        
        // 5. 发送通知(非事务,不影响主流程)
        notificationService.sendTransferNotification(request);
    }
}

@Service
public class AccountService {
    @Transactional(
        isolation = Isolation.REPEATABLE_READ,
        propagation = Propagation.MANDATORY  // 必须在外层事务中调用
    )
    public void deduct(Account account, BigDecimal amount) {
        // 扣款操作
    }
}

@Service
public class AuditService {
    @Transactional(
        propagation = Propagation.REQUIRES_NEW,  // 独立事务
        isolation = Isolation.READ_COMMITTED     // 日志不需要强一致性
    )
    public void logTransaction(TransferRequest request) {
        // 审计日志
    }
}

@Service
public class NotificationService {
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void sendTransferNotification(TransferRequest request) {
        // 调用外部通知服务,不需要事务
    }
}

五、最佳实践总结

1. 隔离级别选择原则:

  • Web应用:READ COMMITTED(平衡性能与一致性)
  • 金融系统:REPEATABLE READ 或 SERIALIZABLE(强一致性)
  • 报表系统:READ UNCOMMITTED 或 READ COMMITTED(查询性能优先)
  • 电商系统:根据业务模块选择不同级别

2. 传播属性使用指南:

  • 默认使用:REQUIRED(满足80%场景)
  • 日志审计:REQUIRES_NEW(独立提交)
  • 复杂业务:NESTED(部分回滚能力)
  • 查询方法:SUPPORTS(灵活适应)
  • 外部调用:NOT_SUPPORTED(避免事务传播)

3. MVCC优化建议:

  • 控制事务长度:避免长事务导致版本链过长
  • 合理设计索引:提升快照读效率
  • 定期清理:监控undo log大小,避免膨胀
  • 版本选择:根据业务选择当前读或快照读

4. 监控与调优:

sql 复制代码
-- 监控长事务
SELECT * FROM information_schema.innodb_trx 
WHERE TIME_TO_SEC(timediff(now(), trx_started)) > 60;

-- 查看锁等待
SELECT * FROM information_schema.innodb_lock_waits;

-- 监控undo log
SHOW VARIABLES LIKE 'innodb_undo%';

理解这些核心概念和技术细节,可以帮助你设计出更合理、高性能的数据库应用架构,有效平衡一致性、并发性和性能之间的关系。

相关推荐
fengxin_rou3 天前
【MySQL 事务并发实战】:隔离级别、MVCC 与幻读问题彻底解
数据库·mysql·事务·并发
无小道7 天前
Mysql——吃透事务以及隔离级别
mysql·面试·事务·隔离级别
熏鱼的小迷弟Liu8 天前
【MySQL】MySQL中的MVCC是什么?它与隔离级别有什么关系?
mysql·mvcc
wenha23 天前
数据库隔离级别
数据库·mysql·sqlserver·隔离级别
九皇叔叔24 天前
MySQL 8.x 隔离级别调整
数据库·mysql·事务·隔离级别
阿维的博客日记24 天前
我现在能理解mvcc让读不阻塞,但是无法理解mvcc让写不阻塞??
mysql·事务·mvcc
阿维的博客日记24 天前
隔离性和mvcc有什么关系吗
数据库·mysql·事务·mvcc·隔离性
识君啊1 个月前
中小厂数据库事务高频面试题
java·数据库·mysql·隔离级别·数据库事务·acid
ん贤1 个月前
数据库事务
数据库·mysql·事务
鬼先生_sir1 个月前
MySQL进阶-事务与锁机制
数据库·mysql·mvcc