Redis 分布式锁的实现原理
Redis 分布式锁通常利用 Redis 提供的原子操作来实现。最常用的方式是使用 SET 命令的某些选项,如 NX(Not eXists,意为"仅当键不存在时")和 EX(Expire,意为"设置键的过期时间")。以下是 Redis 实现分布式锁的基本步骤:
- 客户端 A 想要获得一个锁,它会生成一个随机的唯一值作为锁的值(通常是一个随机的 UUID),然后用 SET 命令尝试设置一个 key,例如 "lock_key"。客户端将使用 NX 选项确保只有在 key 不存在时才进行设置操作,并使用 EX 选项设置一个过期时间,以避免死锁。
sql
SET lock_key random_value NX EX 10
这个命令尝试将 "lock_key" 的值设置为 "random_value",仅当 "lock_key" 不存在时才设置,并且设置 "lock_key" 10秒后过期。
-
如果 SET 操作成功,客户端 A 获得了锁,并且可以执行它的操作。在这个期间,任何其他客户端尝试获取同一个锁将会失败。
-
客户端 A 在执行完它的操作后,会删除这个锁。这里有一个需要注意的地方,为了安全地释放锁,客户端 A 需要先确认锁内的值是它最初设置的值(即 "random_value"),然后再删除。这需要使用 Lua 脚本来原子地执行这两个操作。
lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
这个 Lua 脚本确保只有设置了锁的客户端可以释放这个锁。
- 如果客户端 A 在它的锁过期之前未能释放锁(比如因为客户端崩溃或操作耗时太长等原因),锁将自动过期,从而允许其他客户端可以获得锁。这样保证了即使发生异常情况,锁资源也不会被永久占用。
要注意的是,上述方法依赖于时间的一致性,因此在分布式系统中可能会有风险,特别是在时间不同步或网络延迟的情境下。因此,有一些更复杂的算法被设计出来提高分布式锁的安全性及可靠性,比如 Redlock 算法。
Redlock 算法
Redlock 这种方法会用到多个 Redis 实例来降低单点故障的风险。客户端尝试在多个实例上依次获取锁,并且只有在大多数实例上成功获取锁时,才认为整个锁定操作成功。不过 Redlock 算法的有效性和安全性在社区中存在争议,工程应用时需要进行严格的评估。
Redis 官方推荐的 Redisson 客户端库
此外,Redis 官方推荐使用 Redisson 这个 Java 客户端库,它提供了成熟的分布式锁功能,包括重试机制、锁的自动续期等。
总结
利用 Redis 提供的基本原子操作可以实现简单的分布式锁,但对于需要更高稳定性和安全性的场景,可能需要更复杂的算法或库支持。在分布式系统中实现锁机制总是需要细心设计和严格测试的。
java
package org.middleware.redis.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @author 周康(周康)
* @date 2024/3/11 16:14
*/
@Slf4j
@Component
public class RedisLock {
private static final String KEY_PRE = "{WF_RLock}_%s";
//占用锁的最大时间 单位 秒
private static final int LOCK_HOLDING_MAX_SECONDS = 4;
// 线程休息时间 单位 毫秒
private static final int LOCK_RETRY_INTERVAL = 50;
private static final String UNLOCK_LUA_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
/**
* 锁定库存的脚本,是保证setnx和 expire操作的原子
*/
//@formatter:off
private static final String LOCK_LUA_SCRIPT =
"local flag = redis.call('setnx',KEYS[1],0); " +
"if(flag == 1) then " +
"local maxtime = tonumber(ARGV[1]); " +
"redis.call('expire',KEYS[1],maxtime) " +
"end ; " +
"return tonumber(flag) == 1;";
//@formatter:on
private static final String LOCK_LUA_VALUE_SCRIPT =
"local flag = redis.call('setnx',KEYS[1],KEYS[2]); " +
"if(flag == 1) then " +
"local maxtime = tonumber(ARGV[1]); " +
"redis.call('expire',KEYS[1],maxtime) " +
"end ; " +
"return tonumber(flag) == 1;";
private final RedisTemplate<String, Object> redisTemplate;
public RedisLock(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 尝试获取锁,会在等待时长里每隔50ms轮询再次尝试获取锁
*
* @param lockKey lockKey
* @param maxWaitingMillis 最大等待时间 毫秒, <=0,则不进行尝试
*/
public boolean tryLock(String lockKey, int maxWaitingMillis) {
if (maxWaitingMillis <= 0) {
if (this.lock(lockKey)) {
return true;
} else {
log.debug("获取锁失败,lockKey:{}", lockKey);
return false;
}
}
long curTime = System.currentTimeMillis();
while ((System.currentTimeMillis() - curTime) < TimeUnit.MILLISECONDS.toMillis(maxWaitingMillis)) {
try {
if (this.lock(lockKey)) {
return true;
}
TimeUnit.MILLISECONDS.sleep(LOCK_RETRY_INTERVAL);
} catch (InterruptedException e) {
log.debug("获取锁失败,lockKey:{}", lockKey, e);
}
}
return false;
}
/**
* 尝试获取锁,会在等待时长里每隔50ms轮询再次尝试获取锁
*
* @param lockKey lockKey
* @param maxWaitingMillis 最大等待时间 毫秒, <=0,则不进行尝试
* @param value 所锁定的值
*/
public boolean tryLock(String lockKey, int maxWaitingMillis, String value) {
if (maxWaitingMillis <= 0) {
if (this.lock(lockKey, value)) {
return true;
} else {
log.debug("获取锁失败,lockKey:{}", lockKey);
return false;
}
}
long curTime = System.currentTimeMillis();
while ((System.currentTimeMillis() - curTime) < TimeUnit.MILLISECONDS.toMillis(maxWaitingMillis)) {
try {
if (this.lock(lockKey, value)) {
return true;
}
TimeUnit.MILLISECONDS.sleep(LOCK_RETRY_INTERVAL);
} catch (InterruptedException e) {
log.debug("获取锁失败,lockKey:{}", lockKey, e);
}
}
return false;
}
/**
* 获取锁,获取成功,返回true,否则返回false
*/
public boolean lock(String lockKey) {
Boolean locked = redisTemplate.execute(
new DefaultRedisScript<>(LOCK_LUA_SCRIPT, Boolean.class),
Collections.singletonList(String.format(KEY_PRE, lockKey)),
LOCK_HOLDING_MAX_SECONDS);
return locked != null && locked;
}
/**
* 获取锁,并锁定值 ,获取成功,返回true,否则返回false
*/
public boolean lock(String lockKey, String value) {
Boolean locked = redisTemplate.execute(
new DefaultRedisScript<>(LOCK_LUA_VALUE_SCRIPT, Boolean.class),
Arrays.asList(String.format(KEY_PRE, lockKey), value),
LOCK_HOLDING_MAX_SECONDS);
return locked != null && locked;
}
public void releaseLock(String lockKey) {
redisTemplate.delete(String.format(KEY_PRE, lockKey));
}
/**
* 进行释放锁,当锁定的值与所需要释放的值相等时才进行释放
*/
public void releaseLock(String lockKey, String value) {
redisTemplate.execute(
new DefaultRedisScript<>(UNLOCK_LUA_SCRIPT, Boolean.class),
Collections.singletonList(String.format(KEY_PRE, lockKey)),
value);
}
}