找回密码
 立即注册
首页 业界区 业界 【面试题】数据库事务隔离与传播属性是什么? ...

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

茅香馨 昨天 23:45
数据库事务隔离与MVCC深度剖析

一、事务隔离问题详解

1. 脏读(Dirty Read)

定义:一个事务读取了另一个未提交事务修改的数据。
核心问题:读到了"临时"的、可能被回滚的数据,破坏了数据一致性。
场景示例
  1. -- 事务A(转账操作,但未提交)
  2. BEGIN;
  3. UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- 余额从1000改为900
  4. -- 事务B(读取数据)
  5. BEGIN;
  6. SELECT balance FROM accounts WHERE id = 1;  -- 读到900(脏数据)
  7. -- 此时页面显示用户余额为900
  8. -- 事务A因异常回滚
  9. ROLLBACK;
  10. -- 实际余额仍为1000,但事务B以为余额是900
复制代码
危害

  • 业务决策基于错误数据
  • 可能导致"幽灵数据"问题
  • 财务系统等对一致性要求高的场景绝对不能接受
2. 不可重复读(Non-repeatable Read)

定义:同一事务内,多次读取同一数据行,结果不一致(被其他已提交事务修改)。
核心问题:事务内的读一致性被破坏。
场景示例
  1. -- 事务A(统计报表事务)
  2. BEGIN;
  3. -- 第一次读取
  4. SELECT balance FROM accounts WHERE id = 1;  -- 返回1000
  5. -- 事务B(更新操作并提交)
  6. BEGIN;
  7. UPDATE accounts SET balance = 900 WHERE id = 1;
  8. COMMIT;
  9. -- 事务A继续
  10. -- 第二次读取(同一事务内)
  11. SELECT balance FROM accounts WHERE id = 1;  -- 返回900
  12. -- 事务A同一数据行读取结果不一致,影响报表准确性
  13. COMMIT;
复制代码
与脏读的区别

  • 脏读:读取未提交的数据
  • 不可重复读:读取已提交的数据,但同一事务内前后不一致
3. 幻读(Phantom Read)

定义:同一事务内,多次执行相同查询,返回的行数不同(被其他已提交事务插入/删除)。
核心问题:影响范围查询的一致性。
场景示例
  1. -- 事务A(统计部门人数)
  2. BEGIN;
  3. SELECT COUNT(*) FROM employees WHERE dept_id = 1;  -- 返回5人
  4. -- 事务B(新增员工并提交)
  5. BEGIN;
  6. INSERT INTO employees(name, dept_id) VALUES('新员工', 1);
  7. COMMIT;
  8. -- 事务A再次统计
  9. SELECT COUNT(*) FROM employees WHERE dept_id = 1;  -- 返回6人
  10. -- 好像出现了"幻影行",统计结果不一致
  11. COMMIT;
复制代码
不可重复读 vs 幻读

  • 不可重复读:针对已存在行变化
  • 幻读:针对结果集行数变化(新增或删除行)
二、事务隔离级别详解

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

隔离级别脏读不可重复读幻读实现机制性能适用场景READ UNCOMMITTED❌ 可能❌ 可能❌ 可能无锁/直接读最高数据仓库分析、不关心一致性的统计READ COMMITTED✅ 避免❌ 可能❌ 可能MVCC+行锁高Oracle默认,Web应用常用REPEATABLE READ✅ 避免✅ 避免❌ 可能*MVCC+行锁+间隙锁中MySQL默认,需要读一致性SERIALIZABLE✅ 避免✅ 避免✅ 避免严格锁/序列化最低金融交易、票务系统*注:MySQL的REPEATABLE READ通过Next-Key Locking解决了大部分幻读问题
各数据库默认级别:
  1. -- MySQL (默认: REPEATABLE READ)
  2. SELECT @@transaction_isolation;  -- REPEATABLE-READ
  3. -- PostgreSQL (默认: READ COMMITTED)
  4. SHOW transaction_isolation;  -- read committed
  5. -- Oracle (默认: READ COMMITTED)
  6. -- SQL Server (默认: READ COMMITTED)
复制代码
设置隔离级别示例:
  1. -- 会话级别设置
  2. SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
  3. -- 全局设置
  4. SET GLOBAL TRANSACTION ISLOLATION LEVEL REPEATABLE READ;
  5. -- 在事务开始时指定
  6. START TRANSACTION WITH CONSISTENT SNAPSHOT;
复制代码
三、MVCC(多版本并发控制)深度剖析

什么是MVCC?

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

