分布式锁之一: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.总结

通过以上策略,你可以有效地减少死锁的发生概率,并提高系统的稳定性。确保在设计锁机制时充分考虑执行时间和任务特性,以制定合理的过期时间。

参考:https://www.cnblogs.com/shenlei-blog/p/15597272.html

posted @ 2024-05-30 11:38  先锋之客  阅读(104)  评论(0)    收藏  举报