前言
上图所示是我们搭建的分布式环境,有三个购票项目,对应一个库存,每一个系统会有多个线程,和上文一样,对库存的修改操作加上锁,能不能保证这6个线程的线程安全呢?
当然是不能的,因为每一个购票系统都有各自的JVM进程,互相独立,所以加synchronized只能保证一个系统的线程安全,并不能保证分布式的线程安全。
所以需要对于三个系统都是公共的一个中间件来解决这个问题。
这里我们选择Redis来作为分布式锁,多个系统在Redis中set同一个key,只有key不存在的时候,才能设置成功,并且该key会对应其中一个系统的唯一标识,当该系统访问资源结束后,将key删除,则达到了释放锁的目的。
分布式锁需要注意哪些点
1)互斥性
在任意时刻只有一个客户端可以获取锁。
这个很容易理解,所有的系统中只能有一个系统持有锁。
2)防死锁
假如一个客户端在持有锁的时候崩溃了,没有释放锁,那么别的客户端无法获得锁,则会造成死锁,所以要保证客户端一定会释放锁。
Redis中我们可以设置锁的过期时间来保证不会发生死锁。
3)持锁人解锁
解铃还须系铃人,加锁和解锁必须是同一个客户端,客户端A的线程加的锁必须是客户端A的线程来解锁,客户端不能解开别的客户端的锁。
4)可重入
当一个客户端获取对象锁之后,这个客户端可以再次获取这个对象上的锁。
Redis分布式锁流程
Redis分布式锁的具体流程:
1)首先利用Redis缓存的性质在Redis中设置一个key-value形式的键值对,key就是锁的名称,然后客户端的多个线程去竞争锁,竞争成功的话将value设为客户端的唯一标识。
2)竞争到锁的客户端要做两件事:
设置锁的有效时间 目的是防死锁 (非常关键)
需要根据业务需要,不断的压力测试来决定有效期的长短。
分配客户端的唯一标识,目的是保证持锁人解锁(非常重要)
所以这里的value就设置成唯一标识(比如uuid)。
3)访问共享资源
4)释放锁,释放锁有两种方式,第一种是有效期结束后自动释放锁,第二种是先根据唯一标识判断自己是否有释放锁的权限,如果标识正确则释放锁。
加锁和解锁
加锁
1)setnx命令加锁
set if not exists 我们会用到Redis的命令setnx,setnx的含义就是只有锁不存在的情况下才会设置成功。
2)设置锁的有效时间,防止死锁 expire
加锁需要两步操作,思考一下会有什么问题吗?
假如我们加锁完之后客户端突然挂了呢?那么这个锁就会成为一个没有有效期的锁,接着就可能发生死锁。虽然这种情况发生的概率很小,但是一旦出现问题会很严重,所以我们也要把这两步合为一步。
幸运的是,Redis3.0已经把这两个指令合在一起成为一个新的指令。
来看jedis的官方文档中的源码:
解锁
检查是否自己持有锁(判断唯一标识);
删除锁。
解锁也是两步,同样也要保证解锁的原子性,把两步合为一步。
这就无法借助于Redis了,只能依靠Lua脚本来实现。
set方法
Redis 节点故障后,主备切换的数据一致性
Redis 节点故障后,主备切换的数据一致性
客户端 A 从 Master 获取了锁;
Master 宕机了,存储锁的 Key 还没有来得及同步到 Slave 上;
Slave 升级为 Master;
客户端 B 从新的 Master 获取到了对应同一个资源的锁;
客户端 A 和客户端 B 同时持有了同一个资源的锁,锁的安全性被打破。
解决方法:redlock