面试官问redis分布式锁,如何设计才能让他满意?

前言

对于分布式锁的问题我也查过很多资料,感觉很多方式实现的并不完善,或者看着云里雾里的,不知所以然,于是就整理了这篇文章,希望对您有用,有写的不对的地方,欢迎留言指正。

首先咱们来聊聊什么是分布式锁,到底解决了什么问题?直接看代码

 $stock = $this->getStockFromDb();//查询剩余库存
 if ($stock>0){
      $this->ReduceStockInDb(); // 在数据库中进行减库存操作
      echo "successful";
 }else{
    echo  "库存不足";
 }

很简单的一个场景,用户下单,咱们查询商品库存够不够,不够的话直接返回库存不足类似的错误信息,如果库存够的话直接在数据库中库存-1,然后返回成功,在业务逻辑上这段代码是没有什么问题的。

但是,这段代码是存在严重的问题的。

如果库存只剩 1,并且在并发比较高的情况下,比如两个请求同时执行了这段代码,同时查到库存为 1,然后顺利成章的都去数据库执行 stock-1 的操作,这样库存就会变成-1,然后就会引发超卖的现象,刚才说的是两个请求同时执行,如果同时几千个请求打过来,可见造成的损失是非常大的。于是呢有些聪明人就想了个办法,办法如下。

大家都知道 redis 有个 setnx 命令,不知道的话也没关系,我已经帮你查过了


setnx.png

我们把上面的代码优化一下

version-1

 $lock_key="lock_key";
 $res = $redis->setNx($lock_key, 1);
 if (!$res){
      return "error_code";
 }

 $stock = $this->getStockFromDb();//查询剩余库存
 if ($stock>0){
      $this->ReduceStockInDb(); // 在数据库中进行减库存操作
      echo "successful";
 }else{
    echo  "库存不足";
 }

$redis->delete($lock_key);
  • 第一次请求进来会去 setNx,当然结果是返回 true,因为 lock_key 不存在,然后下面业务逻辑正常进行,任务执行完了之后把lock_key删除掉,这样下一次请求进来重复上述逻辑

  • 第二次请求进来同样会去执行 setNx,结果返回 false,因为lock_key已经存在,然后直接返回错误信息(你双11抢购秒杀产品的时候给你返回的系统繁忙就是这么来的),不执行库存减 1 的操作

  • 有的同学可能有疑惑,咱们不是说高并发的情况下么?要是两个请求同时 setNx 的话获取的结果不都是 true 了,同样会同时去执行业务逻辑,问题不是一样没解决么?但是大家要明白 redis 是单线程的,具备原子性,不同的请求执行 setnx 是顺序执行的,所以这个是不用担心的。

看似问题解决了,其实并不然。

我们这里伪代码写的简单,查询一下库存,然后减1操作而已,但是真实的生产环境中的情况是非常复杂的,在一些极端情况下,程序很可能会报错,崩溃,如果第一次执行加锁了之后程序报错了,那这个锁永远存在,接下来的请求永远也请求不进来了,所以咱们继续优化

version-2

 try{ //新加入try catch处理,这样程序万一报错会把锁删除掉
   $lock_key="lock_key";
   $expire_time = 5;//新加入过期时间,这样锁不会一直占有
   $res = $redis->setNx($lock_key, 1, $expire_time);
   if (!$res){
        return "error_code";
   }

   $stock = $this->getStockFromDb();//查询剩余库存
   if ($stock>0){
        $this->ReduceStockInDb(); // 在数据库中进行减库存操作
        echo "successful";
   }else{
      echo  "库存不足";
   }
 }finally {
    $redis->delete($lock_key);
}
  • 在setnx的时候给加上过期时间,这样至少不会让锁一直存在成为死锁
  • 做try catch处理,万一程序抛出异常把锁删掉,也是为了解决死锁问题

这次是把死锁问题解决了,但是问题还是存在,大家可以先想一想还存在什么问题再接着往下看。

存在的问题如下

  • 我们的过期时间是5秒钟,万一这个请求执行了6秒钟怎么办?超出的那一秒,跟没有加锁有什么区别?其实不仅仅如此,还有一个更严重的问题存在。比如第二个请求也是执行6秒,那么在第二个请求在超出的那1秒才进来的时候,第一个请求执行完了,当然会删除第二个请求加的锁,如果一直并发都很大的话,锁跟没有加没什么区别。
  • 针对上述问题,最直接的办法是加长过期时间,但是这个不是解决问题的最终办法。把时间设置过长也会产生新的问题,比如各种原因机器崩溃了,需要重启,然后你把锁设置的时间是1年,同时也没有delete掉,难道机器重启了再等一年?另外这样设置固定值的解决方案在计算机当中是不允许的,曾经的“千年虫”问题就是类似的原因导致的

  • 在加超时时间的时候一定要注意一定是一次性加上,保证其原子性,不要先setnx之后,再设置expire_time,这样的话万一在setnx之后那一个瞬间系统挂了,这个锁依然会成为一个永久的死锁

  • 其实上述问题的主要原因在于,请求1会删掉请求2的锁,所以说锁需要保证唯一性。

