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

目录

前言

SetNX

pom

yml

Controller

存在的问题

可重入的SetNX

Redisson

pom

yml

Controller


前言

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

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

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

SetNX

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

pom

        <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 的简单示例

@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

示例代码如下:

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

        <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

@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 "成功";
    }
}
  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值