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

相关推荐
num_killer4 小时前
小白的Langchain学习
java·python·学习·langchain
wdfk_prog5 小时前
[Linux]学习笔记系列 -- hashtable
linux·笔记·学习
期待のcode5 小时前
Java虚拟机的运行模式
java·开发语言·jvm
程序员老徐5 小时前
Tomcat源码分析三(Tomcat请求源码分析)
java·tomcat
a程序小傲5 小时前
京东Java面试被问:动态规划的状态压缩和优化技巧
java·开发语言·mysql·算法·adb·postgresql·深度优先
仙俊红5 小时前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥5 小时前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
小楼v5 小时前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地6 小时前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209256 小时前
Guava Cache 原理与实战
java·后端·spring