基于 Redis 的分布式锁 Spring Boot 集成 Redisson 使用分布式锁确保对共享资源的互斥访问

目录

前言

SetNX

pom

yml

Controller

存在的问题

可重入的SetNX

Redisson

pom

yml

Controller


前言

工作中开发过一个上传文件的接口,每个区县都有自己的资源压缩包需要上传到系统,系统接收到压缩包后,需要解压,提取出里面的文件保存到文件服务器中,解析里面的SQLITE文件得到数据保存到数据库中。

由于处理的过程会比较耗时,所以使用了异步处理的方式来优化用户体验,接口接收到文件后快速响应,返回上传成功,异步线程在后台继续执行解析压缩包业务逻辑。为了防止在异步线程处理期间,用户再次上传压缩包,从而导致上传资源数据不一致问题,在异步线程处理期间要获取锁来保证上传资源数据一致。由于项目的架构是微服务架构,所以需要使用分布式锁

项目中有使用 Redis,所以可以基于 Redis 来实现分布式锁,利用 Redis 的 SetNX便能够简单实现分布式锁了。

SetNX

使用 Spring 框架中的 RedisTemplate 客户端,通过它可以对Redis进行多种操作

pom

XML 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--spring2.0集成redis所需common-pool2-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

yml

application.yml

复制代码
spring:
  redis:
    host: localhost
    password: ''
    port: 6379
    timeout: 10000
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        max-wait: -1
        min-idle: 0

server:
  port: 8888

Controller

以下是一个简单的 RedisTemplate 使用 SetNX 的简单示例

java 复制代码
@RestController
@RequestMapping("/redis")
public class RedisTemplateController {

    @Resource
    RedisTemplate<String, String> redisTemplate;

    @GetMapping("/setNX")
    public String setNX(String key) {
        Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 60, TimeUnit.SECONDS);
        if (!Boolean.TRUE.equals(flag)) {
            return "请稍后再试";
        }

        try {
            System.out.println("processing");
            Thread.sleep(4 * 1000);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        } finally {
            redisTemplate.delete(key);
        }

        return "成功";
    }
}

存在的问题

使用 Redis + RedisTemplate,可以非常简单高效地实现分布式锁,针对绝大部分小系统已经够用了,因为大部分系统终其一生都遇不到那些极端情况,就算遇到了也是重启大法解决一切。。。

存在以下的问题:

  1. 锁无法续期:如果为锁设置了过期时间,万一超过了过期时间程序还没有执行完,而锁就被释放了,程序就可能出错了。
  2. 锁永不释放:如果不设置过期时间,那么如果客户端崩溃,那么该分布式锁就永远不会被释放了。
  3. 不可重入:同一个线程无法重复获取到锁,导致发生死锁。

可重入的SetNX

直接使用 SetNX 会存在以上的问题,不可重入的问题,实际上可以通过线程ID来标识锁,在加锁的时候,将唯一的线程ID也保存进去,这样在判断锁的时候,就可以通过线程ID来判断该分布式锁是不是属于当前线程的了,如果是,则重入数+1

示例代码如下:

java 复制代码
public class RedisLockUtil {

    public static synchronized boolean lock(RedisTemplate<String, Serializable> redisTemplate, String key) {
        String currentThreadId = String.valueOf(Thread.currentThread().getId());
        String lockValue = (String) redisTemplate.opsForValue().get(key);
        if (lockValue == null) {
            lockValue = String.format("%s:%s", currentThreadId, 1);
            redisTemplate.opsForValue().set(key, lockValue);
            return true;
        }
        String[] parts = lockValue.split(":");
        if (parts.length == 2 && parts[0].equals(currentThreadId)) {
            int count = Integer.parseInt(parts[1]) + 1;
            lockValue = String.format("%s:%s", currentThreadId, count);
            redisTemplate.opsForValue().set(key, lockValue, 60, TimeUnit.SECONDS);
            return true;
        }

        return false;
    }

