博主信息
小爱豆
博文
258
粉丝
0
评论
0
访问量
4930
积分:0
P豆:630

Redis实现分布式锁

2021年10月24日 20:34:03阅读数:11博客 / 小爱豆

分布式锁的实现方式有很多,本篇文章讲述一下使用Redis实现分布式锁。网上有很多使用Redis实现分布式锁的代码,但是这些代码或多或少都有问题。这篇文章会写一个实现,同时标明一些注意点。

场景

为了便于阐述,这里假设一个游戏场景,用户A有开山斧一把,价值500元宝,用户B有800元宝,想买A的开山斧,这些数据都存在Redis中。需要编写代码成功的实现该笔交易。

问题

Redis实现分布式锁,需要考虑如下问题:

持有锁的进程因为操作时间过长而导致锁被自动释放,但进程本身并不知晓这一点,甚至还可能会错误地释放掉了其他进程持有的锁。一个持有锁并打算执行长时间操作的进程已经崩溃,但其他想要获取锁的进程不知道哪个进程持有着锁,也无法检测出持有锁的进程已经崩溃,只能白白地浪费时间等待锁被释放。在一个进程持有的锁过期之后,其他多个进程同时尝试去获取锁,并且都获得了锁。

三个特性

实现一个最低保障的分布式锁,需要具备三个特性

安全属性(Safety property): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取。活性B(Liveness property B): 容错。 只要大部分Redis节点都活着,客户端就可以获取和释放锁.

命令

使用Redis实现分布式锁,一般使用SETNX或者SET命令,SETNX不能同时设置过期时间,如果使用的版本大于等于2.6.12,可以使用SET命令,可以使用这个命令原子性的实现SETNX和EXPIRE的功能,下面是两个命令的简介

SETNX

命令格式:SETNX key value

时间复杂度:O(1)

说明:将key设置值为value,如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做。SETNX是”SET if Not eXists”的简写。

返回值

1 如果key被设置了 如果key没有被设置

SET

命令格式:SET key value [EX seconds] [PX milliseconds] [NX|XX]

时间复杂度:O(1)

说明:将键key设定为指定的“字符串”值。如果 key 已经保存了一个值,那么这个操作会直接覆盖原来的值,并且忽略原始类型。当set命令执行成功之后,之前设置的过期时间都将失效。

选项

从2.6.12版本开始,redis为SET命令增加了一系列选项:

EX seconds – 设置键key的过期时间,单位时秒PX milliseconds – 设置键key的过期时间,单位是毫秒NX – 只有键key不存在的时候才会设置key的值XX – 只有键key存在的时候才会设置key的值

实现

此处使用SETNX实现,毕竟有的公司Redis版本可能较低,QQ号码出售使用SETNX可以实现,SET更加没有问题。

代码如下:

<?phpfunction uuid($prefix = ''){
   $chars = md5(uniqid(mt_rand(), true));
   $uuid  = substr($chars, , 8) . '-';
   $uuid .= substr($chars, 8, 4) . '-';
   $uuid .= substr($chars, 12, 4) . '-';
   $uuid .= substr($chars, 16, 4) . '-';
   $uuid .= substr($chars, 20, 12);
   $ret = $prefix . $uuid;
   return strtoupper($ret);}function acquireLock($redis,$lockName, $acquireTime = 10, $lockTime = 10){
   $lockKey    = 'lock:' + $lockName;
   $identifier = uuid('identify');
   $end        = time() + $acquireTime;
   while (time() < $end) {
       if ($redis->setnx($lockKey, $identifier)) {
           $redis->expire($lockKey, $lockTime);
           return $identifier;
       } elseif ($redis->ttl($lockKey) == -1) {
           $redis->expire($lockKey, $lockTime);
       }
       usleep(1000);
   }
   return false;}function process(){
   $redis      = new Redis();
   $lockName = 'market';
   //1.获取锁    $locked = acquireLock($redis,$lockName);
   if($locked === false){
       return false;
   }
   //2.进行交易    //判断A和B是否满足交易条件    //使用管道,对A和B进行操作
   //3.释放锁    $releaseRes = releaseLock($redis,$lockName,$locked);
   if($releaseRes === false){
       return false;
   }}function releaseLock($redis,$lockName,$identifier){
   $lockKey    = 'lock:' + $lockName;
   $redis->watch($lockKey);
   if($redis->get($lockKey) === $identifier){
       $redis->multi();
       $redis->del($lockKey);
       $redis->exec();
       return true;
   }
   $redis->unwatch();
   return false;}

说明:

获取锁:
创建唯一的$identifier,这个值用于删除key的时候,判断是否为当前客户端获取的锁,以免删除其它客户端的锁while循环用于在一段时间内不停的获取锁如果能够获取锁便获取,同时设置超时时间,防止线程运行时崩溃,锁永远无法释放如果没能成功获取到锁,检查当前锁的过期时间,如果未设置过期时间,进行设置,防止其他线程获得锁后立即崩溃,没有设置过期时间

