06-Redis 实现分布式锁和解决锁续期问题

本文深入探讨了分布式锁的概念及其在高并发场景中的应用。首先介绍了分布式锁的必要性,并详细阐述了其实现所需的四个关键条件。随后,以Redis为例,讲解了如何使用SET命令加锁及Lua脚本安全地释放锁。最后,介绍了Redisson客户端如何实现锁的自动续期功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一 为什么需要分布式锁

二 分布式锁实现的条件

三 Redis 实现分布式锁

3.1 加锁

3.2 释放锁

四 分布式锁续期实现原理

五 redisson 实现分布式锁

5.1 引入 pom 依赖

5.2 代码实现


一 为什么需要分布式锁

在一些高并发的场景中,需要对共享变量进行互斥访问。也就是说在同一时间点有且只能有一个客户端能对共享资源进行访问。如电商系统中的秒杀系统等。

二 分布式锁实现的条件

为了保证分布式锁的可用性,我们要保证分布式锁的实现过程中必须满足以下四个条件:

1 互斥性:在任意时刻,有且只有一个客户端能持有锁。

2 死锁:获取锁的客户端因为某些原因而宕机,而未能释放锁,其他客户端无法获取此锁,需要有机制来避免该类问题的发生。

3 容错性:只要大部分的Redis节点正常运行,客户端就可以加锁和释放锁。

4 安全性:加锁和释放锁必须是同一个客户端,客户端自己不能将别的客户端加的锁给释放了。

三 Redis 实现分布式锁

3.1 加锁

SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX second :设置键的过期时间为 second 秒。
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。
  • NX :只在键不存在时,才对键进行设置操作。
  • XX:只在键已经存在时,才对键进行设置操作。
  • SET 操作成功完成时,返回 OK ,否则返回 nil。

伪代码实现:

