悲观锁,乐观锁和redis分布式锁

​​悲观锁(Pessimistic Lock)​​

​​为什么叫 "悲观"?​​

因为它 ​​"悲观" 地认为并发操作一定会发生冲突​​,所以在操作数据之前,​​先加锁​​,确保其他事务无法修改这条数据,直到当前事务完成。

​​实现方式​​(数据库层面):

  • SELECT ... FOR UPDATE(MySQL)

  • SELECT ... WITH (UPDLOCK)(SQL Server)

  • 其他数据库的排他锁机制

​​特点​​:

  • ​​先锁再操作​​,防止并发修改

  • ​​适用于高并发写操作​​(如抢购、库存扣减)

  • ​​可能降低并发性能​​(锁会阻塞其他事务)

​​示例​​(ThinkPHP):

 
Db::startTrans(); // 开启事务 $info = $this->where(['id' => $id])->lock(true)->find(); // 加悲观锁 // ... 处理数据 ... Db::commit(); // 提交事务(释放锁)
 
 

​​乐观锁(Optimistic Lock)​​

​​为什么叫 "乐观"?​​

因为它 ​​"乐观" 地认为并发操作不会冲突​​,所以 ​​不加锁​​,而是在更新时检查数据是否被修改过(通常用版本号或时间戳)。

​​实现方式​​:

  • ​​版本号机制​​(version字段)

  • ​​时间戳机制​​(update_time字段)

  • ​​CAS(Compare-And-Swap)​​(如 Redis)

​​特点​​:

  • ​​不加锁,先操作再检查冲突​​

  • ​​适用于读多写少的场景​​

  • ​​冲突时需要重试或回滚​​

​​示例​​(ThinkPHP):

 
// 假设数据表有 `version` 字段 $data = $this->where(['id' => $id])->find(); $newData = ['name' => 'new_name', 'version' => $data['version'] + 1]; $result = $this->where(['id' => $id, 'version' => $data['version']])->save($newData); if (!$result) { // 更新失败,说明数据被其他人修改过,可以重试或报错 }
 
 

​​对比总结​​

特性

悲观锁(Pessimistic Lock)

乐观锁(Optimistic Lock)

​​加锁方式​​