    public static synchronized void unlock(RedisTemplate<String, Serializable> redisTemplate, String key) {
        String currentThreadId = String.valueOf(Thread.currentThread().getId());

        String lockValue = (String) redisTemplate.opsForValue().get(key);
        if (lockValue != null) {
            String[] parts = lockValue.split(":");
            if (parts.length == 2 && parts[0].equals(currentThreadId)) {
                int count = Integer.parseInt(parts[1]);
                if (count > 1) {
                    lockValue = String.format("%s:%s", currentThreadId, count - 1);
                    redisTemplate.opsForValue().set(key, lockValue, 60, TimeUnit.SECONDS);
                } else {
                    redisTemplate.delete(key);
                }
            }
        }
    }

}

这也只是解决了可重入的问题,还是存在锁续期,锁永远不会被释放问题。

Redisson

解决以上问题,一劳永逸的方式就是直接使用 Redisson ,Redisson 的 lock 通过 Watchdog机制,解决了锁续期的问题。

在没有指定过期时间的前提下,Redisson 客户端实例获取到一个分布式锁,Watchdog 机制基于 Netty 的时间轮启动一个后台任务,定期向Redis发送续期命令,重新设置锁的过期时间,默认续期30秒,每10秒做一次续期。

当分布式锁释放或者 Redisson 客户端关闭时,Watchdog 也停止锁的续期任务。这样就完美地保证程序执行期间获取到的锁不会提前释放。即使客户端崩溃,也不会出现锁永远不会被释放的情况,因为客户端崩溃,Watchdog 也一起停止了续期任务,过了过期时间,锁自己就释放了。

pom

XML 复制代码
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.11.4</version>
        </dependency>

yml

application.yml

复制代码
spring:
  redis:
    redisson:
      config: "classpath:redisson.yml"

server:
  port: 8888

redisson.yml

{

"singleServerConfig":{

"idleConnectionTimeout":10000,

"pingTimeout":1000,

"connectTimeout":10000,

"timeout":3000,

"retryAttempts":3,

"retryInterval":1500,

"subscriptionsPerConnection":5,

"clientName":null,

"address": "redis://localhost:6379",

"subscriptionConnectionMinimumIdleSize":1,

"subscriptionConnectionPoolSize":50,

"connectionMinimumIdleSize":32,

"connectionPoolSize":64,

"database":0

},

"threads":0,

"nettyThreads":0,

"codec":{

"class":"org.redisson.codec.JsonJacksonCodec"

},

"transportMode":"NIO"

}

Controller

java 复制代码
@RestController
@RequestMapping("/redisson")
public class RedissonLockController {

    @Resource
    private RedissonClient redissonClient;

    @GetMapping("/lock")
    public String reentrantLock(String key) {
        RLock reentrantLock = redissonClient.getLock(key);
        try {
            if (!reentrantLock.tryLock()) {
                return "请稍后再试";
            }

            System.out.println("processing");
            Thread.sleep(4 * 1000);
        } catch (Exception e) {
            reentrantLock.unlock();
        } finally {
            reentrantLock.unlock();
        }
        return "成功";
    }
}
相关推荐
KK溜了溜了18 分钟前
JAVA-springboot log日志
java·spring boot·logback
我命由我123451 小时前
Spring Boot 项目集成 Redis 问题:RedisTemplate 多余空格问题
java·开发语言·spring boot·redis·后端·java-ee·intellij-idea
面朝大海,春不暖,花不开1 小时前
Spring Boot消息系统开发指南
java·spring boot·后端
hshpy1 小时前
setting up Activiti BPMN Workflow Engine with Spring Boot
数据库·spring boot·后端
jay神2 小时前
基于Springboot的宠物领养系统
java·spring boot·后端·宠物·软件设计与开发
不知几秋2 小时前
Spring Boot
java·前端·spring boot
篱笆院的狗2 小时前
如何使用 Redis 快速实现布隆过滤器?
数据库·redis·缓存
G探险者3 小时前
《深入理解 Nacos 集群与 Raft 协议》系列五:为什么集群未过半,系统就不可用?从 Raft 的投票机制说起
分布式·后端
G探险者3 小时前
《深入理解 Nacos 集群与 Raft 协议》系列一:为什么 Nacos 集群必须过半节点存活?从 Raft 协议说起
分布式·后端
howard20053 小时前
5.4.2 Spring Boot整合Redis
spring boot·整合redis