1. 版本链机制
  1. -- 每行数据隐藏的系统字段:
  2. -- DB_TRX_ID: 创建/最后一次修改该行的事务ID
  3. -- DB_ROLL_PTR: 回滚指针,指向undo log中的旧版本
  4. -- DB_ROW_ID: 隐藏的自增ID(如果表没有主键)
复制代码
版本链示例
  1. 当前行 → 版本1 (事务10修改) → 版本2 (事务20修改) → 版本3 (事务30修改)
  2.         ↑                   ↑                   ↑
  3.     回滚指针             回滚指针             回滚指针
复制代码
2. ReadView机制

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

  • trx_list: 当前活跃事务ID列表
  • up_limit_id: 活跃事务中最小ID
  • low_limit_id: 下一个将要分配的事务ID
  • creator_trx_id: 创建该ReadView的事务ID
3. 可见性判断规则

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

  • 如果 DB_TRX_ID < up_limit_id,说明在ReadView创建前已提交 → 可见
  • 如果 DB_TRX_ID >= low_limit_id,说明在ReadView创建后才开始 → 不可见
  • 如果 up_limit_id ≤ DB_TRX_ID < low_limit_id:

    • 如果 DB_TRX_ID 在 trx_list 中,说明未提交 → 不可见
    • 否则已提交 → 可见

MVCC在不同隔离级别的表现

1. READ COMMITTED(读已提交)
  1. -- 每次查询都生成新的ReadView
  2. -- 只能看到已提交的数据
  3. 事务A: SELECT * FROM users;  -- 生成ReadView1
  4. 事务B: INSERT INTO users ... COMMIT;  -- 已提交
  5. 事务A: SELECT * FROM users;  -- 生成ReadView2,能看到B的修改
复制代码
实现机制:每次SELECT都重新生成ReadView
2. REPEATABLE READ(可重复读)
  1. -- 事务第一次查询时生成ReadView,后续复用
  2. -- 保证同一事务内看到的数据一致
  3. 事务A: BEGIN;
  4. 事务A: SELECT * FROM users;  -- 生成ReadView(事务开始时)
  5. 事务B: INSERT INTO users ... COMMIT;
  6. 事务A: SELECT * FROM users;  -- 使用同一个ReadView,看不到B的插入
复制代码
实现机制:事务开始时生成ReadView并复用
MVCC的Undo Log实现
  1. -- 更新操作示例
  2. UPDATE users SET name = 'Bob' WHERE id = 1;
  3. -- MVCC执行流程:
  4. 1. 将当前行拷贝到Undo Log(保存旧版本)
  5. 2. 修改当前行,更新DB_TRX_ID为当前事务ID
  6. 3. 设置DB_ROLL_PTR指向Undo Log中的旧版本
  7. -- 读操作:
  8. 通过版本链和ReadView找到合适的可见版本
复制代码
MVCC的优缺点

优点:


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


  • 存储开销:需要存储多个版本的数据
  • 清理机制:需要定期清理过期版本(purge操作)
  • 写冲突:写操作之间仍可能冲突
MVCC与锁的配合
  1. -- 实际是MVCC+锁的混合机制
  2. SELECT * FROM users WHERE id = 1;  -- MVCC,无锁快照读
  3. SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 当前读,加锁
  4. UPDATE users SET name = '...' WHERE id = 1;  -- 当前读,加锁
复制代码
四、Spring事务传播属性详解

传播属性是什么?

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

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

行为:如果当前存在事务,则加入该事务;如果当前没有事务,则新建一个事务。
使用场景:大多数业务方法,确保操作在事务中执行。
  1. @Service
  2. public class OrderService {
  3.     @Transactional(propagation = Propagation.REQUIRED)
  4.     public void placeOrder(Order order) {
  5.         // 如果调用方有事务,加入;否则新建事务
  6.         orderDao.save(order);
  7.         inventoryService.deductStock(order);  // 也会在同一个事务中
  8.     }
  9. }
复制代码
2. REQUIRES_NEW - 新建事务

行为:创建一个新的事务,如果当前存在事务,则挂起当前事务。
使用场景:日志记录、审计操作等,需要独立提交,不受主事务影响。
  1. @Service
  2. public class AuditService {
  3.     @Transactional(propagation = Propagation.REQUIRES_NEW)
  4.     public void logOperation(String action) {
  5.         // 独立事务,即使主事务回滚,日志仍然保留
  6.         auditDao.save(new AuditLog(action));
  7.     }
  8. }
  9. // 调用示例
  10. @Transactional
  11. public void businessMethod() {
  12.     try {
  13.         // 业务操作
  14.         orderService.process();
  15.     } catch (Exception e) {
  16.         // 即使业务回滚,审计日志仍然提交
  17.         auditService.logOperation("业务异常: " + e.getMessage());
  18.         throw e;
  19.     }
  20. }
