目录
1.2 Redis通过一个lock变量实现一个最简单的分布式锁实现代码:
2.1.1 问题1. 如果加完锁执行业务过程中,还未到释放锁的环节,系统中断,出现死锁的情况,怎么解决?
2.2.2 问题2. 业务未完成,锁到期自动释放,但把别人正在持有的锁删除了,应该如何解决(如下图)?
3.1 此分布式锁解决了上述的问题1与问题2,但释放锁还存在问题
1. redis分布式锁
1.1基本原理图示如下
1.2 Redis通过一个lock变量实现一个最简单的分布式锁实现代码:
//查询缓存中没有想要获取的数据
//开始
String lockKey = "lock_key";
// 1 获取锁。即“占坑”操作
boolean locked = jedis.set(lockKey, "lockValue", "NX") != null;
if (locked) {
try {
//1.1 加锁成功
// 1.1.1 执行业务逻辑,即查询数据库获得数据并返回
} finally {
// 1.1.2 释放锁
jedis.del(lockKey);
}
} else {
// 1.2 未获取锁,处理逻辑
//1.2.1 休眠100ms 为避免重试频率过高
return //1.2.2 返回本方法
//通过返回本方法实现自旋,不断尝试获取锁
}
2 升级简单分布式锁(实现原子加锁与安全删锁)
上述的方法可以实现一个简单的分布式锁,即所有资源抢占时所有服务器排队抢占锁。
2.1 但1中的简单分布式锁存在几个问题:
2.1.1 问题1. 如果加完锁执行业务过程中,还未到释放锁的环节,系统中断,出现死锁的情况,怎么解决?
解决方案:设置锁自动过期时间,且必须在加锁的同时设置过期时间(必须为原子操作,分开操作也可能会导致死锁问题)。
2.2.2 问题2. 业务未完成,锁到期自动释放,但把别人正在持有的锁删除了,应该如何解决(如下图)?
解决方案:加锁时指定value为自己的uuid,释放锁时匹配自己的uuid才能释放。
2.2 针对以上问题我们将上述分布式锁代码升级为如下代码:
//查询缓存中没有想要获取的数据
//开始
String lockKey = "lock_key";
String clientId = UUID.randomUUID().toString(); // 用于标识持有锁的客户端
int lockTimeout = 10; // 锁的过期时间,单位为秒
// 1 获取锁。即“占坑”操作(原子操作设置过期时间,解决问题1)
boolean locked = jedis.set(lockKey, clientId, "NX", "EX", lockTimeout) != null;
if (locked) {
try {
//1.1 加锁成功
// 1.1.1 执行业务逻辑,即查询数据库获得数据并返回
} finally {
// 1.1.2 释放锁
if (clientId.equals(jedis.get(lockKey))) {
//只能删除自己的锁(解决问题2)
jedis.del(lockKey);
}
}
} else {
// 1.2 未获取锁,处理逻辑
//1.2.1 休眠100ms 为避免重试频率过高
return //1.2.2 返回本方法
//通过返回本方法实现自旋,不断尝试获取锁
}
上述代码成功实现了原子加锁与只能删除自己锁的功能,成功解决了前面提出的两个问题。
3. 继续升级分布式锁(实现原子删锁)
3.1 此分布式锁解决了上述的问题1与问题2,但释放锁还存在问题
3.1.1 问题3. 在线程A释放锁时,因是先发送请求获取到锁A的value,对比成功后,删除。若线程A在对比成功以后,删除锁之前,锁刚好自动过期,而另一个线程B抢占到资源,刚好加上锁。则此时线程A刚好执行删除操作则会删除线程B的锁。应该如何解决(如下图)?
解决方案:释放锁即删除锁时执行原子操作,而删除锁无法像加锁时那么简单,此处使用lua脚本实现原子操作删除锁。
// Lua 脚本,用于原子性释放锁
private static final String releaseLockScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
//上述脚本实现功能为 get查询变量KEYS[1]的值是否等于ARGV[1] ,
//如果等则del删除变量KEYS[1],否则结束脚本
然后在代码中执行上述脚本即可完成删除锁的原子操作,因为脚本是一个整体,查询与删除完成或者失败是一体的。
3.2 实现原子删除操作的代码
private static final String lockKey = "lock_key"; // 锁的 Redis key
private static final String clientId = UUID.randomUUID().toString(); // 用于标识持有锁的客户端
private static final int lockTimeout = 10; // 锁超时时间,单位:秒
private Jedis jedis; // Redis 客户端
// Lua 脚本,用于原子性释放锁
private static final String releaseLockScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
public String getDataFromCacheOrDb() {
// 1. 先查询缓存,判断缓存中是否已经有数据
// 2. 缓存未命中,尝试获取分布式锁
boolean locked = jedis.set(lockKey, clientId, "NX", "EX", lockTimeout) != null;
if (locked) {
try {
// 再次检查缓存,避免多个线程在锁之前都没有缓存
// 3. 缓存依然未命中,去数据库中获取数据
// 4. 将数据写入缓存,设置一个过期时间
return dbData;
} finally {
// 5. 释放锁,使用 Lua 脚本确保只有当前持有锁的客户端才能删除锁
jedis.eval(releaseLockScript, Collections.singletonList(lockKey), Collections.singletonList(clientId));
}
} else {
// 6. 如果未获取到锁,等待其他线程完成缓存操作,稍作延迟后再次尝试读取缓存
try {
Thread.sleep(100); // 短暂休眠,等待其他线程完成操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 7. 重新从缓存读取数据
return ;
}
}
4. 完整实现代码
模拟获取热点数据整个流程:
import redis.clients.jedis.Jedis;
import java.util.Collections;
import java.util.UUID;
public class CacheService {
private static final String lockKey = "lock_key"; // 锁的 Redis key
private static final String cacheKey = "hotspot_data"; // 缓存中的 key
private static final String clientId = UUID.randomUUID().toString(); // 用于标识持有锁的客户端
private static final int lockTimeout = 10; // 锁超时时间,单位:秒
private Jedis jedis; // Redis 客户端
// Lua 脚本,用于原子性释放锁
private static final String releaseLockScript =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
public String getDataFromCacheOrDb() {
// 1. 先查询缓存,判断缓存中是否已经有数据
String cachedData = jedis.get(cacheKey);
if (cachedData != null) {
return cachedData; // 缓存命中,直接返回数据
}
// 2. 缓存未命中,尝试获取分布式锁 实现原子加锁
boolean locked = jedis.set(lockKey, clientId, "NX", "EX", lockTimeout) != null;
if (locked) {
try {
// 再次检查缓存,避免多个线程在锁之前都没有缓存
cachedData = jedis.get(cacheKey);
if (cachedData != null) {
return cachedData; // 如果其他线程已经缓存了数据,直接返回
}
// 3. 缓存依然未命中,去数据库中获取数据
String dbData = getDataFromDb(); // 从数据库中获取数据
// 4. 将数据写入缓存,设置一个过期时间
jedis.setex(cacheKey, 60, dbData); // 60秒缓存有效期,实际情况按需求调整
return dbData;
} finally {
// 5. 释放锁,使用 Lua 脚本确保只有当前持有锁的客户端才能删除锁 实现原子删锁
jedis.eval(releaseLockScript, Collections.singletonList(lockKey), Collections.singletonList(clientId));
}
} else {
// 6. 如果未获取到锁,等待其他线程完成缓存操作,稍作延迟后再次尝试读取缓存
try {
Thread.sleep(100); // 短暂休眠,等待其他线程完成操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 7. 重新从缓存读取数据
return jedis.get(cacheKey);
}
}
// 模拟从数据库获取数据
private String getDataFromDb() {
// 实际中,这里会执行数据库查询操作
return "data_from_db";
}
}
经过升级后的最终分布式锁流程图如下:
5. 总结
分布式锁实现过程:
借助 SET 命令实现加分布式锁原子操作:
• SET key value NX EX seconds:
(NX 表示只有当键不存在时才设置值(即,保证了原子性,避免了多个客户端同时获取锁))。
(EX 设置锁的过期时间,防止锁永远不会释放(即死锁情况))。
实现步骤:
1. 尝试使用 SET key value NX EX seconds 来加锁,实现原子操作。
• 如果返回 OK,说明锁获取成功。
• 如果返回 nil,说明锁已经被其他客户端持有,获取失败。
2. 如果成功获取锁,执行关键业务逻辑。
3. 业务完成后,通过lua脚本释放锁,实现原子操作。