数据库并发控制:悲观锁与乐观锁

一、为什么需要锁

在多用户并发操作同一条数据的场景下,如果不做控制,可能出现数据被重复修改、状态流转异常等问题。例如两个用户同时对同一条订单执行"开始处理"操作,可能导致订单被重复流转、生成重复的下游单据。

锁的核心作用是:保证同一份数据在同一时刻只能被一个事务修改

二、悲观锁(Pessimistic Lock)

1. 核心思想

悲观锁假设并发冲突一定会发生,因此在操作数据前先加锁,其他事务必须等待当前事务完成后才能操作。

2. 实现方式

通过 SQL 的 FOR UPDATE 子句实现行级锁:

SELECT * FROM orders WHERE id = 123 FOR UPDATE;

在 MyBatis-Plus 中的写法:

Order order = baseMapper.selectOne(
    Wrappers.<Order>lambdaQuery()
        .eq(Order::getId, orderId)
        .last("FOR UPDATE"));
3. 锁的释放时机

FOR UPDATE 加的行锁在事务提交(commit)或回滚(rollback)时自动释放,而非方法返回时释放。因此必须在事务内使用,否则每次查询自动提交,锁会立即释放,等于未加锁。

@Transactional(rollbackFor = Exception.class)
public void processOrder(Long orderId) {
    // FOR UPDATE 在此事务内生效,事务提交后释放
    Order order = baseMapper.selectOne(...last("FOR UPDATE"));
    // 业务逻辑...
}   // 方法结束,Spring 提交事务,锁释放
4. 执行流程
事务A                           事务B
─────────────────────────────────────────────
SELECT ... FOR UPDATE  (加锁成功)
执行业务逻辑...                SELECT ... FOR UPDATE  (阻塞等待)
UPDATE ...
COMMIT  (释放锁)               ← 获取锁,继续执行
                               但数据已被修改,校验失败
5. 优缺点
优点缺点
数据一致性强,一定能防住并发加锁期间其他事务阻塞等待,并发性能低
实现简单直观业务逻辑耗时长会导致长时间等待
适合写多读少场景处理不当可能引发死锁

三、乐观锁(Optimistic Lock)

1. 核心思想

乐观锁假设并发冲突概率较低,不主动加锁,而是在更新时校验数据是否已被其他事务修改。若已被修改,则本次更新失败,由调用方决定重试或提示用户。

2. 实现方式一:WHERE 条件携带旧值

更新时将查询时的旧值放入 WHERE 条件,若数据已被修改,WHERE 不匹配,影响行数为 0:

Order exist = baseMapper.selectById(orderId);  // 查出旧状态

Order update = new Order();
update.setStatus(OrderStatusEnum.PROCESSING.getCode());

int updated = baseMapper.update(update,
    Wrappers.<Order>lambdaUpdate()
        .eq(Order::getId, orderId)
        .eq(Order::getStatus, exist.getStatus()));  // 旧状态作为条件

if (updated <= 0) {
    throw new ServiceException("数据状态已变化,请刷新后重试");
}

生成的 SQL:

UPDATE orders 
SET status = 'PROCESSING' 
WHERE id = 123 AND status = 'PENDING';
3. 实现方式二:版本号字段(MyBatis-Plus @Version)

在实体类中添加 @Version 注解的版本号字段,MyBatis-Plus 自动在更新时校验并自增版本号:

public class Order {
    private Long id;
    private String status;
    @Version
    private Integer version;
}

更新时自动生成:

UPDATE orders SET status='PROCESSING', version=version+1 
WHERE id=123 AND version=1;
4. 执行流程
事务A                           事务B
─────────────────────────────────────────────
查出 status=PENDING
                                查出 status=PENDING
UPDATE WHERE status=PENDING
  → 成功,改为 PROCESSING
COMMIT
                                UPDATE WHERE status=PENDING
                                  → 不匹配,影响行数为 0
                                检测到冲突,抛异常
5. 优缺点
优点缺点
不加锁,不阻塞其他事务,并发性能好冲突时需手动处理(重试或报错),逻辑较复杂
不会死锁冲突频繁时不断重试,可能比悲观锁更慢
适合读多写少场景需要业务层判断更新结果

四、双重保险:悲观锁 + 乐观锁组合使用

在核心业务场景中,可同时使用两种锁,层层设防:

@Transactional(rollbackFor = Exception.class)
public Boolean processOrder(Long orderId) {
    // 第一层:悲观锁,保证同一时刻只有一个事务操作
    Order exist = baseMapper.selectOne(
        Wrappers.<Order>lambdaQuery()
            .eq(Order::getId, orderId)
            .last("FOR UPDATE"));
    if (exist == null) {
        throw new ServiceException("订单不存在");
    }

    Order update = new Order();
    update.setStatus(OrderStatusEnum.PROCESSING.getCode());

    // 第二层:乐观锁,作为兜底校验
    int updated = baseMapper.update(update,
        Wrappers.<Order>lambdaUpdate()
            .eq(Order::getId, orderId)
            .eq(Order::getStatus, exist.getStatus()));
    if (updated <= 0) {
        throw new ServiceException("订单状态已变化,请刷新后重试");
    }
    return true;
}

这种写法属于防御性编程:即使悲观锁因事务未生效等原因失效,乐观锁仍能保证数据一致性。

五、选型建议

场景特征推荐方案
状态流转、审批等写操作多、冲突概率高悲观锁
库存扣减、计数器等读多写少、偶发冲突乐观锁 + 重试
支付、转账等核心业务悲观锁 + 乐观锁双重保险
并发量极大、冲突极少乐观锁

六、常见坑点

  1. 悲观锁未在事务内使用FOR UPDATE 必须在事务中才有效,否则锁立即释放
  2. 事务内调用外部接口:长耗时操作会导致锁长时间持有,阻塞其他请求
  3. 乐观锁未判断影响行数:更新后必须检查 updated 是否为 0,否则无法感知冲突
  4. 分隔符冲突:复合键拼接时若字段值包含分隔符,会导致误判

七、小结

悲观锁与乐观锁是数据库并发控制的两种基础手段,并非互斥关系。理解两者的原理和适用场景,根据业务特点合理选择,是保障数据一致性的关键。在核心业务中组合使用两种锁,是工程实践中常见的防御性做法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值