乐观锁与悲观锁

乐观锁和悲观锁是两种常见的并发控制机制,主要用于解决多线程或分布式系统中数据竞争的问题。它们在实现方式、适用场景和性能特点上有显著差异,以下是详细对比和实际应用示例:


一、悲观锁(Pessimistic Locking)

核心思想

  • 假设并发冲突一定会发生,因此在操作数据前直接加锁,确保操作期间数据不被其他事务修改。

  • 实现方式:通过数据库的锁机制(如行锁、表锁)或代码层的互斥锁(如Mutex)实现。

典型应用场景

  1. 强一致性要求高:如银行转账、库存扣减。

  2. 写操作频繁:并发环境下频繁修改同一数据的场景。

  3. 长事务:需要长时间占用资源的操作。

实现示例(MySQL)

sql
 
复制
 
下载
-- 开启事务,使用SELECT ... FOR UPDATE锁定记录
BEGIN;
SELECT * FROM products WHERE id = 123 FOR UPDATE;
-- 执行库存扣减
UPDATE products SET stock = stock - 1 WHERE id = 123;
COMMIT;
  • 说明FOR UPDATE会对查询结果加排他锁,其他事务无法修改该记录,直到当前事务提交。

优点

  • 保证操作的强一致性,避免脏读、幻读。

  • 适合写操作频繁的场景。

缺点

  • 加锁会导致性能下降(高并发时可能形成锁竞争)。

  • 可能引发死锁问题(需设置合理的锁超时时间)。


二、乐观锁(Optimistic Locking)

核心思想

  • 假设并发冲突很少发生,允许无锁操作,仅在提交时检查数据是否被修改。

  • 实现方式:通过版本号(version)或时间戳(timestamp)标记数据版本。

典型应用场景

  1. 读多写少:如用户信息更新、文章点赞。

  2. 高并发轻量级操作:如秒杀、抢购。

  3. 分布式系统:避免跨服务锁的复杂性。

实现示例(MySQL)

  1. 基于版本号的乐观锁

    sql
     
    复制
     
    下载
    -- 查询当前数据和版本号
    SELECT stock, version FROM products WHERE id = 123;
    
    -- 更新时校验版本号
    UPDATE products 
    SET stock = stock - 1, version = version + 1 
    WHERE id = 123 AND version = 1; -- 假设查询到的version是1
    • 如果返回的影响行数为0,说明版本号已变更,需重试或提示失败。

  2. 基于条件的乐观锁(无需版本字段):

    sql
     
    复制
     
    下载
    UPDATE products 
    SET stock = stock - 1 
    WHERE id = 123 AND stock > 0; -- 直接通过业务条件判断

优点

  • 无锁设计,性能高(适合高并发场景)。

  • 避免死锁问题。

缺点

  • 冲突时需要重试或回滚(需代码层处理重试逻辑)。

  • 不保证绝对一致性(可能出现短暂的数据不一致)。


三、两者的关键区别

对比维度悲观锁乐观锁
加锁时机 操作前加锁 提交时检查
性能开销 高(频繁加锁/释放) 低(无锁)
适用场景 写多读少、强一致性 读多写少、高并发
实现复杂度 简单(依赖数据库锁) 复杂(需处理版本号、重试逻辑)
数据一致性 强一致性 最终一致性
典型应用 银行转账、库存扣减 秒杀、用户信息更新

四、实际场景中的选择建议

  1. 悲观锁适用场景

    • 金融交易:如转账操作必须保证金额绝对正确。

    • 库存管理:当库存量极少且不允许超卖时(如限量商品)。

    • 示例代码(PHP中的悲观锁):

      php
       
      复制
       
      下载
      $pdo->beginTransaction();
      $stmt = $pdo->prepare("SELECT * FROM products WHERE id = ? FOR UPDATE");
      $stmt->execute([$productId]);
      // 检查库存并更新...
      $pdo->commit();
  2. 乐观锁适用场景

    • 秒杀系统:高并发下扣减库存(结合Redis + Lua脚本更优)。

    • 用户信息更新:如修改个人资料(冲突概率低)。

    • 示例代码(PHP中的乐观锁重试):

      php
       
      复制
       
      下载
      $maxRetries = 3;
      for ($i = 0; $i < $maxRetries; $i++) {
          $row = $pdo->query("SELECT stock, version FROM products WHERE id = 123")->fetch();
          if ($row['stock'] <= 0) break;
          $result = $pdo->exec("UPDATE products SET stock = stock - 1, version = version + 1 
                               WHERE id = 123 AND version = {$row['version']}");
          if ($result > 0) {
              // 更新成功,退出循环
              break;
          }
      }

五、总结

  • 悲观锁:以“防患于未然”为核心,适合写操作频繁、强一致性要求的场景,但性能开销大。

  • 乐观锁:以“事后补救”为核心,适合高并发、读多写少的场景,需配合重试机制。

实际开发中

  • 在秒杀系统中,通常结合乐观锁(Redis原子操作) + 消息队列实现高性能和高一致性。

  • 在传统业务系统(如订单支付)中,可能选择悲观锁保证强事务。

  •  
 
 
posted @ 2025-05-23 17:30  17601621550  阅读(54)  评论(0)    收藏  举报