先加锁再操作(FOR UPDATE

不加锁,更新时检查冲突

​​适用场景​​

高并发写操作(如抢购)

读多写少(如文章编辑)

​​性能影响​​

可能降低并发性能(锁阻塞)

无锁,冲突时才处理

​​实现方式​​

数据库锁机制

版本号、时间戳、CAS

​​冲突处理​​

其他事务会被阻塞

需要重试或回滚

​​如何选择?​​

  • ​​悲观锁​​:数据竞争激烈,必须保证数据一致性(如支付、库存扣减)。

  • ​​乐观锁​​:冲突概率低,希望提高并发性能(如文章编辑、评论更新)。

 

redis分布式锁:

这段代码的作用是 ​​使用 Redis 的 SETNX(SET if Not eXists)实现分布式锁​​,防止并发重复提交(如重复提现)。具体解析如下:


​​代码解析​​

 
if (!setLockNx($withdrawLockKey, 30)) { //提现锁,处理完手动解锁 json_exit_Base64(401, lang("处理中,请勿重复点击")); }
 
 
  1. ​​setLockNx($withdrawLockKey, 30)​​

    • 调用一个自定义函数(可能是封装了 Redis 的 SETNX命令)。

    • ​​$withdrawLockKey​​:锁的唯一标识(如用户ID + 业务类型,例如 withdraw:user123)。

    • ​​30​​:锁的自动过期时间(单位:秒),防止锁未释放导致死锁。

  2. ​​!setLockNx(...)​​

    • 如果获取锁失败(返回 false),说明锁已存在(即当前有其他请求正在处理相同业务)。

    • 直接返回错误提示,阻止重复操作。

  3. ​​json_exit_Base64(401, ...)​​

    • 返回 HTTP 401 状态码和 Base64 编码的 JSON 错误消息(可能是项目约定的通信格式)。

    • 提示用户:"处理中,请勿重复点击"。

  4. ​​注释说明​​

    • //提现锁,处理完手动解锁:提示开发者需要在业务逻辑完成后 ​​手动释放锁​​(如调用 delLock($withdrawLockKey)),否则锁会在 30 秒后自动过期。


​​实现原理(Redis 分布式锁)​​

  1. ​​加锁​​

    通过 Redis 的 SETNX命令实现原子性锁:

     
    SETNX $withdrawLockKey 1 # 如果 key 不存在则设置成功(返回1),否则失败(返回0) EXPIRE $withdrawLockKey 30 # 设置过期时间,避免程序崩溃导致死锁
     
     

    (注:高版本 Redis 支持 SET $withdrawLockKey 1 NX EX 30一步完成)

  2. ​​解锁​​

    业务处理完成后需手动删除 key:

     
    DEL $withdrawLockKey
     
     

​​适用场景​​

  • ​​提现/支付防重​​:防止用户多次点击导致重复扣款。

  • ​​秒杀/库存扣减​​:避免超卖问题。

  • ​​任何需要分布式环境下的互斥操作​​(如定时任务防并发执行)。


​​注意事项​​

  1. ​​锁的粒度​​

    • 锁的 key 需要足够具体(例如包含用户ID),避免不同用户互相阻塞。

  2. ​​锁的过期时间​​

    • 过期时间(如30秒)需大于业务处理时间,但不宜过长(否则阻塞其他请求)。

  3. ​​原子性问题​​

    • 确保 SETNX + EXPIRE是原子操作(如用 Redis 2.6.12+ 的 SET命令带 NX和 EX参数)。

  4. ​​手动解锁​​

    • 如果业务逻辑异常退出,需在 finally块中释放锁,或依赖自动过期。

        • 安全释放锁:
        • ​​解释 releaseLockNx($withdrawLockKey)的含义​​

          这段代码的作用是 ​​安全释放 Redis 分布式锁​​,而不仅仅是简单执行 DEL命令。它通常是一个 ​​自定义函数​​,封装了更严谨的解锁逻辑,确保 ​​只有锁的持有者才能释放锁​​,避免误删其他客户端的锁。


          ​​为什么不是 delLock($withdrawLockKey)?​​

          1. ​​直接 DEL的问题​​

            如果简单地用 DEL删除锁:

             
            DEL $withdrawLockKey
             
             
            • ​​风险​​:假设客户端 A 获取锁后因某些原因(如网络延迟、GC 停顿)导致锁过期,此时客户端 B 获取了锁。如果 A 仍然调用 DEL,会错误地删除 B 的锁,导致数据不一致。

          2. ​​安全释放锁的方案​​

            正确的做法是 ​​在释放锁时验证持有者身份​​,通常通过以下方式实现:

            • ​​方案 1:Lua 脚本(原子性操作)​​

              检查锁的值是否匹配当前客户端的唯一标识(如 UUID),匹配时才删除:

               
              if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end
               
               
            • ​​方案 2:Redis 事务​​

              使用 WATCHGETDEL组合(但不如 Lua 脚本简洁)。


          ​​releaseLockNx($withdrawLockKey)的典型实现​​

          该函数内部可能包含类似以下逻辑(伪代码):

           
          function releaseLockNx($key) { $redis = new Redis(); $redis->connect('127.0.0.1', 6379); // 假设锁的值是客户端的唯一标识(如 UUID) $clientToken = getCurrentClientToken(); // 获取当前客户端的标识 // 使用 Lua 脚本原子性释放锁 $script = <<<LUA if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end LUA; $result = $redis->eval($script, [$key, $clientToken], 1); return $result === 1; // 返回是否释放成功 }
           
           

          ​​与 delLock($withdrawLockKey)的区别​​

          函数名

          行为

          安全性

          delLock($key)

          直接删除 key,不检查持有者

          低(可能误删其他客户端锁)

          releaseLockNx($key)

          验证持有者身份后删除 key

          高(避免误删)


          ​​为什么项目中选择 releaseLockNx而非 delLock?​​

          1. ​​防御性编程​​

            防止因客户端超时、重试等异常场景导致锁被错误释放。

          2. ​​代码可读性​​

            releaseLockNx明确表达了“安全释放锁”的意图,而 delLock可能被误解为简单删除。

posted @ 2025-09-26 00:52  幽暗天琴  阅读(25)  评论(0)    收藏  举报