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,这种情况会有问题

  1. 线程a 的业务没结束锁就释放了(需要延迟释放锁机制,等到业务结束才释放锁)
  2. 线程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 命令,还有一些时间上的校验等,后面有兴趣再深入吧

posted @ 2023-06-28 17:54  CyrusHuang  阅读(18)  评论(0编辑  收藏  举报