乐观锁操作

这里不考虑分布式的情况,用的是最简单的乐观锁。即在事务里,使用WATCH查看某个变量。

在事务提交时,如果WATCH的变量发生了改变,那么将取消本次事务。 显然,这是一种乐观锁的实现。

127.0.0.1:6379> multi
127.0.0.1:6379> watch ww
127.0.0.1:6379> set lock1 1
127.0.0.1:6379> exec

悲观锁操作

便是使用setnx指令,它的作用就是(set if not exist),用中文来说,就是设置一个键值对,如果它不存在,就设置成功,返回1,如果存在,则返回0。

127.0.0.1:6379> setnx lock_1 1
(integer) 1
127.0.0.1:6379> setnx lock_1 2
(integer) 0
127.0.0.1:6379> del lock_1
(integer) 1

通过这个指令,可以完成最基本的分布式锁操作,比如下图所示。

image-20220508204311974

但是这种方式,仍然存在诸多问题。在这篇文章里紧接着讲了更多升级版本

(1 封私信) 怎样实现redis分布式锁? - 知乎 (zhihu.com)

如何解决操作资源时异常退出,不能释放锁?

给锁加一个过期时间,在redis里,就表现为设置锁的EXPIRE时间

127.0.0.1:6379> SETNX lock 1 // 加锁 
(integer) 1 
127.0.0.1:6379> EXPIRE lock 10 // 10s后自动过期 
(integer) 1
127.0.0.1:6379> SET lock 1 EX 10 NX // 一条命令保证原子性执行 
OK

如何确定过期时间?

举一个详细的场景来说明这个问题:会话A在操作共享资源时,由于锁到期了,就释放锁,然后另一个会话B来操作共享资源,发现没锁,于是加锁,这时,AB已经都在共享资源里了,这不合理。然后当A退出时,会释放锁,于是B的锁直接失效。

所以事实上,我们要解决两个问题

锁过期:A操作共享资源耗时太久,导致锁被自动释放,之后被B持有。

②**释放别人的锁:**A操作共享资源完成后,却又释放了B的锁。

对于①:安插一个类似守护进程的东西:加锁时,先设置一个过期时间,然后我们开启一个「守护线程」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。

对于②:客户端在加锁时,设置一个只有自己知道的「唯一标识」进去。然后在释放锁时,检查释放的锁,是否满足「唯一标识」

// 锁的VALUE设置为UUID,例如,可以是自己的线程 ID,也可以是一个 UUID(随机且唯一)
127.0.0.1:6379> SET lock $uuid EX 20 NX 
OK

释放锁的lua脚本,lua脚本的操作满足原子性。因为 Redis 处理每一个请求是「单线程」执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样一来,GET + DEL 之间就不会插入其它命令了。

// 判断锁是自己的,才释放 
if redis.call("GET",KEYS[1]) == ARGV[1] 
then
    return redis.call("DEL",KEYS[1]) 
else 
    return 0 
end

Redlock

(1 封私信) 怎样实现redis分布式锁? - 知乎 (zhihu.com)

image-20220508213850199

使用多个redis主库作为,锁服务,降低单个节点宕机产生的问题。

整体的流程是这样的,一共分为 5 步:

  1. 客户端先获取「当前时间戳T1」
  2. 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
  3. 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
  4. 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
  5. 加锁失败,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁) 我简单帮你总结一下,有 4 个重点: 客户端在多个 Redis 实例上申请加锁 必须保证大多数节点加锁成功 大多数节点加锁的总耗时,要小于锁设置的过期时间 释放锁,要向全部节点发起释放锁请求