redission分布式锁实现
实现分布式锁,通常是采用两种方式:1、setnx设置过期时间,配合lua脚本;2、使用redission框架
Redis 实现分布式锁的演变和升级:
1、setnx:set if Not eXists -> setnx key value -> 问题:因为没有设置过期时间,所以可能存在死锁的问题(执行一半程序崩溃,后面的释放锁流程没有执行)。
2、set nx ex/px:Redis 2.6+ set key value nx px 3000 -> 问题:不能解决锁续期和锁重入。
3、Lua 脚本:解决锁重入的问题,但存在问题:实现复杂、不能实现锁续期。
4、Redisson 框架:实现简单、可以实现锁重入和锁续期问题。
使用setnx实现分布式锁有几个小问题:1、没有设置过期时间,该如何控制锁持有时间(这个可以通过expire设置过期时间解决);2、锁操作分为2步,判断是否是你的锁,是的话进行释放,这是非原子操作(这个可以通过lua脚本解决);3、设置的过期时间达到,但业务还没执行完成,还需要继续拥有锁,而此时因为设置了过期时间就自动释放锁资源了,别的线程获取到资源,就可能会导致并发问题,即数据不一致,而且还极难复现(这个可以自行在代码中进行锁续期,也就是看门狗机制的实现,当发现业务代码还没执行完成,那么就继续对这个锁加时间)。
使用redission框架实现的分布式锁,1、它底层也是自行集成了lua脚本(即针对锁误删的问题,就是判断锁是否是当前线程和删除这两个的原子操作);2、锁续期底层也是通过看门狗的机制去实现的,只要时间到了,代码还未执行完成,就会完成续期让其继续执行,直到完成业务代码。
本文介绍下redission实现分布式锁:
一、添加依赖:
<!-- Redisson --> <!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.25.2</version> <!-- 请根据实际情况使用最新版本 --> </dependency>
二、配置redissionClient对象
将 RedissonClient 重写,存放到 IoC 容器,并且配置连接的 Redis 服务器信息
import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient() { Config config = new Config(); // 也可以将 redis 配置信息保存到配置文件 config.useSingleServer().setAddress("redis://127.0.0.1:6379"); return Redisson.create(config); } }
三、使用分布式锁
Redisson 分布式锁的操作和 Java 中的 ReentrantLock(可重入锁)的操作很像,都是先使用 tryLock 尝试获取(非公平)锁,最后再通过 unlock 释放锁
import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.concurrent.TimeUnit; @RestController public class LockController { @Autowired private RedissonClient redissonClient; @GetMapping("/lock") public String lockResource() throws InterruptedException { String lockKey = "myLock"; // 获取 RLock 对象 RLock lock = redissonClient.getLock(lockKey); try { // 尝试获取锁(尝试加锁)(锁超时时间是 30 秒) boolean isLocked = lock.tryLock(30, TimeUnit.SECONDS); if (isLocked) { // 成功获取到锁 try { // 模拟业务处理 TimeUnit.SECONDS.sleep(5); return "成功获取锁,并执行业务代码"; } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放锁 lock.unlock(); } } else { // 获取锁失败 return "获取锁失败"; } } catch (InterruptedException e) { e.printStackTrace(); } return "获取锁成功"; } }
贴上一段实际的使用代码,可以参考下:
public ResponseEntity chat(String question) throws Exception { if (!StringUtils.hasLength(question)){ return ResponseEntity.error("问题不能为空"); } long uid = SecurityUserInfoUtil.getSecurityUserDetails().getUid(); String modelLockKey = AppVariable.getModelLockKey(uid, AiModelEnum.OPENAI.getCode(), AiTypeEnum.CHAT.getCode()); // 尝试获取锁 RLock lock = redissonClient.getLock(modelLockKey); boolean tryLock = lock.tryLock(30, TimeUnit.SECONDS); // 获取锁失败,则返回错误信息 if (!tryLock){ return ResponseEntity.error("请勿重复提问"); } // 获取锁成功,则执行逻辑 try { String call = chatModel.call(question); Answer answer = new Answer(); answer.setTitle(question); answer.setContent(call); answer.setModel(AiModelEnum.OPENAI.getCode()); answer.setType(AiTypeEnum.CHAT.getCode()); answer.setUid(uid); boolean save = answerService.save(answer); if (save){return ResponseEntity.success(call); } }catch (Exception e){ e.printStackTrace(); return ResponseEntity.error("请求失败"); } finally { // 释放锁 lock.unlock(); } return ResponseEntity.error("数据保存失败,请重试"); }
key的获取
public class AppVariable { // 获取首页缓存的数据的 key public static String getListCacheKey(Long uid,int model,int type){ return "LIST_CACHE_KEY" + uid + "_" + model + "_" + type; } }
以上代码中:使用了锁的超时时间,那么redission的看门狗机制就会失效,会使用你设置的过期时间,到了时间会自动释放锁资源。而如果你想用redission框架的自动锁续期,就使用下面的代码
RLock lock = redissonClient.getLock(lockKey); try { // 加锁,默认加锁时间 30 秒,Redisson 的 Watch Dog 机制会自动续期(若业务未完成) lock.lock(); // 执行业务逻辑,如操作共享资源、更新数据库等 System.out.println("执行受锁保护的业务逻辑..."); } finally { // 解锁,必须在 finally 中确保释放,避免死锁 lock.unlock(); System.out.println("锁已释放"); }
即不设置过期时间,让redission框架自己控制。
可重入性:同一线程可多次获取同一把锁(lock.lock() 多次调用),解锁时需对应次数 unlock 才会真正释放,也可直接调用 unlock() 一次释放(内部会处理重入计数)。
Watch Dog 机制:加锁后,若业务执行时间超过默认 30 秒,Redisson 会每隔 10 秒(internalLockLeaseTime / 3 ,internalLockLeaseTime 默认 30 秒 )检查线程是否仍持有锁,若持有则延长锁的过期时间,保证业务能完成。
四、实现公平锁
Redisson 默认创建的分布式锁是非公平锁(出于性能的考虑)
RLock lock = redissonClient.getFairLock(lockKey); // 获取公平锁
五、实现读写锁
RReadWriteLock lock = redissonClient.getReadWriteLock(lockKey); // 获取读写锁 lock.readLock(); // 读锁 lock.writeLock(); // 写锁
读写锁的特点就是并发性能高(读读共享、读写/写写不共享),它是允许多个线程同时获取读锁进行读操作的,也就是说在没有写锁的情况下,读取操作可以并发执行,提高了系统的并行度。
但写锁则是独占式的,同一时间只有一个线程可以获得写锁,无论是读还是写都无法与写锁并存,这样就确保了数据修改时的数据一致性。
但写锁则是独占式的,同一时间只有一个线程可以获得写锁,无论是读还是写都无法与写锁并存,这样就确保了数据修改时的数据一致性。
浙公网安备 33010602011771号