redis 应用-分布式锁
单体应用可以使用 synchronized 或 RentranLock 来加锁,synchronized 推荐使用类锁,也就是字节码锁,这样保证是全局唯一的,如果使用对象锁,要根据业务确定这个对象锁在这个业务中是唯一的。
对于微服务架构下,单体应用锁就不合适了,每个服务多个节点部署,虚拟机都不是用一个,肯定保证不了唯一性。比如同一个商品下单,用户 a 的请求到 节点1 处理,用户 b 的请求到 节点2 处理。
分布式锁
所有节点的所有线程都到同一个地方去获取锁,只有一个线程能成功获取,未获取到锁的线程就等待或结束。具备以下条件
- 互斥性:任意时刻只能一个客户端获取到锁
- 锁超时释放:某个客户端超时持有锁,需要自动释放机制。简言之就是 防止死锁
- 安全性:只能当前持有者释放锁
实现方式有数据库、Zookeeper、redis 等,这里只介绍 redis
redis 分布式锁
基本方式
利用 setnx 命令往 redis 添加值的方式来加锁。setnx key value,当 key 不存在才能设置成功
- 业务开始,先使用 setnx 加锁(如果设置值成功,说明加锁成功,反之加锁失败)
- 当加锁成功,处理业务
- 业务结束,释放锁(删除 key)
以上方式是有隐患的,假设 节点1 加锁成功,但是处理业务出现异常或者节点1直接挂了,导致锁始终存在,发生死锁,别的节点再也不能获取锁了
| 线程a | 线程b |
|---|---|
| setnx 加锁成功 | setnx 加锁失败 |
| 业务处理 | setnx 加锁失败 |
| 发生异常或节点挂了(这个锁就永不删除) | setnx 加锁失败 |
方案改进,锁自动过期避免死锁
给 setnx 加过期时间。不要单独给 key 增加过期时间(也就是不要写成两条命令,一条 setnx,一条 expire,合在一起才能保证原子性)
# 分开写
setnx key valye;
expire key 时间;
# 合在一起写
setex key 过期时间 value;
对应 redisTemplate 写法
// 60s 自动释放
redisTemplate.opsForValue().setIfAbsent(key, value, 60L, TimeUnit.SECONDS);
这种还是会有隐患,比如过期时间设置的 10s,但是业务需要 20s,这种情况会有问题
- 线程a 的业务没结束锁就释放了(需要延迟释放锁机制,等到业务结束才释放锁)
- 线程b 执行,发现加锁成功,执行线程b的业务,执行的过程中,线程a 业务结束,线程a 释放锁。a 的锁自动释放了,现在释放的是 线程b 的锁
| 线程a | 线程b |
|---|---|
| setnx 加锁成功 | setnx 加锁失败 |
| 业务处理 | setnx 加锁失败 |
| 发生异常或节点挂了(这个锁就永不删除) | setnx 加锁失败 |
| 自动过期 | setnx 加锁成功 |
方案再次改进,自己释放自己加的锁
自己释放自己的锁,这个比较好实现,setnx 只添加了 key,value 没有处理,可以把 value 设置为一个 uuid,业务结束时,删除值是 uuid 的 key。
| 步骤 | 描述 |
|---|---|
| setnx 加锁 | 如果成功,key 的值设置成一个 uuid |
| 业务处理 | |
| 业务结束,释放锁 | 查一下 key,如果 key 的值是上面设置的 uuid 才删除 key 如果能查到 key,但是值不是上面设置的 uuid,现在如果删除,就把别的线程的 key 删除了 |
这个过程分为 3 个步骤 查找 key,判断 value,删除 key,极端情况下还是会有问题,因为这三个操作是独立的的,没有原子性,可能判断完正准备删除还没删除时,刚好到了 key 的过期时间自动删除,这时删除的还是别的线程的锁。极端情况如下:
| 某个线程 | 锁过期时间 |
|---|---|
| 查找 key | |
| 判断 key() | |
| 到了过期时间,自动删除 | |
| 删除 key |
这时需要保证删除 key 的原子性,利用 redis 的事务或 lua 脚本
方案再次改进,自动续期
当加锁后,开启一个线程,线程的作用也很简单:就是每隔一段时间检查一下 redis 服务器是否存在该 key,如果存在,就把过期时间加长。这样就能避免业务处理时间大于锁过期时间导致的问题。redisson 中就是这种逻辑,这个线程也有个名字叫做 看门狗。redisson 中只有当设置了自动过期时间的 key 才引入 看门狗线程
RedLock
前面讨论的都是单机版 redis 服务器,往往生产环境中 redis 都是多台,哨兵 或 cluster 模式。
可能出现锁丢失,redis master 添加 key,还没有同步到 salve,此时 master 挂了,其中的一个 salve 升级为 master,这时原来 master 中的 key 就丢失了
解决方案就是红锁:大概意思是 master 节点是多个,尽量避免同时挂掉,每个 master 之间不是自动同步的,而是分别写入,超过半数写入成功,才算加锁成功。比如 添加一个 key,往 5 个 master 里都执行 setnx 命令,还有一些时间上的校验等,后面有兴趣再深入吧
