代码技巧之事务与锁

为了方便控制事务的起始,这里使用了编程式事务,保证事务在锁里开始和结束,避免因为事务导致锁失效。

这句话的核心是 “让事务的生命周期与锁的范围严格一致”,避免因事务和锁的范围不匹配导致 “锁看似加了,却没起到保护数据一致性的作用”(即 “锁失效”)。

先明确两个前提概念:
编程式事务:手动控制事务的开始(begin)、提交(commit)、回滚(rollback),比如用 TransactionTemplate 或 PlatformTransactionManager 手动调用方法。
声明式事务:通过注解(如 @Transactional)由框架(如 Spring)自动控制事务,开发者不用手动写开始 / 提交代码。
为什么事务可能导致锁失效?
假设用 声明式事务 + 锁(比如 synchronized),但事务和锁的范围没对齐,就可能出现 “锁失效”。举个具体场景:
场景:秒杀系统中防止超卖(用户重复下单)
业务逻辑:检查库存→扣减库存→创建订单,需要保证同一商品的并发操作互斥(用锁),且操作要么全成功要么全失败(用事务)。
错误示例:声明式事务 + 锁范围外的事务

// 声明式事务:由@Transactional自动控制,事务范围是整个方法

@Transactional
public void seckill(Long productId) {
    // 锁范围:仅包裹核心逻辑
    synchronized (productId.toString().intern()) { 
        // 1. 查库存
        Product product = productMapper.selectById(productId);
        // 2. 扣库存
        if (product.getStock() > 0) {
            product.setStock(product.getStock() - 1);
            productMapper.updateById(product);
            // 3. 创建订单
            orderMapper.insert(new Order(productId));
        }
    }
    // 注意:声明式事务的 commit 会在方法结束时自动执行,此时锁已经释放了!
}

问题出在哪里?
声明式事务的生命周期是 “方法开始时自动开启事务,方法结束时自动提交”,而锁的范围是 synchronized 块内部。这会导致:
线程 A 进入 synchronized 块,执行完扣库存和创建订单后,退出同步块(释放锁)。
但此时事务还未提交(因为方法还没结束,声明式事务的 commit 在方法末尾)。
线程 B 此时获取到锁,查询库存时,由于线程 A 的事务未提交(数据库隔离级别导致可能读不到未提交的修改),可能查到 “旧库存”,导致重复扣减(超卖)。
这种情况下,锁看似加了,但因为事务提交在锁释放之后,其他线程在锁释放后、事务提交前的窗口内,可能读到不一致的数据,锁的保护作用失效(即 “锁失效”)。
为什么编程式事务能避免这个问题?
编程式事务可以手动控制事务的开始和结束,确保 “事务在锁内开始,在锁释放前提交”,让事务和锁的范围严格对齐。
正确示例:编程式事务 + 锁范围内的事务
// 注入事务管理器

@Autowired
private PlatformTransactionManager transactionManager;

public void seckill(Long productId) {
    // 锁范围:包裹整个事务生命周期
    synchronized (productId.toString().intern()) { 
        // 1. 手动开启事务
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        TransactionStatus status = transactionManager.getTransaction(def);
        
        try {
            // 2. 核心业务逻辑(查库存→扣库存→创建订单)
            Product product = productMapper.selectById(productId);
            if (product.getStock() > 0) {
                product.setStock(product.getStock() - 1);
                productMapper.updateById(product);
                orderMapper.insert(new Order(productId));
            }
            // 3. 手动提交事务(在锁内提交)
            transactionManager.commit(status);
        } catch (Exception e) {
            // 异常时回滚事务(也在锁内)
            transactionManager.rollback(status);
        }
    }
    // 锁释放时,事务已经提交/回滚完成
}

关键差异:
编程式事务让事务的 “开始→执行→提交 / 回滚” 全在 synchronized 块内部:
线程 A 获取锁后,先开启事务,执行完业务逻辑后,在释放锁前提交事务。
线程 B 只有在获取锁后,才能执行查询,此时看到的是线程 A 已提交的最新数据(库存已正确扣减),避免了超卖。
事务和锁的范围完全重叠,锁的保护作用有效。
总结
“使用编程式事务,保证事务在锁里开始和结束” 的本质是:
通过手动控制事务生命周期,确保 “事务的提交 / 回滚发生在锁释放之前”,避免因 “锁已释放但事务未提交” 导致的并发数据不一致(即 “锁失效”)。

声明式事务的自动控制虽然方便,但难以精确控制事务与锁的范围对齐;而编程式事务的手动控制,能强制让事务在锁的保护范围内完成,确保并发安全。

posted @ 2025-10-24 14:12  kisshappyboy  阅读(5)  评论(0)    收藏  举报