SpringBoot整合Redisson
1. 引入依赖
<!-- redisson config -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.8</version>
</dependency>
1.1 配置redisson配置文件
server:
port: 8080
spring:
# Redisson配置 spring.redisson.singleServerConfig
redisson:
config:
singleServerConfig:
# 连接空闲超时,单位:毫秒
idleConnectionTimeout: 100000
# 连接超时,单位:毫秒
connectTimeout: 10000
# 命令等待超时,单位:毫秒
timeout: 3000
# 命令失败重试次数
retryAttempts: 3
# 命令重试发送时间间隔,单位:毫秒
retryInterval: 1500
# 密码
password: ${spring.redis.password}
# 单个连接最大订阅数量
subscriptionsPerConnection: 5
# 客户端名称
clientName: null
# 节点地址
address: redis://${spring.redis.host}:${spring.redis.port}
# 发布和订阅连接的最小空闲连接数
subscriptionConnectionMinimumIdleSize: 1
# 发布和订阅连接池大小
subscriptionConnectionPoolSize: 50
# 最小空闲连接数
connectionMinimumIdleSize: 10
# 连接池大小
connectionPoolSize: 64
# redis数据库编号
database: ${spring.redis.database}
# DNS监测时间间隔,单位:毫秒
dnsMonitoringInterval: 5000
# 线程池数量
threads: 0
# Netty线程池数量
nettyThreads: 0
# 编码
codec:
class: "org.redisson.codec.JsonJacksonCodec"
# 传输模式
transportMode: "NIO"
# 配置看门狗的默认 超时 时间为30s,这里改为10s
# 因为看门狗会去修改 锁的超时时间,所以要设置一个链接redis的超时时间
lockWatchdogTimeout: 10000
1.2 也可以用另一种方式初始化Redisson
@Configuration
public class RedissonConfig {
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
使用
@Service
public class LockTestService{
@Autowired
RedissonClient redisson;
public void testLock(){
RLock lock = redisson.getLock("myLock");
try {
lock.lock();
// 执行需要加锁的代码
} finally {
lock.unlock();
}
}
}
2. Redisson的使用
通过第一种方式注入到容器当中的使用方式
/**
*Redisson工具类
* @date 2023/11/02 10:29
**/
@Component
public class RedissonUtil {
@Resource
private RedissonClient client;
/**
* 获取Redis锁
* 基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。
*/
public RLock getLock(String key) {
return client.getLock(key);
}
/**
* 获取Redis公平锁
* 它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。
*/
public RLock getFairLock(String key) {
return client.getFairLock(key);
}
}
public class TestRedissonController {
private final RedissonUtil redissonUtil;
public TestRedissonController(RedissonUtil redissonUtil) {
this.redissonUtil = redissonUtil;
}
/**
* 分布式锁伪代码
* redisson在启用的时候,会默认开启看门狗策略,如果超出key过期时间,进程还没结束的话,看门狗会自动续费1/3的时间
*
* 看门狗机制 是当锁过期的时候,看门狗会查看执行业务的线程是否还存活,如果存活的话,就会对当前锁续期;
* 如果业务线程一直存活,那么当前的分布式锁就不会失效
*
* 解析源码参考:https://blog.51cto.com/u_16213688/7540380
*/
@PostMapping("/getLock")
public void getLock(String key) {
RLock lock = redissonUtil.getLock(key);
try {
log.info("尝试获取redison锁:{}",Thread.currentThread().getName());
// 尝试加锁,最多等待5秒,上锁以后10秒自动解锁
// 1.tryLock leaseTime 如果设置了过期时间,那么当超过这个时间的话,就会自动释放锁
// 2.但是如果 不设置这个值的话默认超时时间是30秒,但是会触发看门狗机制,即:业务没有执行完成之前会一直续费
boolean isOk = lock.tryLock(2, -1, TimeUnit.SECONDS);
if (!isOk) {
// 获取分布式锁失败,可以进行抛出获取锁失败的提示等,根据业务来
log.info("获取分布式锁失败,线程名称:{}",Thread.currentThread().getName());
return;
}
log.info("业务处理中~~~~,线程名称:{}",Thread.currentThread().getName());
// 获取锁成功后,开始处理业务逻辑
Thread.sleep(300000);
log.info("业务执行完成!!!!");
} catch (InterruptedException e) {
// 如果10s没有执行完,锁会自动释放,这里抛出异常
Thread.currentThread().interrupt();
} finally {
log.info("释放分布式锁~~~~,释放线程名称:{}",Thread.currentThread().getName());
// 如果10s没有执行完,锁会自动释放,这里抛出异常,操作失败,需要对数据回滚
// 需要判断是不是当前线程是否还持有锁,因为看门狗是可以对当前锁进行续时的,有可能应该不持有锁了
if (lock.isLocked() && lock.isHeldByCurrentThread())
lock.unlock();
}
System.out.println("eqweqeqweqwewqeqweqeq");
}
}
注意*
当获取redisson锁的时候如果设置了过期时间(tryLock(long waitTime, long leaseTime, TimeUnit unit) 中的leaseTime 时间是过期时间),那么看门狗就无法续费了;
源码地址org.redisson.RedissonLock#tryAcquireAsync
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime != -1) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
// lock acquired
if (ttlRemaining == null) {
// 如果,设置了过期时间就不走,schedule 到期续订方法了,所以这块代码已经写死了,就无法续费了
if (leaseTime != -1) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
// schedule 到期续订
scheduleExpirationRenewal(threadId);
}
}
return ttlRemaining;
});
return new CompletableFutureWrapper<>(f);
}
// schedule 到期续订
protected void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
try {
// 续订到期 方法
renewExpiration();
} finally {
if (Thread.currentThread().isInterrupted()) {
cancelExpirationRenewal(threadId);
}
}
}
}
// 续订到期 方法
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 刷新等待时间
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// 重新安排自身,递归调用自身为锁自动续费
renewExpiration();
} else {
cancelExpirationRenewal(null);
}
});
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
// 刷新等待时间
// internalLockLeaseTime 这个时间是30s
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}