咱们接着优化

version-3

 try{ //新加入try catch处理,这样程序万一报错会把锁删除掉
   $lock_key="lock_key";
   $expire_time = 5;//新加入过期时间,这样锁不会一直占有
   
   $client_id = session_create_id();  //对每个请求生成唯一性的id
   $res = $redis->setNx($lock_key, $client_id, $expire_time);
   if (!$res){
        return "error_code";
   }

   $stock = $this->getStockFromDb();//查询剩余库存
   if ($stock>0){
        $this->ReduceStockInDb(); // 在数据库中进行减库存操作
        echo "successful";
   }else{
      echo  "库存不足";
   }
 }finally {
   if ($redis->get($lock_key) == $client_id){  //在这里加一个判断,保证每次删除的锁是当次请求加的锁,这样避免误删了别的请求加的锁
      $redis->delete($lock_key);
   }
   
}
  • 我们在每个请求生成了唯一client_id,并且把该值写入了lock_key中

  • 在最后删除锁的时候会先判断这个lock_key是否是该请求生成的,如果不是的话则不会删除

但是上面方案还有问题,我们看最后 redis是先进行了get操作判断,然后再删除,是两步操作,并没有保证其原子性,redis的多步操作可以用lua脚本来保证原子性,其实看到lua也不需要感觉太陌生,他就是一种语言而已,在这里的作用是把多个redis操作打包成一个命令去执行,保证了原子性而已

version-4

 try{ //新加入try catch处理,这样程序万一报错会把锁删除掉
   $lock_key="lock_key";
   $expire_time = 5;//新加入过期时间,这样锁不会一直占有
   
   $client_id = session_create_id();  //对每个请求生成唯一性的id
   $res = $redis->setNx($lock_key, $client_id, $expire_time);
   if (!$res){
        return "error_code";
   }

   $stock = $this->getStockFromDb();//查询剩余库存
   if ($stock>0){
        $this->ReduceStockInDb(); // 在数据库中进行减库存操作
        echo "successful";
   }else{
      echo  "库存不足";
   }
 }finally {
    $script = '  //此处用lua脚本执行是为了get对比之后再delete的两步操作的原子性
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';
        return $instance->eval($script, [$lock_key, $client_id], 1);
   
}

这样封装之后,分布式锁应该就比较完善了。当然我们还可以进一步的优化一下用户体验

  • 现在比如一个请求进来之后,如果请求被锁住,会立即返回给用户请求失败,请重新尝试,我们可以适当的延长一点这个时间,不要立即返回给用户请求失败,这样体验会更好
  • 具体方式为用户请求进来如果遇到了锁,可以适当的等待一些时间之后重试,重试的时候如果锁释放了,则这次请求就可以成功

version-5

$retry_times = 3; //重试次数
$usleep_times = 5000;//重试间隔时间

  try{ //新加入try catch处理,这样程序万一报错会把锁删除掉
   
   
    $lock_key="lock_key";
    $expire_time = 5;//新加入过期时间,这样锁不会一直占有
    while($retry_times > 0){
      $client_id = session_create_id();  //对每个请求生成唯一性的id
      $res = $redis->setNx($lock_key, $client_id, $expire_time);
      if ($res){
          break;
      }
      echo "尝试重新获取锁";
      $retry_times--;
      usleep($usleep_times);
   }
   if (!$res){  //重试三次之后都没有获取到锁则给用户返回错误信息
        return "error_code";
   }
   $stock = $this->getStockFromDb();//查询剩余库存
   if ($stock>0){
        $this->ReduceStockInDb(); // 在数据库中进行减库存操作
        echo "successful";
   }else{
      echo  "库存不足";
   }
 }finally {
    $script = '  //此处用lua脚本执行是为了get对比之后再delete的两步操作的原子性
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';
        return $instance->eval($script, [$lock_key, $client_id], 1);
   
}

当然上面的分布式锁还是不够完善的,比如redis主从同步延迟,就会产生问题,像java中redission实现的思想是非常好的,大家感兴趣可以看看源码,今天就聊到这里,感兴趣的朋友可以留言大家一起讨论

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容