当前位置:   article > 正文

redis 分布式锁原理解析及使用教程详细_redis分布式锁

redis分布式锁

一、分布式锁

1、定义

image.png

2、特性

  • 互斥性: 和我们本地锁一样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥。
  • 可重入性: 同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
  • 锁超时: 和本地锁一样支持锁超时,防止死锁。
  • 高效,高可用: 加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
  • 支持阻塞和非阻塞: 和ReentrantLock一样支持lock和trylock以及tryLock(long timeOut)。
  • 支持公平锁和非公平锁(可选): 公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。

二、应用场景

分布式系统中,避免不同节点重复相同的工作。
如果两个节点在同一条数据上面操作,比如多个节点机器对同一个订单操作不同的流程有可能会导致该笔订单最后状态出现错误,造成损失。(可以将多个服务理解为多个线程)

三、实现方式

mysql

  • 悲观锁
  • 乐观锁

zookeeper
redis

  • 原生redis API
  • redission

对比:
**1、**zookeeper分布式锁实现简单,集群自己来保证数据一致性,但是会存在建立无用节点且多节点之间需要同步数据的问题,因此一般适合于并发量小的场景使用,例如定时任务的运行等。
2、redis分布式锁(非redlock)由于redis自己的高性能原因,会有很好的性能,但是极端情况下会存在两个客户端获取锁,因此适用于高并发的场景。
**3、**database分布式锁由于数据库本身的限制:性能不高且不满足高可用(即是存在备份,也会导致数据不一致),因此,工作中很难见到真正使用数据库来作为分布式锁的解决方案,这里使用数据库实现主要是为了理解分布式锁的实现原理

四、redis实现分布式锁的原理

1、预备知识——lua脚本

下面简单介绍下EVAL命令的使用。Redis提供了EVAL命令可以使开发者像调用其他Redis内置命令一样调用脚本:

[EVAL] [脚本内容] [key参数的数量] [key …] [arg …]
  • 1


可以通过key和arg这两个参数向脚本中传递数据,他们的值可以在脚本中分别使用KEYS和ARGV 这两个类型的全局变量访问。比如我们通过脚本实现一个set命令,通过在redis客户端中调用,那么执行的语句是:

eval "return redis.call('set',KEYS[1],ARGV[1])" 1 hello world	
  • 1


EVAL命令是根据 key参数的数量-也就是上面例子中的1来将后面所有参数分别存入脚本中KEYS和ARGV两个表类型的全局变量。当脚本不需要任何参数时也不能省略这个参数。如果没有参数则为0:

eval "return redis.call('get','hello')" 0
  • 1
1)Redis执行lua脚本文件

编写Lua脚本文件

local key = KEYS[1]
local val = redis.call("GET", key);

if val == ARGV[1]
then
redis.call('SET', KEYS[1], ARGV[2])
return 1
else
return 0
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10


执行Lua脚本文件

执行命令: redis-cli -a 密码 --eval Lua脚本路径 key [key …] ,  arg [arg …]
如:redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi
  • 1
  • 2


注意:“–eval"而不是命令模式中的"eval”,一定要有前端的两个-,脚本路径后紧跟key [key …],相比命令行模式,少了numkeys这个key数量值。key [key …] 和 arg [arg …] 之间的“ , ”,英文逗号前后必须有空格,否则死活都报错。

第一次执行:compareAndSet成功,返回1
第二次执行:compareAndSet失败,返回0

[root@vm01  learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi 
(integer) 1
[root@vm01  learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi 
(integer) 0
  • 1
  • 2
  • 3
  • 4
2)学习链接

Lua 教程 | 菜鸟教程

2、redis原生API(以jedis为例)

Redis分布式锁机制,主要借助setnx和expire两个命令完成。

  • setnx:将 key 的值设为 value,当且仅当 key 不存在; 若给定的 key 已经存在,则 SETNX 不做任何动作。
  • expire:为 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除
1)简单版本

通过Redis的setnx、expire命令可以实现简单的锁机制:

  • key不存在时创建,并设置value和过期时间,返回值为1;成功获取到锁;
  • 如key存在时直接返回0,抢锁失败;
  • 持有锁的线程释放锁时,手动删除key; 或者过期时间到,key自动删除,锁释放。

线程调用setnx方法成功返回1认为加锁成功,其他线程要等到当前线程业务操作完成释放锁后,才能再次调用setnx加锁成功。
image.png

存在问题

如果出现了这么一个问题:如果setnx是成功的,但是expire设置失败,一旦出现了释放锁失败,或者没有手工释放,那么这个锁永远被占用,其他线程永远也抢不到锁。
所以,需要保障setnx和expire两个操作的原子性,要么全部执行,要么全部不执行,二者不能分开。
有以下三种方式来保证两个操作的原子性:

  • 事务——redis watch
  • 原子命令
  • lua脚本
