Redis基础知识(学习笔记22--分布式锁 Redisson )

分布式锁是控制分布式系统间同步访问共享资源的一种方式,其可以保证共享资源在并发场景下的数据一致性。

1. 工作原理

当有多个线程要访问某一个共享资源(DBMS中的数据或Redis中的数据,或共享文件等)时,为了达到协调多个线程的同步访问,此时就需要使用分布式锁了。

为了达到同步访问的目的,规定:让这些线程在访问共享资源之前先要获得一个令牌token,只有具有令牌的线程才可以访问共享资源。这个令牌就是通过各种技术实现的分布式锁。而这个分布式锁是一种”互斥资源“,即只有一个。只要有线程抢到了锁,那么其它线程只能等待,直到锁被释放或等待超时。

2. Redisson 

2.1 原理

Redisson 内部使用Lua脚本实现了对可重入锁的添加、重入、续约(续命)、释放。Redisson需要用户为锁指定一个key,但无需为锁指定过期时间,因为它有默认过期时间(当然,也可指定)。由于该锁具有“可重入”功能,所以Redisson会为该锁生成一个计数器,记录一个线程重入锁的次数。

2.2. 加锁与释放锁

其核心是通过Lua脚本实现

 

加锁的Lua脚本

//exists',KEYS[1])==0 不存在,没锁
"if (redis.call('exists',KEYS[1])==0) then "+ --看有没有锁
 // 命令:hset,1:第一回
 "redis.call('hset',KEYS[1],ARGV[2],1) ; "+ --无锁 加锁 
 // 配置锁的生命周期 
 "redis.call('pexpire',KEYS[1],ARGV[1]) ; "+ 
 "return nil; end ;" +
//可重入操作,判断是不是我加的锁
"if (redis.call('hexists',KEYS[1],ARGV[2]) ==1 ) then "+ --我加的锁
 //hincrby 在原来的锁上加1
 "redis.call('hincrby',KEYS[1],ARGV[2],1) ; "+ --重入锁
 "redis.call('pexpire',KEYS[1],ARGV[1]) ; "+ 
 "return nil; end ;" +
//否则,锁存在,返回锁的有效期,决定下次执行脚本时间
"return redis.call('pttl',KEYS[1]) ;" --不能加锁,返回锁的时间

lua的作用:保证这段复杂业务逻辑执行的原子性。

lua的解释:

  • KEYS[1]) : 加锁的key
  • ARGV[1] : key的生存时间,默认为30秒
  • ARGV[2] : 加锁的客户端ID (UUID.randomUUID()) + “:” + threadId)

释放锁的lua脚本

# 如果key已经不存在,说明已经被解锁,直接发布(publish)redis消息(无锁,直接返回)
"if (redis.call('exists', KEYS[1]) == 0) then " +
 "redis.call('publish', KEYS[2], ARGV[1]); " +
 "return 1; " +
 "end;" +
# key和field不匹配,说明当前客户端线程没有持有锁,不能主动解锁。 不是我加的锁 不能解锁 (有锁不是我加的,返回)
 "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
 "return nil;" +
 "end; " +
# 将value减1 (有锁是我加的,进行hincrby -1 )
 "local counter = redis.call('hincrby', KEYS[1], ARGV[3],-1); " +
# 如果counter>0说明锁在重入,不能删除key
 "if (counter > 0) then " +
 "redis.call('pexpire', KEYS[1], ARGV[2]); " +
 "return 0; " +
# 删除key并且publish 解锁消息
# 可重入锁减完了,进行del操作
 "else " + 
 "redis.call('del', KEYS[1]); " + #删除锁
 "redis.call('publish', KEYS[2], ARGV[1]); " +
 "return 1; "+
 "end; " +
 "return nil;",
  • – KEYS[1] :需要加锁的key,这里需要是字符串类型。
  • – KEYS[2] :redis消息的ChannelName,一个分布式锁对应唯一的一个channelName: “redisson_lockchannel{” + getName() + “}”
  • – ARGV[1] :reids消息体,这里只需要一个字节的标记就可以,主要标记redis的key已经解锁,再结合 redis的Subscribe,能唤醒其他订阅解锁消息的客户端线程申请锁。
  • – ARGV[2] :锁的超时时间,防止死锁
  • – ARGV[3] :锁的唯一标识,也就是刚才介绍的 id(UUID.randomUUID()) + “:” + threadId

2.3 网址

https://github.com/redisson/redisson

知识wiki

https://github.com/redisson/redisson/wiki/Table-of-Content

3. Redisson 常用锁

