分布式锁之一:Redis实现分布式锁
分布式锁之一:Redis实现分布式锁
分布式锁一般有三种实现方式:1、基于数据库乐观锁;2、基于Redis的分布式锁;3、基于Zookeeper的分布式锁。本文档主要介绍基于Redis实现分布式锁的方法。
1、加锁
// redis加锁
public boolean getLock(Jedis jedis,String key,int expire){
String uuid =UUID.randomUUID().toString();
String result = jedis.set(key,uuid,"NX","PX",expire);
if(Objects.equals("OK",result)){
logger.info("redis加锁成功");
return true;
}else{
logger.info("redis锁已存在,加锁失败");
return false;
}
}
- 第一个为key,我们使用key来当锁,因为key是唯一的。
- 第二个为value,使用uuid便于解锁的时候确保解的是同一把锁。
- 第三个为nxxx,这个参数我们填的是NX,
- 第四个为nxxx,这个参数我们填的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
- 第五个为time,与第四个参数相呼应,代表key的过期时间。
2.执行业务流程
public void doTask(){
//处理业务逻辑
}
3.解锁
//redis解锁
public boolean unLock(Jedis jedis,String key,String uuid){
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
long res = redisClient.executeCmd( jedis -> jedis.eval(script,Collections.singleonList(RedisConstans.CLOSE_LEAVE_USER),Collections.singletonList(uuid)));
if(Objects.equals(1L,res)){
logger.info("redis解锁成功");
return true;
}else{
logger.info("redis解锁失败")
}
}
- 使用lua脚本解锁,把它变成原子操作,保证锁的释放正确。
但是以上代码还是存在问题的,会产生续约和集群同步延迟问题。
续约问题
假想这样一个场景,如果过期时间为30S,A线程超过30S还没执行完,但是自动过期了。这时候B线程就会再拿到锁,造成了同时有两个线程持有锁
这时候就要考虑延长锁的过期时间了。可以设置一个合理的过期时间,保证业务能处理完。或者使用Redisson。
集群同步延迟问题
用于redis的服务肯定不能是单机,因为单机就不是高可用了,一量挂掉整个分布式锁就没用了。
在集群场景下,如果A在master拿到了锁,在没有把数据同步到slave时,master挂掉了。B再拿锁就会从slave拿锁,而且会拿到。又出现了两个线程同时拿到锁。
Redisson
Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。
Redisson通过lua脚本解决了上面的原子性问题,通过“看门狗”解决了续约问题,但是它应该解决不了集群中的同步延迟问题。
总结
redis分布式锁的方案,无论用何种方式实现都会有续约问题与集群同步延迟问题。总的来说,是一个不太靠谱的方案。如果追求高正确率,不能采用这种方案。
但是它也有优点,就是比较简单,在某些非严格要求的场景是可以使用的,比如社交系统一类,交易系统一类不能出现重复交易则不建议用。
在Spring Boot中使用Redis实现分布式锁
这种机制可以有效地控制对共享资源的访问,避免数据竞争和不一致的问题。
步骤 1: 添加依赖
确保在你的pom.xml中添加了Spring Data Redis相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce.core</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
步骤 2: 配置Redis连接
在application.properties或application.yml文件中配置Redis的连接信息:
spring.redis.host=localhost
spring.redis.port=6379
步骤 3: 创建分布式锁工具类
创建一个工具类来处理分布式锁的获取和释放:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisLock {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean tryLock(String lockKey, String requestId, long timeout) {
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, timeout, TimeUnit.SECONDS);
return success != null && success;
}
public void unlock(String lockKey, String requestId) {
String value = redisTemplate.opsForValue().get(lockKey);
if (requestId.equals(value)) {
redisTemplate.delete(lockKey);
}
}
}
步骤 4: 使用分布式锁
在你的服务或控制器中,可以通过RedisLock类来获取和释放锁。以下是一个示例:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class MyService {
@Autowired
private RedisLock redisLock;
private static final String LOCK_KEY = "myLock";
public void performTask() {
String requestId = String.valueOf(System.currentTimeMillis()); // 使用当前时间戳作为请求ID
// 尝试获取锁
if (redisLock.tryLock(LOCK_KEY, requestId, 10)) { // 10秒超时
try {
// 执行需要加锁的操作
System.out.println("Lock acquired! Performing task...");
Thread.sleep(5000); // 模拟任务执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// 释放锁
redisLock.unlock(LOCK_KEY, requestId);
System.out.println("Lock released!");
}
} else {
System.out.println("Could not acquire lock, please try again later.");
}
}
}
步骤 5: 测试分布式锁
你可以在控制器中调用MyService的performTask方法进行测试:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyController {
@Autowired
private MyService myService;
@GetMapping("/test-lock")
public String testLock() {
myService.performTask();
return "Task initiated!";
}
}
步骤 6: 启动应用并测试
启动你的Spring Boot应用,并访问以下URL来测试分布式锁功能:
http://localhost:8080/test-lock
注意事项
- 过期时间:为了避免死锁,确保在设置锁时指定合理的过期时间。
- 锁的唯一性:在分布式环境中,锁的唯一性非常重要,建议使用唯一的lockKey和requestId。
- 异常处理:确保在任务执行过程中正确处理异常,并在finally块中释放锁。
为了避免死锁,如何确保在设置锁时指定合理的过期时间
为了避免死锁,确保在设置锁时指定合理的过期时间,可以遵循以下几个原则:
1. 分析任务执行时间
在设置锁的过期时间时,首先应分析被锁定操作的最长执行时间。可以通过历史监控数据或日志分析来获取这个信息。
2. 设置适当的过期时间
将锁的过期时间设置为预计的最大执行时间加上一些安全边际。例如,如果你预计某个操作通常需要3秒,设置锁的过期时间为5秒或6秒。
3. 使用可重入锁
如果你的操作可能会重入(即同一个线程可能会多次尝试获取同一把锁),考虑使用可重入锁。这种锁允许同一个请求在持有锁的情况下再次获取锁,而不会导致死锁。
4.动态调整过期时间
在执行任务的过程中,可以定期检查锁的状态,并在必要时动态延长锁的过期时间。这样可以防止因为任务执行时间超过预期而导致的锁过期。
5. 监控与报警
实施监控措施,跟踪锁的使用情况,并在发现锁频繁超时或未释放的情况时报警。这有助于及时发现问题并进行处理。
6. 日志记录与分析
7.动态调整过期时间示例代码
我们可以添加动态调整过期时间的逻辑。以下是一个简单的实现:
public boolean tryLock(String lockKey, String requestId, long timeout) {
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, timeout, TimeUnit.SECONDS);
return success != null && success;
}
// 动态调整锁的过期时间
public void extendLock(String lockKey, String requestId, long extraTime) {
String value = redisTemplate.opsForValue().get(lockKey);
if (requestId.equals(value)) {
redisTemplate.expire(lockKey, extraTime, TimeUnit.SECONDS);
}
}
8.总结
通过以上策略,你可以有效地减少死锁的发生概率,并提高系统的稳定性。确保在设计锁机制时充分考虑执行时间和任务特性,以制定合理的过期时间。

浙公网安备 33010602011771号