2)set命令拓展

使用set的命令时,同时设置过期时间,不再单独使用 expire命令
命令格式:

set key value [EX seconds] [PX milliseconds] [NX|XX]
  • 1

EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回OK,失败返回(nil)
XX:key存在时设置value,成功返回OK,失败返回(nil)

加锁代码实现
package com.crazymaker.springcloud.standard.lock;

@Slf4j
@Data
@AllArgsConstructor
public class JedisCommandLock {

    private  RedisTemplate redisTemplate;

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static   boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

这个set()方法一共有五个形参:
第一个为key,我们使用key来当锁,因为key是唯一的。
第二个为value,我们传的是requestId,requestId可以使用UUID.randomUUID().toString()方法生成。
第三个为nx/xx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为ex/px,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:

  • 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。
  • 已有锁存在,不做任何操作。
解锁代码实现
package com.crazymaker.springcloud.standard.lock;

@Slf4j
@Data
@AllArgsConstructor
public class JedisCommandLock {
    
    private static final Long RELEASE_SUCCESS = 1L;
    
    /**
    * 释放分布式锁
    * @param jedis Redis客户端
    * @param lockKey 锁
    * @param requestId 请求标识
    * @return 是否释放成功
    */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

那么这段Lua代码的功能是什么呢?
首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。
第一行代码,我们写了一个简单的Lua脚本代码。
第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

3)加锁、解锁全部使用lua脚本实现
4)存在问题

如果操作共享资源的时间大于过期时间(数据还没使用完就过期了),就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。

解决方案——自动续期

3、redission

Redisson 官方文档中文翻译

1)基本流程

image.png

2)提供的锁的类型
  • Lock——普通锁
  • Fair Lock——公平锁
  • MultiLock——基于Redlock 锁
  • ReadWriteLock——读写锁
  • Semaphore——信号量
  • CountDownLatch——计数器
3)lock
  1. 设置默认有效时间为30 s
  2. 获取当前线程id
  3. 获取锁的剩余时间ttl
    1. 尝试加锁
      1. 使用lua 脚本:如果lockkey 不存在,则使用hset设置lockkey,并设置过期时间
      2. 使用hexists 获取当前线程对应的 lockkey ,如果存在,则key +1(可重入原理),并设置过期时间
    2. 注册监听事件(trylock或者设置过期时间的 lock 是没有自动续期的,只是使用redis 发布/订阅 模式监听锁的状态
      1. 如果获取锁成功,为锁延长过期时间,每次延长的时间为参数 LockWatchdogTimeout / 3
  4. 如果ttl 大于0,说明锁没有过期
  5. 如果ttl < 0 ,说明锁已经过期,可以直接获取
4)unlock
  1. 判断锁是否存在,不存在的话用 publish 命令发布释放锁的消息,订阅者收到后就能做下一步的拿锁处理;
  2. 锁存在但不是当前线程持有,返回空置nil;
  3. 当前线程持有锁,用 hincrby 命令将锁的可重入次数 -1,然后判断重入次数是否大于 0,是的话就重新刷新锁的过期时长,返回0,否则就删除锁,并发布释放锁的消息,返回 1;
  4. 取消"看门狗"的续时线程;

image.png

4、redis集群如何保证分布式锁的可靠性

为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。(MOGO 的部署方式是Keeplive+ VIP + 主从哨兵,通过配置 Keeplive + VIP 进行web 负载均衡Redis高可用架构—Keepalive+VIP - 如.若 - 博客园)
Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis集群中有一个 master节点宕机了,此时之前的slave 节点被选举为新的 master节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
针对这个问题,Redis 之父 antirez 设计了 Redlock 算法 open in new window 来解决。
Redis RedLock 完美的分布式锁么?
【求锤得锤的故事】Redis锁从面试连环炮聊到神仙打架。

五、分布式锁的最佳实践

单节点redis——redission
集群redis
可以接受极端情况下的加锁问题——redission
不可以接受集群引起的加锁问题——zookeeper
性能比对:
redission>zookeeper>mysql
可靠性比对:
zookeeper> redis> mysql

参考文献:
通俗讲解分布式锁:场景和使用方法-阿里云开发者社区
Redis进阶- Redisson分布式锁实现原理及源码解析 - 腾讯云开发者社区-腾讯云
Redisson分布式锁源码解读_浩骞的博客-CSDN博客_redisson中lock和trylock的区别
Redission分布式锁原理 - harara-小念 - 博客园

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/696722
推荐阅读
相关标签
  

闽ICP备14008679号