一、什么是分布式锁
在一个分布式的系统中,也会涉及到多个节点访问同一个公共资源的情况,此时就需要通过锁来做互斥控制,避免出现类似于 "线程安全" 的问题;
而 Java 中的 synchronized,只能在当前进程中生效,在分布式的这种多个进程多个主机的场景下就无能为力了,此时就要使用到分布式锁;
考虑网购秒杀这样一个场景,限量 1 件商品,现有两个用户(两个客户端),在第一个用户访问一个服务器查询商品剩余量 > 0 之后,执行 -= 1 之前,第二个用户访问另一个服务器,查询此时商品剩余量仍然 > 0,也会执行 -= 1 操作,此时就会存在超卖问题(一个东西同时卖给两个用户);
在这个过程中,为了防止穿插执行的情况,可以使用 MySQL 的事务来解决,但如果使用的数据库不是 MySQL,没有事务,或者执行一段特定的操作,就不能使用 MySQL 的事务了;
此时就要使用分布式锁来解决上述问题,所谓的分布式锁,也是一个 / 一组单独的服务器程序,给其他服务器提供加锁这样的服务;Redis 就是一种典型的可以用来实现分布式锁的方案(mysql,zookeeper 也可以实现分布式锁的效果);
在购买过程前,需要下加锁,完成购买操作之后,再解锁,这样在购买期间,就不会被其他服务器穿插执行了;
二、Redis 实现分布式锁
1. setnx
Redis 实现分布式锁依靠 setnx 命令, setnx key value,如果 key 存在,就设置失败,否则设置 key value;
通过 setnx 实现加锁后,服务器执行相应逻辑之后,解锁就要通过 del 命令来删除 key;
2. set nx ex
如果某个服务器加锁成功后,执行相应逻辑时,服务器宕机,没有执行到解锁操作,就会导致其他服务器也无法获取到这个没有释放的锁(没有删除的 key);
因此就要为该 key 设置过期时间,一旦过期时间到了,就会自动删除该 key,可以使用 Redis 中的set nx ex 命令;
那能否使用 setnx 命令 + expire 设置过期时间呢?
答案是不能的,因为 Redis 中的多个命令之间是不能保证原子性的,不保证执行成功,因此使用一条命令实现是更稳妥的;
3. 引入校验 Id
加锁就是设置一个 key value,而解锁就是把相应的 key 删除;
那就可能存在服务器 1 执行了加锁操作,而服务器 2 执行了解锁操作;(误操作,bug)
这就需要引入校验机制,为每个服务器编号,在设置 key value 的时候,key 对应要对哪个资源加锁,value 就可以存储服务器的编号(标识出当前锁是哪个服务器加上的);
在解锁的时候,先判断该锁对应的编号是否为当前服务器的编号,如果是,才进行解锁操作;
4. 引入 lua 脚本
在上述解锁的过程中,需要先判定,再进行删除,这是两步操作(不是原子的),一个服务器内部,也可能是多线程的,若此时,该服务器内部有两个线程都在执行上述操作,就可能出现问题
此时可以使用 Redis 的事务解决该问题(避免插队),但实践中往往使用 lua 脚本来解决该问题;
lua 是一个特别轻量的编程语言,Lua 语法简单精炼,执行速度快,解释器也比较轻量,并且 Redis 本身就支持 Lua 作为内嵌脚本;因此可以使用 lua 编写一些逻辑,把这个脚本上传到 Redis 服务器上来执行,执行 lua 脚本的过程中是原子的;Redis 官方文档中也明确说到,lua是事务的替代方案;
if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1])
else return 0
end;
上述代码可以编写成一个 .lua 后缀的⽂件,由 redis-cli 或者 jedis 等客户端加载,并发送给 Redis 服务器,由 Redis 服务器来执行这段逻辑,这样执行的效果就是原子的了;
5. 引入 watch dog
当我们为 key 设置了过期时间后,key 过期后会自动删除,但是此时任务还没执行完,这就会导致锁失效,其他服务器的任务来了就会重新设置 key;但如果将 key 的过期时间设置的太长,此时若服务器宕机,就会让其他服务器不能及时获取到锁;
因此就可以引入看门狗 —— 加锁服务器上的一个单独的线程,通过这个线程来完成对锁的过期时间的续约;
比如,某个 key 的过期时间是 5 秒,同时看门狗线程会每 3 秒检测一次,当第一次经过 3 秒后,看门狗线程就会判定当前任务是否已经完成:
- 如果任务未完成,则会将过期时间重新设置为 5 秒;
- 如果任务已经完成,则通过执行上述 lua 脚本的方式删除 key;
这就解决了锁提前失效的问题;并且即使当加锁的服务器挂了,那么此时看门狗线程也会随之挂掉,此时就不会续约,这个 key 到过期时间了就自动删除了,不会影响其他的服务器;
三、总结
以上内容只是如何实现一个简单的互斥锁,在一些实际的应用场景中,我们可能会使用一些其他特殊的锁,比如可重入锁,公平锁,读写锁等等;
关于 Redis 实现分布式锁,在实践中,我们并不会真的自己实现一个分布式锁,已经有很多的库帮我们实现好了,直接使用即可,比较 Redisson;