锁要解决的就是 资源竞争的问题,也就是要把执行的指令顺序化。
分布式锁本质上要实现的目标就是在 Redis 里面占资源,占资源一般是使用 setnx(set if not exists)
指令,只允许被一个客户端占用。先来先占, 用完了,再调用 del 指令释放。
所谓原子操作是指不会被线程调度机制打断的操作。这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch 线程切换。
分布式锁(悲观锁)
分布式锁是指在分布式的部署环境下,通过锁机制来让多客户端互斥的对共享资源进行访问。
分布式锁的特点
- 互斥性:与本地锁一样,互斥性是最基本的特点,分布式锁需要保证在不同节点的不同线程的互斥。
- 可重入性:同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
- 锁超时:和本地锁一样支持锁超时,防止死锁。
- 高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
- 支持阻塞和非阻塞:和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)
操作分布式锁
使用 setnx 指令,只允许一个客户端占锁。先来先占,用完了,再调用 del 指令释放锁。
setnx key value
拿到锁后,执行 del 之前,程序挂了
这时会出现死锁,锁永远得不到释放。这时需要设置锁的过期时间:
expire key time
拿到锁后,执行设置超时指令之前,程序挂了
Redis 的 setnx 命令是当 key 不存在时设置 key,但 setnx 不能同时完成 expire 设置失效时长,不能保证 setnx 和 expire 的原子性。可以使用 set 命令完成 setnx 和 expire 的操作,并且这种操作是原子操作。 下面是 set 命令的可选项:
set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key 不存在时设置 value,成功返回OK,失败返回(nil)
XX:key 存在时设置 value,成功返回OK,失败返回(nil)
// 案例:设置 name=p7+,失效时长100s,不存在时设置
1.1.1.1:6379> set name p7+ ex 100 nx
OK
1.1.1.1:6379> get name
"p7+"
1.1.1.1:6379> ttl name
(integer) 94
超时问题怎么处理?
Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之间拿到了锁。
有一个更加安全的方案是为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key。但是匹配 value 和删除 key 不是一个原子操作,Redis 也没有提供类似于 delifequals 这样的指令,这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行。
锁的重入性
可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加锁,那么这个锁就是可重入的。比如 Java 语言里有个 ReentrantLock 就是可重入锁。Redis 分布式锁如果要支持可重入,需要对客户端的 set 方法进行包装,使用线程的 Threadlocal 变量存储当前持有锁的计数。