Redis分布式锁

1.分布式锁简介

在同一个单体应用内部,我们往往使用Synchroized或者Lock的方式来解决多线程之间的安全问题,但是在分布式的架构下,在多个应用之间,就需要一种比较高级的锁机制来处理不同应用之间的线程安全问题。解决方案就是分布式锁。

2. Redis分布式锁

1.原理

Redis分布式锁机制,主要是依靠两个命令来完成。setnxexpire

  • setnx:当key不存在时,将key设置为value,存在则不做任何操作,并且返回0setnx key value
  • expire:设置key的过期时间 expire key time

步骤: 1. 当key不存在时去创建key,并设置value和对应的过期时间,返回1。成功加锁
2. 如果key存在,则会返回0,此时抢锁失败。
3. 当持有锁的线程释放锁时,手动删除key,或者过期时间到了,自动删除key->释放锁
问题:
1.如果加锁成功了,但是设置过期时间失败,但是此时系统挂掉了,那么这个锁就永远不会被释放,此时其他线程就会一直拿不到这个锁了。
解决:
1.setnxd的同时,也设置过期时间,让一起执行。set key value [过期时间类型EX Second| PX millisenconds] [NX|XX]
其中EX,PX,NX,XX的描述信息为:
image.png 2.使用Lua脚本进行处理,命令如下
EVAL script numkeys key [key...] arg [arg...]

  • script: 时一段Lua5.1脚本程序,它会被运行在Redis服务器上下文中
  • numkeys:用户指定键名参数的个数
    在Lua脚本中,可以使用redis.call()命令来执行Redis命令。例如
    eval "return redis.call('set', KEY[1], ARGV[1])" 1 foo bar
    这段脚本可以实现将foo的值设置为bar
  • 1: 代表单数的个数,为一个
  • foo:key[1]
  • bar: ARGV[1]

2.实现

  • 加锁 加锁就是调用set key PX NX命令设置一个key值
  • 解锁 解锁就是删除指定key,即释放了锁
  • 注意: 要保证操作的原子性,如果把判断锁和释放锁分开处理的话,可能会出现误解锁的情况,所以最好保持执行的原子性,即使用Lua脚本把判断锁和是加锁,释放锁都放在一起

3.锁过期问题

如果某个业务操作规定时10s,锁设置的时20s,但是由于操作过于复杂,导致锁的执行时间超过了20s,拿此时业务会在无锁的情况下执行,此时就会发生数据紊乱的情况。 解决:

  1. 乐观锁方式,增加版本号 通过根据锁的版本号来判断,当前线程持有的锁是否还存在,如果对比后版本号不一致,则直接拒绝处理业务。但是这种方式会入侵代码。
  2. 通过watch dog自动延期机制 当客户端加锁成功了,就会启动一个后台守护线程,该线程每10s会检查一下,如果客户端还持有锁,就会不断的延长锁key的生存时间。注:watchDog只在未指定显示的加锁时间才有效,即默认设置key过期时间的情况下。

3. Redisson分布式锁

1.简介

Redisson是基于Netty的Redis客户端。不但能操作原生的Redis数据结构,还为使用者提供了一系列具有分布式特性的常用工具类,实现了分布式锁。

  1. Redisson分布式锁和JUC的Lock方法相似。RLock接口继承了Lock接口。

  2. 所得结构是Hash -key:锁的名字 -字段:UUID+threadId -值:代表的重入次数 !

  3. 锁的重入 当锁重入一次,重入值就会+1,并且过期时间也会更新

2.加锁原理

  1. 判断有没有“DISLOCK”,如果没有,设置UUID:1=1,并且设置它的过期时间
  2. 如果key和字段都存在,进行锁重入,执行命令incrby UUID:1 1 ,结果:DISLOCK:{UUID:1 2}
  3. 客户端2进入,判断有没有KEY,没有字段,返回过期时间,客户端2自旋等待

3.释放锁

1.判断KEY是否存在,如果不存在返回nil,如果存在,使用hincrby-1 把重入次数减1 2.减完后,counter>0值依然大于0,则返回0 3.剪完后,counter<=0,则删除key,并使用publish广播锁释放消息