2. 处理业务

需要先判断交易双方是否都满足条件,因为锁定的是整个市场,所以一旦获得锁,交易双方的状态都不会再进行改变使用管道能保证整个交易能像事务一样被处理,而且性能会比用redis的事务更好

3. 释放锁

使用watch监控锁,一旦key被变更,删除key的事务不会被执行需要判断key的值是否和本线程记录的$identifier一样,只有一致才能进行删除使用事务来做删除key的操作,使用事务的原因是防止中途该锁被别的线程获取如果失败,记得unwatch

4. 其他问题

可重入问题:可重入指的是,线程可以再次获取到锁。实现方法比较简单,只需要在acquireLock的时候,传入identifier,判断当前锁的identifier和传入的是否一致,如果一致则可以进行操作线程未执行完毕,锁的超时时间已过,其他线程获取到锁:解决该问题的一个方案是,当获取到锁后,在超时时间经过一半的时候检查锁是否存在或者被修改,如果没有变化且线程正常运行,则延长超时时间


思考

如果基于Redis单实例,假设这个单实例总是可用,这种方法已经足够安全。

但有两种特殊情况大家需要关注一下:

主从结构中存在明显的竞态:

客户端A从master获取到锁在master将锁同步到slave之前,master宕掉了。slave节点被晋级为master节点客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!

在Redis的分布式环境中,有N个Redis master

这种情况可以使用Redlock算法

总结

本文讲述了怎样用Redis实现分布式锁,并写了具体实现和相关的分析。要用Redis实现分布式锁,有很多细节需要思考,大家可以根据自己的业务形态设计符合自己要求的锁,在复杂度和安全性上做好折中。


版权申明:本博文版权归博主所有,转载请注明地址!如有侵权、违法,请联系admin@php.cn举报处理!

全部评论

文明上网理性发言,请遵守新闻评论服务协议

条评论
  • 本篇文章给大家介绍一下Redis中的,介绍一下为什么需要Redis是如何的,希望对大家有所帮助!
    三种:1、基于数据库;2、基于缓存(Redis等);3、基于Zookeeper
    本篇文章给大家介绍一下如何使用Redis一个安全可靠的,说明的主要要素,常见误区。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。
    ​下面由Redis教程栏目给大家介绍关于3种Redis的对比,希望对需要的朋友有所帮助!我们通常使用的synchronized或者Lock都是线程,对同一个JVM进程内的多个线程有效。
    redis能用的的加命令别是INCR、SETNX、SET,本文就来为大家介绍一下PHP+redis和解的方法,希望对大家有一定的帮助。
    关系型数据库遵循ACID规则,事务在英文中是transaction,和世界中的交易很类似,redis栏目介绍数据库CAP原理到底是什么。
    redis限流的有3种,别是:1、基于Redis的setnx的操作,给指定的key设置了过期践;2、基于Redis的数据结构zset,将请求打造成一个zset数组;3、基于Redis的令牌桶算法
    Redis限流的有3种,别是:1、基于Redis的setnx的操作,给指定的key设置了过期践;2、基于Redis的数据结构zset,将请求打造成一个zset数组;3、基于Redis的令牌桶算法
    下面由Redis教程栏目给大家介绍正确地使用Redis的SETNX机制,希望对需要的朋友有所帮助!
    下面由Redis教程​栏目给大家介绍redis原理及,希望对需要的朋友有所帮助!redis是nosql(也是个巨大的map) 单线程,但是可处理1秒10w的并发...
    css响应局的方法:1、使用flex局,优点是代码简单、局方便;2、使用绝对局,结合使用media可以响应局;3、使用grid局,优点是写法简便;4、使用float局,优点是兼容性比较好
    常见的乐观有两种,别是:1、版本号机制;2、CAS算法。其中,通过版本号机制乐观是最经典的方法。版本号机制一般是在数据表中加上一个数据库版本号version字段。
    redis可以采用非脚本方和Lua脚本方限流。
    使用Spring Session和Redis解决Session跨域共享问题,象阐述,在项目中前后端代码未做离,在两台例的情况下服务正常运行偶尔会弹出类似需要重新登录的提示,后台报错信息这是处理器异常
    在bootstrap中可以利用栅格系统来响应局,其方法:首先通过一系列的行与列的组合来创建页面局;然后根据不同屏幕尺寸,选择不同的栅格选项;最后自动排版即可。
    本篇文章给大家享一些Redis中关于缓存的面试题,内含答案解析。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。
    借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。
    CSS怎么流?下面本篇文章给大家介绍一下使用CSS流的两种方,希望对大家有所帮助!
    redisson和下列一下自行封装两种方的区别(场景):redisson未获取到的会进入等待,直到获取到。另外两种方如果未获取到,会放弃,不会执行业务代码。
    本篇文章给大家介绍一下Redis限流的三种方。有一定的参考价值,有需要的朋友可以参考一下,希望对大家有所帮助。