目录
- 一、背景
- 二、具体实现
- 1.RedisTemplate 实现(非阻塞)
- 2.RedisLockRegistry 实现(阻塞)
- 3.Redisson 实现(阻塞)
- 三、思考
- 1.为什么可以用 setIfAbsent(),而不能用 setnx() ?
一、背景
在分布式服务中,经常有例如定时任务这样的场景。
在定时任务中,如果不使用 quartz
这样的分布式定时工具,只是简单使用 @Schedule
注解来实现定时任务,在服务分布式部署中,就有可能存在定时任务并发重复执行问题。
对于解决以上场景中的问题,我们引入了分布式锁。
二、具体实现
1.RedisTemplate 实现(非阻塞)
RedisUtils 工具类:
@Componentpublic class RedisUtils {@Autowiredprivate 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);}}
使用示例:
@Resourceprivate 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,即获取锁失败。
2.RedisLockRegistry 实现(阻塞)
RedisLockRegistry 是 spring-integration-redis
中提供的 Redis 分布式锁实现类。
集成 spring-integration-redis:
<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:
@Configurationpublic class RedisLockConfig {@Beanpublic RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactor) {// 第一个参数 redisConnectionFactory// 第二个参数 registryKey,分布式锁前缀,建议设置项目名称// 该构造方法对应的分布式锁,默认有效期是60秒,可以自定义。return new RedisLockRegistry(redisConnectionFactory, "boot-launch");// return new RedisLockRegistry(redisConnectionFactory, "boot-launch", 60);}}
使用RedisLockRegistry:
代码实现:
@Resourceprivate RedisLockRegisty redisLockRegistry;public void updateUser(String userId) {String lockKey = "config" + userId;Lock lock = redisLockRegistry.obtain(lockKey); // 获取锁资源try {lock.lock(); // 加锁// TODO: 业务代码} finally {lock.unlock(); // 释放锁}}
注解实现:
@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:
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.15.0</version><exclusions><exclusion><groupId>org.redisson</groupId><artifactId>redisson-spring-data-23</artifactId></exclusion></exclusions></dependency>
配置:
1)在配置文件中添加如下内容:
spring:redis:redisson:file: classpath:redisson.yaml
2)然后新建一个 redisson.yaml
文件,也放在 resources
目录下:
singleServerConfig:idleConnectionTimeout: 10000connectTimeout: 10000timeout: 3000retryAttempts: 3retryInterval: 1500password: 123456subscriptionsPerConnection: 5clientName: nulladdress: "redis://192.168.161.3:6379"subscriptionConnectionMinimumIdleSize: 1subscriptionConnectionPoolSize: 50connectionMinimumIdleSize: 32connectionPoolSize: 64database: 0dnsMonitoringInterval: 5000threads: 0nettyThreads: 0codec: ! {}transportMode: "NIO"
代码实现:
@Resourceprivate 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