public class DistributedLock {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    @Resource 
    RedisCluster redisCluster;
    public static boolean lock(String key, String requestId, int expireTime) {
        String result = redisCluster.set(key, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

代码参数解释:

  • key:与业务相关,具有唯一性。
  • requestId:客户端通过 requestId 加锁,同时该客户端必须通过 requestId 释放锁。
  • SET_IF_NOT_EXIST:即当 key 不存在时,我们进行 set 操作;若 key 已经存在,则不做任何操作。
  • SET_WITH_EXPIRE_TIME:给这个 key 加一个过期时间的设置。
  • expireTime:key 的过期时间。

3.2 释放锁

-- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

伪代码实现

public class DistributedLock {
    @Resource 
    RedisCluster redisCluster;
    public static boolean unLock(String key, String requestId) {
        String script = "if redisCluster.call('get', KEYS[1]) == ARGV[1] then return redis.redisCluster('del', KEYS[1]) else return 0 end";
        Object result = redisCluster.eval(script, Collections.singletonList(key), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}

那么为什么要使用 Lua 语言来实现呢?

因为要确保上述 get 和 del 操作是原子性的。如果调用 redisCluster.del()方法的时候,如果这把锁已经不属于当前客户端的话,del 操作会释放其他客户端加的锁。

比如客户端 A 加锁,一段时间之后客户端A 释放锁,在执行 redisCluster.del() 之前,锁突然过期了,此时客户端 B 尝试加锁成功,然后客户端 A 再执行 del() 方法,则将客户端 B 的锁给释放了。

四 分布式锁续期实现原理

这块有一个异常情况,假设我们给锁设置的过期时间太短,业务还没执行完成,锁就过期了,这块应该如何处理呢?

如何给分布式锁续期?

redisson 是 Redis 的一个客户端,它能给 Redis 分布式锁实现过期时间自动续期。

redisson 如何实现过期时间自动续期?

redisson 加锁成功后,会单独创建一个线程来监视这个锁。假设我们锁的过期时间是3s,这个线程在1s的时候就会查看当前的锁是否释放,如果没有释放,将过期时间再次设置成3s。

这个相当于是一个定时任务,每隔过期时间的1/3就会来确认一次锁是否释放,没有释放就续期。这个是从源码查看的,这块就暂时不深究了,源码看这(拜托,面试请不要再问我Redis分布式锁的实现原理【石杉的架构笔记】)。

五 redisson 实现分布式锁

5.1 引入 pom 依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.8.2</version>
</dependency>

5.2 代码实现

Config config = new Config();
config.useClusterServers()
    .setScanInterval(2000) // cluster state scan interval in milliseconds
    .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
    .addNodeAddress("redis://127.0.0.1:7002");

RedissonClient redisson = Redisson.create(config);

RLock lock = redisson.getLock("anyLock");

lock.lock();

lock.unlock();

/docs/reference/patterns/distributed-locks/

https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers

### 分布式锁续期机制的解决方案 在 Java 中使用 Redis 实现分布式锁时,续期(也称为锁的延期)是一个关键问题。续期的主要目的是防止因锁过期而导致的误操作或数据不一致。以下是关于如何实现分布式锁续期机制的详细说明。 #### 1. **续期的核心原理** 分布式锁的续期通常通过定期更新锁的过期时间来实现。具体来说,在加锁成功后,客户端会启动一个定时任务或线程,周期性地调用 Redis 的 `EXPIRE` 命令延长锁的有效期[^3]。例如: ```java // 定义续期方法 public void renewLock(String lockKey, long expireTimeInSeconds) { redisTemplate.expire(lockKey, expireTimeInSeconds, TimeUnit.SECONDS); } ``` 这种方法需要确保以下几点: - 续期操作必须在锁的有效期内完成。 - 必须保证续期操作的原子性,以避免其他客户端错误地延长了不属于自己的锁。 #### 2. **基于 Lua 脚本的原子续期** 为了提高安全性,可以使用 Redis 的 Lua 脚本来实现原子性的续期操作。Lua 脚本能够在 Redis 服务器端一次性执行多个命令,从而避免网络延迟带来的不确定性。以下是一个示例 Lua 脚本: ```lua if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("EXPIRE", KEYS[1], ARGV[2]) else return 0 end ``` 在 Java 中调用该脚本的方式如下: ```java public boolean renewLockWithLua(String lockKey, String lockValue, int expireTimeInSeconds) { DefaultRedisScript<Long> script = new DefaultRedisScript<>(); script.setScriptText( "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('EXPIRE', KEYS[1], ARGV[2]) else return 0 end" ); script.setResultType(Long.class); Long result = redisTemplate.execute(script, Collections.singletonList(lockKey), lockValue, expireTimeInSeconds); return result != null && result > 0; } ``` 这种方式确保了只有持有正确锁值的客户端才能续期,从而避免了潜在的竞争条件问题[^4]。 #### 3. **心跳机制** 另一种常见的续期方式是通过心跳机制实现。客户端在获取锁后,会启动一个后台线程,每隔一段时间检查锁是否仍然有效,并尝试续期。如果发现锁已经丢失,则停止续期并释放资源。 ```java public class LockHeartbeat implements Runnable { private final String lockKey; private final String lockValue; private final RedisTemplate<String, String> redisTemplate; public LockHeartbeat(String lockKey, String lockValue, RedisTemplate<String, String> redisTemplate) { this.lockKey = lockKey; this.lockValue = lockValue; this.redisTemplate = redisTemplate; } @Override public void run() { while (true) { try { Thread.sleep(5000); // 每隔5秒检查一次 if (redisTemplate.opsForValue().get(lockKey).equals(lockValue)) { redisTemplate.expire(lockKey, 30, TimeUnit.SECONDS); // 续期30秒 } else { break; // 锁已丢失,退出心跳 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } } ``` 这种方式的优点在于灵活性较高,但需要注意线程管理以及异常处理。 #### 4. **Redlock 算法中的续期** 如果使用 Redlock 算法实现分布式锁,则可以通过类似的方式对锁进行续期。由于 Redlock 需要在多个 Redis 节点上同时获取锁,因此续期时也需要对所有节点执行续期操作。以下是伪代码示例: ```java public boolean renewRedlock(List<RedisClient> redisClients, String lockKey, String lockValue, int expireTimeInSeconds) { int successCount = 0; for (RedisClient client : redisClients) { if (renewLockWithLua(client, lockKey, lockValue, expireTimeInSeconds)) { successCount++; } } return successCount >= redisClients.size() * 0.5 + 1; // 多数节点续期成功 } ``` 这种方式确保了即使部分节点失效,只要大多数节点续期成功,锁依然有效[^2]。 ### 注意事项 - 续期频率应小于锁的过期时间,通常设置为过期时间的一半。 - 在高并发场景下,需注意避免因网络延迟或 Redis 故障导致的续期失败。 - 如果使用 Redisson 等第三方库,则无需手动实现续期逻辑,因为这些库已经内置了完善的续期机制[^3]。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值