Redis实现分布式锁
1. 什么是分布式锁
分布式锁是控制分布式系统同时访问资源的一种方式,在单机或者单进程下面,多线程并发的时候,可以加一个锁比如 synchronized 或者 Reentrantlock 类来控制资源访问。在分布式系统简单加锁就不适用了。前面两个都是在一个 JVM 或者线程有效的,分布式情况下,是跨多个 JVM 的,所以这种锁就会失效。
常见的分布式锁可以用 Redis 去实现。
https://www.cnblogs.com/wangyingshuo/p/14510524.html
分布式锁的特性
互斥性:只有一个竞争者才能持有锁,这一点要尽可能保证。
对称性:同一个锁,加锁和解锁只能时同一个竞争者,不能把其他竞争者的锁给释放 了,UUID 作为 value,保证自己的锁只能自己释放。
可靠性:比如正在拿锁的服务挂掉了,要有一些容灾的处理
通过 Redis 的命令
通过 Redis 的 setnx 命令来设置一个分布式锁。
setnx key value
通过 setnx key value 如果 key 不存在那么会返回 1,如果 key 存在那么就返回 0.
执行完成逻辑之后之后再 delete 这个锁即可。
要为 delete 失败进行兜底。如果执行逻辑过程中服务挂了,那么锁一直就会存在,其他线程就只能在外面等待。
加一个过期的时间
如果 delete 的服务挂掉了,那么锁一直就得不到释放。需要加一个过期的时间,expire,但是 setnx 和 expire 并不具备原子性,如果 setnx 获取锁之后,服务挂掉了,那么也不行。
redis 提供了 set key value nx ex seconds。 nx 表示具有 setnx 的特定,ex 表示增加了过期时间,最后一个参数就是过期时间的值。
业务执行时间太长了
客户端 A 还在处理业务当中,时间太长锁过期了。 被 B 使用 del 命令删除了,如果马上有其他客户端来获取锁,就会和 A 一起共享数据了。
给这个锁的 value 设置值得时候, UUID+线程 ID。保证每一个客户端的唯一标识不一样,当删除锁的时候,需要判断检查这个 value 和客户端标识是否一样。 这个也会有问题,删除锁就变成了 读取变量、判断变量、删除变量多个操作,就要保证原子性。这个地方就需要整合 Redis 的 LUA 脚本,整合 LUA 脚本。
使用 Redis 的 LUA 脚本来执行,删除锁不是原子性的操作。
锁过期的时候业务还没有完成
有的业务可能因为慢 sql 或者调用第三方执行时间太长了,导致锁已经过去了,这个时候 b 拿到锁,a 执行完成之后再去释放锁,就会出现问题。
可以使用看门狗机制来实现这个问题,每隔一段时间看一下锁,如果还持有锁,那么就给锁进行延期。
Redission
可以使用 Redission 来实现分布式锁。
总结
实现代码
使用原生的命令+lua 脚本实现分布式锁
import redis.clients.jedis.Jedis;
public class DistributedLock {
private Jedis jedis;
public DistributedLock(Jedis jedis) {
this.jedis = jedis;
}
// 获取锁
public boolean tryLock(String lockKey, String lockValue, int expireTime) {
String luaScript = "if redis.call('exists', KEYS[1]) == 0 then " +
"redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2], 'NX'); " +
"return 1; " +
"else return 0; end";
Object result = jedis.eval(luaScript, 1, lockKey, lockValue, String.valueOf(expireTime));
return "1".equals(result.toString());
}
// 释放锁
public boolean releaseLock(String lockKey, String lockValue) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"redis.call('del', KEYS[1]); return 1; else return 0; end";
Object result = jedis.eval(luaScript, 1, lockKey, lockValue);
return "1".equals(result.toString());
}
}
启动看门狗机制,在创建锁的时候启动一个新的线程,大概是设置过期时间的一半,然后去看一下这个锁也没有过期,没有过期就延迟时间。
// 启动看门狗机制
public void startWatchdog(String lockValue, int expireTime) {
new Thread(() -> {
while (true) {
try {
// 每隔 expireTime / 2 秒延长锁的过期时间
Thread.sleep(expireTime / 2 * 1000);
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"redis.call('expire', KEYS[1], ARGV[2]); " +
"return 1; else return 0; end";
jedis.eval(luaScript, 1, LOCK_KEY, lockValue, String.valueOf(expireTime));
} catch (InterruptedException e) {
break;
}
}
}).start();
}
Redis 的看门狗机制通过定期延长锁的过期时间,避免了因任务执行时间较长而导致锁提前释放的问题。它是一个非常有用的技术,尤其在分布式系统中,可以帮助确保任务在完成前锁不会被误释放,防止死锁的发生。
使用 RedisSession
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");
}
}
}
【用大白话的方式,带你彻底搞懂Redis分布式锁!】https://www.bilibili.com/video/BV1nk4y1u781?vd_source=5e7bb87290f27a331ea3de93e582c5ad
面试问题
你提到了 lua,lua 能保证原子性吗?
分布式锁实现的要点是什么?
加锁、解锁、自己的锁自己删,不能删别人的锁、防止死锁。
#Redis实现分布式锁#牛牛的面试专栏,希望自己在25年可以拿到一份大厂的SP Offer 你的点赞和收藏都是我持续更新的动力