文章目录
背景:
基于优惠卷抢票下面实现的分布式锁。使用 redission 组件来实现。
1. 什么是分布式锁
前端 nginx 将流量代理到不同的服务上面去,不同的实列服务访问同一个资源。
分布式锁是控制分布式系统同时访问资源的一种方式,在单机或者单进程下面,多线程并发的时候,可以加一个锁比如 synchronized 或者 Reentrantlock 类来控制资源访问。在分布式系统简单加锁就不适用了。前面两个都是在一个 JVM 或者线程有效的,分布式情况下,是跨多个 JVM 的,所以这种锁就会失效。
常见的分布式锁可以用 Redis 去实现。
2. 分布式锁的特性
分布式锁需要保证以下的特点:
互斥性:只有一个竞争者才能持有锁,这一点要尽可能保证。
对称性:同一个锁,加锁和解锁只能时同一个竞争者,不能把其他竞争者的锁给释放 了,UUID 作为 value,保证自己的锁只能自己释放。
可靠性:比如正在拿锁的服务挂掉了,要有一些容灾的处理。配置超时时间等
3.实现
3.1 通过 Redis setnx 命令
通过 Redis 的 setnx 命令来设置一个分布式锁。
setnx key value
通过 setnx key value 原理: 如果 key 不存在那么会返回 1,如果 key 存在那么就返回 0.
执行完成逻辑之后之后再 delete key 就可以释放锁了
SETNX lock:order:123 "thread-123-uuid-abc" # 尝试获取锁
key 就是业务的 ID,Value 是对应的线程 ID+拼接 UUID。
3.2 删除锁
对于 delete 操作:
value 必须不同,存在 A 获取锁的时候,业务执行时间太长了,锁自动过期了,然后 B 获取到了锁,这个时候锁已经被占用了,A 线程结束 了执行,这个时候删除了这把锁,那么其他线程就会进行修改数据。
还要要为 delete 失败进行兜底。如果执行逻辑过程中服务挂了,那么锁一直就会存在,其他线程就只能在外面等待。
使用 lua 脚本来删除锁
3.3 锁需要有过期时间
如果 delete 的服务挂掉了,那么锁一直就得不到释放。需要加一个过期的时间,expire,但是 setnx 和 expire 并不具备原子性,如果 setnx 获取锁之后,服务挂掉了,那么也不行。
redis 提供了 set key value nx ex seconds。 nx 表示具有 setnx 的特定,ex 表示增加了过期时间,最后一个参数就是过期时间的值。
3.4 互斥性保证
客户端 A 还在处理业务当中,时间太长锁过期了。 被 B 使用 del 命令删除了,如果马上有其他客户端来获取锁,就会和 A 一起共享数据了。
给这个锁的 value 设置值得时候, UUID+线程 ID。保证每一个客户端的唯一标识不一样,当删除锁的时候,需要判断检查这个 value 和客户端标识是否一样。 这个也会有问题,删除锁就变成了 读取变量、判断变量、删除变量多个操作,就要保证原子性。这个地方就需要整合 Redis 的 LUA 脚本,整合 LUA 脚本。 为什么不适用线程 ID 呢,不同的 JVM 线程的 ID 可能相同。
3.5 锁续期
有的业务可能因为慢 sql 或者调用第三方执行时间太长了,导致锁已经过去了,这个时候 b 拿到锁,a 执行完成之后再去释放锁,就会出现问题。
可以使用看门狗 watch dog 看门够机制 机制来实现这个问题,新启动一个线程每隔一段时间看一下锁,如果还持有锁,那么就给锁进行延期。
Redission
可以使用 Redission 来实现分布式锁。
1.看门狗机制
2.while 循环不断获取锁
3.redission 可重入
Redission 设置锁,通过配置 redissonClient 拿到 Rlock。
redisson 实现的分布式锁-可重入
底层基于 hash 来实现的,key 是线程 ID,Value 是加锁的次数。
redisson 实现的分布式锁数据一致性
如果某个主机拿到锁还没有释放,数据也没有同步到另一个从节点上面,主节点挂了。
springboot 继承 Redission
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.Redisson;
public class RedissonDistributedLock {
private RedissonClient redissonClient;
public RedissonDistributedLock() {
// 创建 Redisson 客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
this.redissonClient = Redisson.create(config);
}
// 获取锁
public boolean tryLock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待 10 秒,上锁后 30 秒自动释放
return lock.tryLock(10, 30, java.util.concurrent.TimeUnit.SECONDS);
} catch (InterruptedException e) {
return false;
}
}
// 释放锁
public void releaseLock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.unlock();
}
public static void main(String[] args) {
RedissonDistributedLock lock = new RedissonDistributedLock();
String lockKey = "myLock";
if (lock.tryLock(lockKey)) {
System.out.println("Lock acquired");
// 执行任务
lock.releaseLock(lockKey);
System.out.println("Lock released");
} else {
System.out.println("Unable to acquire lock");
}
}
}
总结
面试问题
你提到了 lua,lua 能保证原子性吗?
lua 脚本本身是不能保证原子性的,能保证原子性的是 Redis 的核心逻辑是单线程的。
分布式锁实现的要点是什么?
互斥性、对称性、可靠性。