分布式锁是一种用于多台服务器上处理同一资源的并发访问的锁机制。它用于协调分布式系统中的各个节点,确保它们的操作不会相互干扰。
分布式锁可以应用于如下场景:
分布式锁的必要性在于,在分布式环境下不同的节点同时访问同一资源时容易造成数据冲突和安全问题,使用分布式锁可以有效避免这些问题。
Redis (Remote Dictionary Server)是一种使用 C 语言编写的,开源的 in-memory 数据库系统,支持多种数据结构类型,如字符串、哈希表、列表、集合等。由于其可高效地存储键值对,并且具有多种灵活的使用方式,因此经常被用作缓存、会话存储和消息队列等场景。
Redis 的实现分布式锁的一种基本方式是使用 Redis 命令 SETNX(SET if Not eXists),该命令可以将一个 key-value 键值对设置为与给定值关联,而且只有在该键不存在时才能够设置成功。我们可以通过将 key 设为所需占用的资源名称,并将 value 设置为占用该资源的进程或线程的标识符,来实现分布式锁。
具体实现代码如下:
Jedis jedis = new Jedis("localhost", 6379);
String lockKey = "resource_key";
String requestId = UUID.randomUUID().toString();
int expireTime = 10000;
boolean success = jedis.set(lockKey, requestId, "NX", "PX", expireTime) != null;
以上代码中使用了 Redis 的 Java 客户端 Jedis。首先,我们定义了资源的键值对 key 和要占用该资源的标识符 requestId。然后,我们调用了 Jedis 中的 set 命令,并添加了两个参数 “NX” 和 “PX”,前者表示只有当该锁当前并未被占用时,才能占用该资源;后者表示如果当前请求的锁超时,以毫秒为单位的过期时间应设为 expireTime。
Redisson 是一个基于 Redis 实现的 Java 分布式框架。Redisson 提供了多种分布式锁实现方式:
其中红锁是 Redisson 实现的一种分布式锁算法,其目的是提高 Redis 集群下的分布式锁安全性。
具体实现代码如下:
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
RLock rLock = redissonClient.getLock("resource_key");
try {
boolean lockSuccess = rLock.tryLock(10, 100, TimeUnit.SECONDS);
if (lockSuccess) {
// 获取锁成功,执行业务逻辑
} else {
// 获取锁失败,进行其他处理
}
} finally {
// 释放锁
rLock.unlock();
}
以上代码中首先创建了一个 RedissonClient 对象,然后通过该对象获取了实例化后的 RLock 锁对象。接下来,我们调用了 RLock 中的 tryLock 方法尝试获取锁并等待 10 秒,如果获取锁成功就执行业务逻辑,否则进行其他处理。最后在业务逻辑执行完后要调用 RLock 的 unlock 方法释放锁。
Redis 分布式锁的优点包括:
但是 Redis 分布式锁也存在如下缺点:
Lua 脚本语言是一门轻量级、高效性且可扩展的脚本编程语言,被广泛应用于游戏开发、Web 开发、图形处理等领域。在 Redis 中,也可以使用 Lua 脚本对 Redis 进行操作。
在 Redis 中可以通过 EVAL 命令来执行 Lua 脚本,并通过传递参数来实现对 Redis 的操作。EVAL 命令的具体用法为:
EVAL script numkeys key [key ...] arg [arg ...]
其中script 参数为 Lua 脚本的代码,numkeys 参数为需要传递给 Lua 脚本的键值对中 Key 的数量,key [key …] 是表示需要传递给 Lua 脚本的键值对中的 Key 值,而 arg [arg …] 则是表示需要传递给 Lua 脚本的键值对中 Value 的值。
在 Redis 中可以利用 SETNX 命令实现分布式锁:当一个 Key 的 Value 不存在时,将其设置为需要加锁的值,表示加锁成功;当一个 Key 的 Value 已存在时,表示锁已被其他客户端占用,加锁失败。
但是在分布式环境中由于网络延迟、故障等因素的存在,会导致 SETNX 命令无法保证加锁的正确性。因此,我们需要采用更加复杂的算法来实现 Redis 的分布式锁。
Redlock 算法是一种在分布式环境中实现互斥锁的算法,其基本思路为:在多个 Redis 节点上,针对同一个 Key 同时进行 SETNX 操作,当 SETNX 操作的数量达到一定的条件后,表示锁已被正确地加上。同时,为了防止某个节点挂掉后,锁不能被正常释放,引入了过期时间机制。
下面是基于 Lua 实现的 Redlock 算法代码:
local key = KEYS[1]
local value = ARGV[1]
local ttl = ARGV[2]
local lock_num = tonumber(ARGV[3])
local retry_delay = 5000 -- 重试等待时间,单位 ms
local max_retry_count = 3 -- 最大重试次数
local quorum = math.ceil(lock_num / 2)
math.randomseed(redis.call('TIME')[1])
-- 可以重试的最大次数
local total_retry_count = max_retry_count
while total_retry_count > 0 do
local locked_nodes = 0
local failed_nodes = {}
for _, instance in ipairs(redis.replicas(name)) do
local ok = pcall(function ()
if instance:setnx(key, value) == 1 then
instance:expire(key, ttl)
locked_nodes = locked_nodes + 1
end
end)
if not ok then
table.insert(failed_nodes, instance)
end
if locked_nodes >= quorum then
return true
end
end
for _, node in ipairs(failed_nodes) do
redis.call('del', key)
end
total_retry_count = total_retry_count - 1
if total_retry_count > 0 then
redis.pcall('sleep', retry_delay / 1000.0)
end
end
return false
上述代码中,首先传入了锁的键名 key 和需要加锁的值 value。为了防止某个节点挂掉后锁无法被正常释放,还需要设置过期时间 ttl。由于多个节点同时执行 SETNX 操作,因此还需要传入需要加锁的节点数 lock_num。
在代码的实现中首先定义了重试等待时间 retry_delay,以及最大重试次数 max_retry_count。由于 Redis 集群可能存在网络延迟等因素,可能导致某些节点加锁失败,因此需要进行多次尝试。在每次尝试加锁之前,都会将总的重试次数 total_retry_count 减一,直到达到最大重试次数 max_retry_count 为止。
在每轮尝试中依次遍历所有节点,执行 setnx 操作,并在加锁成功后,为锁设置过期时间。同时,记录加锁成功的节点数,并判断是否达到了需要锁定节点数的一半(即 quorum)。如果当前已经锁定了足够数量的节点,则直接返回 true,表示加锁成功。
在某些情况下,可能会出现加锁失败的情况。此时,需要依次将已经加锁成功的节点解锁。并将加锁失败的节点记录到 failed_nodes 中,等待下一次尝试加锁时进行处理。
最后在每轮完整的尝试中如果仍然无法达到预定的节点数量,则进行睡眠 wait_time 的操作,以延迟加锁操作。当总重试次数 total_retry_count 达到最大值后,退出循环,返回 false,表示加锁失败。
Redlock 算法虽然可以在分布式环境下实现可靠的互斥访问,但同时也存在一些缺陷。比如,如果 Redis 集群中的 master 和 replica 的数量不足,可能会导致加锁失败。因此,在进行实际开发时,需要根据具体的业务场景进行修改和优化。
Lua 脚本实现 Redis 分布式锁的优点在于:
而其缺点则主要有:
Redis 分布式锁的基本原理是利用 Redis 的 SETNX(SET if Not eXists)命令和 EXPIRE 命令,通过 Redis 的单线程特性保证在分布式环境下实现互斥锁的效果。具体来说,当一个客户端请求获取锁时,利用 SETNX 命令尝试往 Redis 中写入一个值作为锁标识,如果 SETNX 成功返回 1,说明这个客户端成功获取了锁;如果返回 0,说明锁已经被其他客户端占用,此时应该重试或者放弃获取锁。为了防止因为某些原因导致持有锁的客户端失联后锁一直得不到释放,需要通过 EXPIRE 命令为锁设置一个过期时间,保证即使持有锁的客户端失联,锁也最终会被自动释放。
利用 Redis 的 EVAL 命令可以执行 Lua 代码,并且 EVAL 命令在 Redis 中被视为一个命令,可以保证在执行 EVAL 命令期间 Redis 不会被其他客户端发送的命令所打断。因此,通过 EVAL 命令执行 Lua 代码可以实现类似 Redis 内置命令一样的原子性操作。利用 EVAL 命令和 Lua 脚本可以很方便地实现 Redis 分布式锁,具体步骤如下:
public class RedisLockUtil {
private static final String LOCK_PREFIX = "lock_";
private static final Long RELEASE_SUCCESS = 1L;
/**
* 获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 过期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String lockValue = LOCK_PREFIX + Thread.currentThread().getId();
String result = jedis.set(lockKey, lockValue, "NX", "EX", expireTime);
if ("OK".equals(result)) {
return true;
}
String currentValue = jedis.get(lockKey);
if (currentValue != null && Long.parseLong(currentValue) < System.currentTimeMillis()) {
String oldValue = jedis.getSet(lockKey, lockValue);
return requestId.equals(oldValue);
}
return false;
}
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String currentValue = jedis.get(lockKey);
if (currentValue != null && requestId.equals(currentValue)) {
Long result = jedis.del(lockKey);
return RELEASE_SUCCESS.equals(result);
} else {
return false;
}
}
}
在使用 Redis 实现分布式锁时,需要注意以下几点:
以下是 Redis 实现分布式锁的最佳实践:
boolean getLock(String key, String value, int expiredTime) {
if (redisClient.setnx(key, value) == 1) {
redisClient.expire(key, expiredTime);
return true;
}
return false;
}
这里采用 Redis 的 setnx 命令来进行加锁,如果返回值为 1 ,表示加锁成功;否则返回 0 ,代表已有其他客户端持有该锁。接着需要设置过期时间,以防止加锁方因故无法释放锁,从而出现死锁。
boolean releaseLock(String key, String value) {
if (redisClient.get(key).equals(value)) {
redisClient.del(key);
return true;
}
return false;
}
释放锁的过程需要保证原子性,应该首先验证当前操作客户端是否是持有该锁的客户端。如果相等,则代表当前可以安全释放这个锁。
以下是分布式锁错误使用:
Redis 是一个内存型的数据存储系统,拥有高性能、可靠性好等特点。而 Lua 是一种轻量级脚本语言,使用方便快捷,并且可以作为 Redis 的扩展语言,用来为 Redis 提供强大的扩展功能。
分布式锁是一种在分布式系统中协调并发进程访问共享资源,避免出现数据不一致等问题的技术。
在 Redis 中分布式锁使用的是基于 Redis 的原子性操作 setnx + expire 组合实现加锁和解锁。而以 Lua 语言为基础实现的分布式锁,它的源码可以在 Redis 运行时进行加载,它不仅本身提供了诸多抽象接口,而且也有用于提供必要依赖的库。
选择 Redis 还是 Lua 实现的分布式锁,需要根据个人情况具体定夺,思考如下几点:
总之Redis 和 Lua 实现的分布式锁各有优劣,应根据具体场景需求综合考虑。