【面试题】数据库事务隔离与传播属性是什么?
数据库事务隔离与MVCC深度剖析
一、事务隔离问题详解
1. 脏读(Dirty Read)
定义:一个事务读取了另一个未提交事务修改的数据。
核心问题:读到了"临时"的、可能被回滚的数据,破坏了数据一致性。
场景示例:
-- 事务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)
定义:同一事务内,多次读取同一数据行,结果不一致(被其他已提交事务修改)。
核心问题:事务内的读一致性被破坏。
场景示例:
-- 事务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)
定义:同一事务内,多次执行相同查询,返回的行数不同(被其他已提交事务插入/删除)。
核心问题:影响范围查询的一致性。
场景示例:
-- 事务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解决了大部分幻读问题
各数据库默认级别:
-- MySQL (默认: REPEATABLE READ)
SELECT @@transaction_isolation; -- REPEATABLE-READ
-- PostgreSQL (默认: READ COMMITTED)
SHOW transaction_isolation; -- read committed
-- Oracle (默认: READ COMMITTED)
-- SQL Server (默认: READ COMMITTED)
设置隔离级别示例:
-- 会话级别设置
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. 版本链机制
-- 每行数据隐藏的系统字段:
-- 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: 活跃事务中最小IDlow_limit_id: 下一个将要分配的事务IDcreator_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(读已提交)
-- 每次查询都生成新的ReadView
-- 只能看到已提交的数据
事务A: SELECT * FROM users; -- 生成ReadView1
事务B: INSERT INTO users ... COMMIT; -- 已提交
事务A: SELECT * FROM users; -- 生成ReadView2,能看到B的修改
实现机制:每次SELECT都重新生成ReadView
2. REPEATABLE READ(可重复读)
-- 事务第一次查询时生成ReadView,后续复用
-- 保证同一事务内看到的数据一致
事务A: BEGIN;
事务A: SELECT * FROM users; -- 生成ReadView(事务开始时)
事务B: INSERT INTO users ... COMMIT;
事务A: SELECT * FROM users; -- 使用同一个ReadView,看不到B的插入
实现机制:事务开始时生成ReadView并复用
MVCC的Undo Log实现
-- 更新操作示例
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的优缺点
优点:
- 读写不阻塞:读操作不会阻塞写操作,写操作不会阻塞读操作
- 高并发:避免锁竞争,提升并发性能
- 回滚高效:通过版本链快速回滚
缺点:
- 存储开销:需要存储多个版本的数据
- 清理机制:需要定期清理过期版本(purge操作)
- 写冲突:写操作之间仍可能冲突
MVCC与锁的配合
-- 实际是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(默认) - 需要事务
行为:如果当前存在事务,则加入该事务;如果当前没有事务,则新建一个事务。
使用场景:大多数业务方法,确保操作在事务中执行。
@Service
public class OrderService {
@Transactional(propagation = Propagation.REQUIRED)
public void placeOrder(Order order) {
// 如果调用方有事务,加入;否则新建事务
orderDao.save(order);
inventoryService.deductStock(order); // 也会在同一个事务中
}
}
2. REQUIRES_NEW - 新建事务
行为:创建一个新的事务,如果当前存在事务,则挂起当前事务。
使用场景:日志记录、审计操作等,需要独立提交,不受主事务影响。
@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) 机制,可以部分回滚。
@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 - 支持事务
行为:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
使用场景:查询方法,可以接受事务但不强求。
@Service
public class QueryService {
@Transactional(propagation = Propagation.SUPPORTS)
public User getUserById(Long id) {
// 有事务就加入,没有也无妨
return userDao.findById(id);
}
}
5. NOT_SUPPORTED - 不支持事务
行为:以非事务方式执行,如果当前存在事务,则挂起该事务。
使用场景:不需要事务支持的操作,如复杂计算、调用外部API。
@Service
public class ReportService {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public Report generateMonthlyReport() {
// 复杂统计计算,不需要事务
// 也不会受外部事务影响
return reportDao.complexQuery();
}
}
6. NEVER - 绝不使用事务
行为:以非事务方式执行,如果当前存在事务,则抛出异常。
使用场景:确保方法不在事务上下文中执行。
@Service
public class UtilityService {
@Transactional(propagation = Propagation.NEVER)
public void clearCache() {
// 缓存清理,绝对不能有事务
// 如果调用方有事务,会抛出异常
cacheManager.clearAll();
}
}
7. MANDATORY - 强制存在事务
行为:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
使用场景:必须在事务中执行的关键操作。
@Service
public class PaymentService {
@Transactional(propagation = Propagation.MANDATORY)
public void processPayment(Payment payment) {
// 必须在事务中执行,否则报错
// 确保数据一致性
accountDao.deduct(payment.getAmount());
paymentDao.save(payment);
}
}
传播属性组合使用策略
分层架构中的传播属性设计:
// 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);
}
}
常见陷阱与解决方案:
// 陷阱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; // 或者重新抛出
}
}
隔离级别与传播属性的组合实践
@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. 监控与调优:
-- 监控长事务
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%';
理解这些核心概念和技术细节,可以帮助你设计出更合理、高性能的数据库应用架构,有效平衡一致性、并发性和性能之间的关系。
❤️ 如果你喜欢这篇文章,请点赞支持! 👍 同时欢迎关注我的博客,获取更多精彩内容!
本文来自博客园,作者:佛祖让我来巡山,转载请注明原文链接:https://www.cnblogs.com/sun-10387834/p/19369929

浙公网安备 33010602011771号