Redis学习(八)Java三种方式实现分布式锁

一、背景

在分布式服务中,经常有例如定时任务这样的场景。

在定时任务中,如果不使用 quartz 这样的分布式定时工具,只是简单使用 @Schedule 注解来实现定时任务,在服务分布式部署中 ,就有可能存在定时任务并发重复执行问题

对于解决以上场景中的问题,我们引入了分布式锁

二、具体实现

1.RedisTemplate 实现(非阻塞)

RedisUtils 工具类:

java 复制代码
@Component
public class RedisUtils {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 原子性操作 加锁
     */
    public boolean setIfAbsent(String key, String value, long timeout, TimeUnit unit) {
        return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit));
    }

    /**
     * 原子性操作 解锁
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }
}

使用示例:

java 复制代码
@Resource
private RedisUtils redisUtils;

public void updateUserWithRedisLock(SysUser sysUser) throws InterruptedException {
    // 1.获取分布式锁
    boolean lockSuccess = RedisUtils.setIfAbsent("SysUserLock" + sysUser.getId(), "value", 30, TimeUnit.SECONDS);
    if (lockSeccess) {
        // 加锁成功...
        // TODO: 业务代码

        // 释放锁
        redisUtils.delete("SysUserLock" + sysUser.getId());
    } else {
        // 如果需要阻塞的话,就睡一段时间再重试
        Thread.sleep(100);
        updateUserWithRedisLock(sysUser);
    }
}

setIfAbsent() 方法的作用就是在 lock key 不存在的时候,才会设置值并返回 true;如果这个 key 已经存在了就返回 false,即获取锁失败。slefAbsen


2.RedisLockRegistry 实现(阻塞)

RedisLockRegistryspring-integration-redis 中提供的 Redis 分布式锁实现类。

集成 spring-integration-redis:

xml 复制代码
<dependency>
	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-interation</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.integration</groupId>
    <artifactId>spring-integration-redis</artifactId>
</dependency>

注册RedisLockRegistry:

java 复制代码
@Configuration
public class RedisLockConfig {
    
    @Bean
    public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactor) {
        // 第一个参数 redisConnectionFactory
        // 第二个参数 registryKey,分布式锁前缀,建议设置项目名称
        // 该构造方法对应的分布式锁,默认有效期是60秒,可以自定义。
        return new RedisLockRegistry(redisConnectionFactory, "boot-launch");
        // return new RedisLockRegistry(redisConnectionFactory, "boot-launch", 60);
    }
}

使用RedisLockRegistry:

代码实现:

java 复制代码
@Resource
private RedisLockRegisty redisLockRegistry;

public void updateUser(String userId) {
    String lockKey = "config" + userId;
    Lock lock = redisLockRegistry.obtain(lockKey); // 获取锁资源
    try {
        lock.lock(); // 加锁
        
        // TODO: 业务代码
    } finally {
        lock.unlock(); // 释放锁
    }
}

注解实现:

java 复制代码
@RedisLock("lock-key")
public void save() {
    
}

RedisLockRegistry 实现的分布式锁是支持阻塞的。RedisLocckRegistry 是 Spring Integration Redis 模块中的一个类,它是基于 Redis 实现的分布式锁机制。

当一个线程尝试获取分布式锁时,如果该锁已经被其他线程占用,则当前线程会被阻塞,等待锁释放。阻塞的线程会一直等待,直到锁被成功获取或者等待超时。

阻塞的实现是通过 Redis 的特性实现的:通过 Redis 的 SETNX 命令(SET if Not eXists)尝试将锁的键值对设置到 Redis 中。如果设置成功,则表示获取锁成功;如果设置失败,则表示锁已被其他线程占用,当前线程会被阻塞。

因此,使用 RedisLockRegistry 可以实现支持阻塞的分布式锁机制,能够实现多线程之间的协调和互斥。


3.Redisson 实现(阻塞)

Redission 是一个独立的 Redis 客户端,是与 Jedis、Lettuce 同级别的存在。

集成 Redisson:

xml 复制代码
<dependency>
	<groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.15.0</version>
    <exclusions>
    	<exclusion>
        	<groupId>org.redisson</groupId>
            <!-- 默认是 Spring Data Redis v.2.3.x 所以排除掉 -->
            <artifactId>redisson-spring-data-23</artifactId>
        </exclusion>
    </exclusions>
</dependency>

配置:

1)在配置文件中添加如下内容:

yaml 复制代码
spring:
  redis:
    redisson:
      file: classpath:redisson.yaml

2)然后新建一个 redisson.yaml 文件,也放在 resources 目录下:

yaml 复制代码
singleServerConfig:
  idleConnectionTimeout: 10000
  connectTimeout: 10000
  timeout: 3000
  retryAttempts: 3
  retryInterval: 1500
  password: 123456
  subscriptionsPerConnection: 5
  clientName: null
  address: "redis://192.168.161.3:6379"
  subscriptionConnectionMinimumIdleSize: 1
  subscriptionConnectionPoolSize: 50
  connectionMinimumIdleSize: 32
  connectionPoolSize: 64
  database: 0
  dnsMonitoringInterval: 5000
threads: 0
nettyThreads: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}
transportMode: "NIO"

代码实现:

java 复制代码
@Resource
private RedissonClient redissonClient;

public void updateUser(String userId) {
    String lockKey = "config" + userId;
    RLock lock = redissonClient.getLock(lockKey); // 获取锁资源
    try {
        lock.lock(10, TimeUnit.SECONDS); // 加锁,可以指定锁定时间
        
        // TODO: 业务代码
    } finally {
        lock.unlock(); // 释放锁
    }
}

对比 RedisLockRegistry 和 Redisson 实现:

  • Redisson 优点:可以为每一个锁指定不同的超时时间 ,而 RedisLockRegistry 目前只能针对所有的锁设置统一的超时时间

注意:如果业务执行超时之后,再去 unlock 会抛出 java.lang.IllegalMonitorStateException

三、思考

1.为什么可以用 setIfAbsent(),而不能用 setnx() ?

  • setIfAbsent() 是 Redis 专门为分布式锁实现的原子操作,其中封装了获取 key、设置 key、设置过期时间等操作。
  • setnx() 主要用于分布式 ID 的生成,如果要用来实现分布式锁的话,虽然可以通过返回结果为0表示获取锁失败,1表示获取锁成功,但是对于设置过期时间的操作需要手动实现,无法保证原子性

整理完毕,完结撒花~ 🌻

参考地址:

1.Java三种方式实现redis分布式锁,https://blog.csdn.net/w_monster/article/details/124472493

相关推荐
毕设源码-赖学姐26 分钟前
【开题答辩全过程】以 基于Android的校园快递互助APP为例,包含答辩的问题和答案
java·eclipse
damo0129 分钟前
stripe 支付对接
java·stripe
麦麦鸡腿堡1 小时前
Java的单例设计模式-饿汉式
java·开发语言·设计模式
假客套1 小时前
Request method ‘POST‘ not supported,问题分析和解决
java
傻童:CPU1 小时前
C语言需要掌握的基础知识点之前缀和
java·c语言·算法
YJlio2 小时前
Process Monitor 学习笔记(5.24):工具栏参考与高效快捷键指南
笔记·学习·php
爱吃山竹的大肚肚2 小时前
@Valid校验 -(Spring 默认不支持直接校验 List<@Valid Entity>,需用包装类或手动校验。)
java·开发语言
deng-c-f2 小时前
Linux C/C++ 学习日记(30):协程(一):同步和异步、协程的简要介绍、用户态CPU调度的实现
学习·协程·同步/异步
雨夜之寂2 小时前
mcp java实战 第一章-第一节-MCP协议简介.md
java·后端
艾德金的溪2 小时前
redis-7.4.6部署安装
前端·数据库·redis·缓存