基于 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 "成功";
    }
}
相关推荐
赵丙双19 分钟前
猜测、实现 B 站在看人数
redis·redisson·b站·哔哩哔哩·在看人数·在线人数
sco52821 小时前
【Shiro】Shiro 的学习教程(三)之 SpringBoot 集成 Shiro
spring boot·后端·学习
鹿子铭4 小时前
单线程Redis:Redis为什么这么快
数据库·redis
爱摄影的程序猿5 小时前
JAVA springboot面试题今日分享
java·spring boot·spring·面试
爱打lan球的程序员7 小时前
redis分布式锁和lua脚本
数据库·redis·分布式
说书客啊7 小时前
计算机毕业设计 | springboot旅行旅游网站管理系统(附源码)
java·数据库·spring boot·后端·毕业设计·课程设计·旅游
愿尽7 小时前
JavaWeb【day11】--(SpringBootWeb案例)
java·spring boot
正在绘制中7 小时前
如何部署Vue+Springboot项目
前端·vue.js·spring boot
张乔248 小时前
启动spring boot项目时,第三方jar包扫描不到的问题
spring boot
ahauedu8 小时前
Spring Boot3项目的常见通用整体架构
spring boot·后端·架构