3.1  可重入锁

Redisson的分布式锁RLock是一种可重入锁。当一个线程获取到锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。

  • JDK中的reentrantLock(entrant是进入者、新会员的意思,reentrant是 可重入、可重入的 的意思)是可重入锁,其是通过AQS(抽象对象同步器)实现的锁机制。
  • synchronized也是可重入锁,其是通过监视器模式(本质是OS的互斥锁)实现的锁机制。

3.2 公平锁  Fair Lock

Redisson的可重入锁RLock默认是一种非公平锁,但也支持可重入公平锁 Fair Lock。当有多个线程同时申请锁时,这些线程会进入到一个FIFO队列,只有队首元素才会获取到锁,其它元素等待。只有当锁被释放后,才会再将锁分配给当前的队首元素。

3.3 联锁  MultiLock

Redisson 分布式锁可以实现联锁 MultiLock。当一个线程需要同时处理多个共享资源时,可使用联锁。即一次性申请多个锁,同时锁定多个共享资源。联锁可以预防死锁。相当于对共享资源的申请实现了原子性:要么都申请到,只要缺少一个资源,则将申请到的资源释放。其是OS底层原理中 AND 型信号量机制的典型应用。

3.4  红锁 RedLock

Redisson 分布式锁可以实现红锁 RedLock。红锁由多个锁(同时至少是向三个Redis集群申请的锁)构成,只有当这些锁中的大部分锁申请成功时,红锁才申请成功。红锁一般用于解决Redis主从集群锁丢失问题。

红锁和联锁的区别:红锁实现的是对同一个共享资源的同步访问控制,而联锁实现的是多个共享资源的同步访问控制。

3.5  读写锁  RReadWriteLock

通过Redisson可以获取到读写锁 RReadWriteLock。通过 RReadWriteLock 实例可分别获取到读锁 RedissonReadLock 和 写锁 RedissonWriteLock。读锁与写锁分别是实现了RLock的可重入锁。

一个共享资源,在没有写锁的情况下,允许同时添加多个读锁。只要添加了写锁,任何读锁与写锁都不可以再次添加。即读锁是共享资源,写锁是排他锁。

3.6 信号量 Semaphore

通过Redisson可以获取到信号量RSemaphore。RSemaphore的常用场景有两种:一种是,无论谁添加的锁,任何其它线程都可以解锁,就可以使用RSemaphore。另外,当一个线程需要一次申请多个资源时,可使用RSemaphore。RSemaphore是信号量机制的典型应用。 

3.7 可过期性信号量 PermitExpirableSemaphore

通过Redisson可以获取到可过期信号量PermitExpirableSemaphore。该信号量是在RSemaphore基础上,为每个信号量增加了一个过期时间,且每个信号都可以通过独立的ID来辨识。释放时也只能通过提交该ID才能释放。

不过,一个线程每次只能申请一个信号量,当然每次也只会释放一个信号量。这是与RSemaphore不同的地方。

该信号量为互斥信号量时,其就等同于可重入锁。或者说,可重入锁就相当于信号量为1的可过期信号量。

可过期信号量与可重入锁的区别,可重入锁:相当于用户每次只能申请1个信号量,且只有一个用户可以申请成功。可过期信号量:用户每次只能申请1个信号量,但可以有多个用户申请成功。

3.8 闭锁  RCountDownLatch

通过Redisson 可以获取到分布式闭锁 RCountDownLatch,其与JDK的JUC中的闭锁CountDownLatch原理一样,用法类似。其常用于一个或多个线程的执行必须在其它某些任务执行完毕的场景。例如,大规模分布式并行计算中,最终的合并计算必须基于很多并行计算的运行完毕。

闭锁中定义了一个计数器和一个阻塞队列。阻塞队列中存放者待执行的线程(又称 合并线程)。每当一个并行任务执行完毕(又称 条件线程),计数器就减1.当计数器递减到0时就会唤醒阻塞队列中的所有线程。

如果不使用Redisson,那么通常使用Barrier队列解决该问题,而Barrier队列通常使用Zookeeprt实现。

 

学习笔记--参阅特别声明

1.【Redis视频从入门到高级】

【https://www.bilibili.com/video/BV1U24y1y7jF?p=11&vd_source=0e347fbc6c2b049143afaa5a15abfc1c】

2.《Redis:Redisson分布式锁的使用方式(推荐使用)》

https://www.jb51.net/database/319949d8d.htm

 

作者:东山絮柳仔原文地址:https://www.cnblogs.com/xuliuzai/p/18409643

%s 个评论

要回复文章请先登录注册