复制代码
3. NESTED - 嵌套事务

行为:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则新建事务。
关键特性:使用保存点(Savepoint) 机制,可以部分回滚。
  1. @Service
  2. public class ComplexService {
  3.     @Transactional(propagation = Propagation.NESTED)
  4.     public void updateUserProfile(User user, Profile profile) {
  5.         userDao.update(user);
  6.         profileDao.update(profile);
  7.         // 如果失败,只回滚这个方法,不影响外部事务
  8.     }
  9. }
  10. // 外层事务
  11. @Transactional
  12. public void completeUserRegistration(User user) {
  13.     userService.register(user);           // 主事务的一部分
  14.     complexService.updateUserProfile(...); // 嵌套事务,可独立回滚
  15.     notificationService.sendWelcome(user); // 主事务的一部分
  16. }
复制代码
4. SUPPORTS - 支持事务

行为:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
使用场景:查询方法,可以接受事务但不强求。
  1. @Service
  2. public class QueryService {
  3.     @Transactional(propagation = Propagation.SUPPORTS)
  4.     public User getUserById(Long id) {
  5.         // 有事务就加入,没有也无妨
  6.         return userDao.findById(id);
  7.     }
  8. }
复制代码
5. NOT_SUPPORTED - 不支持事务

行为:以非事务方式执行,如果当前存在事务,则挂起该事务。
使用场景:不需要事务支持的操作,如复杂计算、调用外部API。
  1. @Service
  2. public class ReportService {
  3.     @Transactional(propagation = Propagation.NOT_SUPPORTED)
  4.     public Report generateMonthlyReport() {
  5.         // 复杂统计计算,不需要事务
  6.         // 也不会受外部事务影响
  7.         return reportDao.complexQuery();
  8.     }
  9. }
复制代码
6. NEVER - 绝不使用事务

行为:以非事务方式执行,如果当前存在事务,则抛出异常。
使用场景:确保方法不在事务上下文中执行。
  1. @Service
  2. public class UtilityService {
  3.     @Transactional(propagation = Propagation.NEVER)
  4.     public void clearCache() {
  5.         // 缓存清理,绝对不能有事务
  6.         // 如果调用方有事务,会抛出异常
  7.         cacheManager.clearAll();
  8.     }
  9. }
复制代码
7. MANDATORY - 强制存在事务

行为:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
使用场景:必须在事务中执行的关键操作。
  1. @Service
  2. public class PaymentService {
  3.     @Transactional(propagation = Propagation.MANDATORY)
  4.     public void processPayment(Payment payment) {
  5.         // 必须在事务中执行,否则报错
  6.         // 确保数据一致性
  7.         accountDao.deduct(payment.getAmount());
  8.         paymentDao.save(payment);
  9.     }
  10. }
复制代码
传播属性组合使用策略

分层架构中的传播属性设计:
  1. // Controller层 - 不管理事务
  2. @RestController
  3. public class UserController {
  4.     @Autowired
  5.     private UserFacade userFacade;
  6. }
  7. // Facade/Service层 - 开启事务
  8. @Service
  9. public class UserFacade {
  10.     @Transactional(propagation = Propagation.REQUIRED)
  11.     public UserDTO registerUser(UserRequest request) {
  12.         // 协调多个Service,统一事务管理
  13.         User user = userService.createUser(request);
  14.         profileService.initProfile(user.getId());
  15.         auditService.logRegistration(user);
  16.         return convertToDTO(user);
  17.     }
  18. }
  19. // 业务Service层 - 根据需要使用不同传播属性
  20. @Service
  21. public class UserService {
  22.     // 主业务方法,使用默认REQUIRED
  23.     @Transactional
  24.     public User createUser(UserRequest request) {
  25.         return userRepository.save(convertToEntity(request));
  26.     }
  27. }
  28. @Service  
  29. public class AuditService {
  30.     // 审计日志,独立事务
  31.     @Transactional(propagation = Propagation.REQUIRES_NEW)
  32.     public void logRegistration(User user) {
  33.         auditRepository.save(new AuditLog("USER_REGISTER", user.getId()));
  34.     }
  35. }
  36. @Service
  37. public class ProfileService {
  38.     // 嵌套事务,可部分回滚
  39.     @Transactional(propagation = Propagation.NESTED)
  40.     public void initProfile(Long userId) {
  41.         profileRepository.createDefaultProfile(userId);
  42.     }
  43. }
