1.简单锁
redis 的 setnx
命令可以提供互斥,可以实现一个简单分布式锁
1.1 加锁
127.0.0.1:6379> SETNX lock 1(integer) 1 // 客户端1,加锁成功
127.0.0.1:6379> SETNX lock 1(integer) 0 // 客户端2,加锁失败
1.2 释放锁
del
命令可以释放锁,
127.0.0.1:6379> DEL lock // 释放锁(integer) 1
1.3 存在问题
死锁问题: 线程挂掉或程序异常,锁无法释放
2.简单锁 + 超时
setnx+expire 可以为分布式锁加一个超时时间,这样死锁问题可以解决
2.1 实现
127.0.0.1:6379> SETNX lock 1 // 加锁(integer) 1127.0.0.1:6379> EXPIRE lock 10 // 10s后自动过期(integer) 1
2.2 问题
- 锁过期: 线程 1 持有锁时,由于程序执行时间过长,导致锁过期,此时如果被其他线程获取到锁,易产生并发问题
- 释放其他线程的锁: 在 1 的情况下, 当线程 1 执行完程序后,很可能会把线程二的锁释放掉
3 简单锁 + 超时 + 线程 id
3.1 实现
在加锁的时候 key 为加锁资源, value 为线程 id, 解锁时校验线程,可以保证锁不会被其他线程释放
// 锁的VALUE设置为UUID127.0.0.1:6379> SET lock $uuid EX 20 NXOK
// 锁是自己的,才释放if redis.get("lock") == $uuid: redis.del("lock")
3.2 问题
- 释放锁时非原子操作, 先 get 再 del, (问题不大,一般程序中并发请求都是 setnx, 如果用 set 则会产生问题)
- 锁过期问题依旧存在
3.3 优化
使用 lua 脚本释放锁可以实现原子化
// 判断锁是自己的,才释放if redis.call("GET",KEYS[1]) == ARGV[1]then return redis.call("DEL",KEYS[1])else return 0end
4. 锁过期问题
综上所述,使用 setnx + 超时 + 线程 id+lua 脚本 基本可以实现一个严谨的分布式锁
唯一问题似乎只有锁超时的问题了,锁超时问题可以这么处理:
加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。
业界成熟方案有 redission,可以提供
- 可重入锁
- 乐观锁
- 公平锁
- 读写锁
- Redlock(红锁,下面会详细讲)
具体使用方法参考