复制代码
常见陷阱与解决方案:
  1. // 陷阱1:自调用导致@Transactional失效
  2. @Service
  3. public class OrderService {
  4.     public void processOrder(Order order) {
  5.         // 自调用,@Transactional失效!
  6.         this.updateInventory(order);  
  7.     }
  8.    
  9.     @Transactional
  10.     public void updateInventory(Order order) {
  11.         // 不会开启事务
  12.     }
  13. }
  14. // 解决方案1:使用代理对象
  15. @Service
  16. public class OrderService {
  17.     @Autowired
  18.     private OrderService selfProxy;  // 注入自身代理
  19.    
  20.     public void processOrder(Order order) {
  21.         selfProxy.updateInventory(order);  // 通过代理调用
  22.     }
  23. }
  24. // 解决方案2:使用AopContext
  25. @EnableAspectJAutoProxy(exposeProxy = true)
  26. public class Application {
  27.     // 配置类启用代理暴露
  28. }
  29. public void processOrder(Order order) {
  30.     OrderService proxy = (OrderService) AopContext.currentProxy();
  31.     proxy.updateInventory(order);
  32. }
  33. // 陷阱2:异常被捕获不抛出
  34. @Transactional
  35. public void saveWithRollback() {
  36.     try {
  37.         userRepository.save(user);
  38.         throw new RuntimeException();  // 触发回滚的异常
  39.     } catch (Exception e) {
  40.         // 异常被捕获,事务不会回滚!
  41.         log.error("Error occurred", e);
  42.     }
  43. }
  44. // 解决方案:手动回滚或重新抛出
  45. @Transactional
  46. public void saveWithRollback() {
  47.     try {
  48.         userRepository.save(user);
  49.         throw new RuntimeException();
  50.     } catch (Exception e) {
  51.         TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  52.         throw e;  // 或者重新抛出
  53.     }
  54. }
复制代码
隔离级别与传播属性的组合实践
  1. @Service
  2. public class FinancialService {
  3.    
  4.     // 资金转移:最高隔离级别,需要事务
  5.     @Transactional(
  6.         isolation = Isolation.SERIALIZABLE,
  7.         propagation = Propagation.REQUIRED,
  8.         timeout = 30,
  9.         rollbackFor = {BusinessException.class, RuntimeException.class}
  10.     )
  11.     public void transferFunds(TransferRequest request) {
  12.         // 1. 检查账户(需要一致性读)
  13.         Account from = accountService.getAccount(request.getFromAccountId());
  14.         Account to = accountService.getAccount(request.getToAccountId());
  15.         
  16.         // 2. 扣款(强一致性要求)
  17.         accountService.deduct(from, request.getAmount());
  18.         
  19.         // 3. 存款
  20.         accountService.deposit(to, request.getAmount());
  21.         
  22.         // 4. 记录交易日志(独立事务)
  23.         auditService.logTransaction(request);
  24.         
  25.         // 5. 发送通知(非事务,不影响主流程)
  26.         notificationService.sendTransferNotification(request);
  27.     }
  28. }
  29. @Service
  30. public class AccountService {
  31.     @Transactional(
  32.         isolation = Isolation.REPEATABLE_READ,
  33.         propagation = Propagation.MANDATORY  // 必须在外层事务中调用
  34.     )
  35.     public void deduct(Account account, BigDecimal amount) {
  36.         // 扣款操作
  37.     }
  38. }
  39. @Service
  40. public class AuditService {
  41.     @Transactional(
  42.         propagation = Propagation.REQUIRES_NEW,  // 独立事务
  43.         isolation = Isolation.READ_COMMITTED     // 日志不需要强一致性
  44.     )
  45.     public void logTransaction(TransferRequest request) {
  46.         // 审计日志
  47.     }
  48. }
  49. @Service
  50. public class NotificationService {
  51.     @Transactional(propagation = Propagation.NOT_SUPPORTED)
  52.     public void sendTransferNotification(TransferRequest request) {
  53.         // 调用外部通知服务,不需要事务
  54.     }
  55. }
复制代码
五、最佳实践总结

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. 监控与调优:
  1. -- 监控长事务
  2. SELECT * FROM information_schema.innodb_trx
  3. WHERE TIME_TO_SEC(timediff(now(), trx_started)) > 60;
  4. -- 查看锁等待
  5. SELECT * FROM information_schema.innodb_lock_waits;
  6. -- 监控undo log
  7. SHOW VARIABLES LIKE 'innodb_undo%';
复制代码
理解这些核心概念和技术细节,可以帮助你设计出更合理、高性能的数据库应用架构,有效平衡一致性、并发性和性能之间的关系。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册