redis分布式锁实现的两种方式 lua脚本与redisson,解决自定义超时后锁自动续期失效问题
·
分布式锁可以解决日常分布式系统多不同进程访问同一个共享资源导致资源失去原子性的问题,保证这个共享的资源在同一时间只能被一个线程操作。因为它是一个跨JVM的互斥机制,可以用来控制系统分布式部署情况下共享资源的访问
引入redis的maven依赖
<!-- redis引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yml配置文件:
spring: redis: #密码 password: kevin #连接超时时长(毫秒) timeout: 30000 cluster: #集群节点以逗号分隔,或换行后 - 开头 nodes: - 127.0.0.1:6381 - 127.0.0.1:6382 - 127.0.0.1:6383 - 127.0.0.1:6384 - 127.0.0.1:6385 - 127.0.0.1:6386 # 获取失败 最大重定向次数 max-redirects: 3 #lettuce连接池信息 # 连接池最大连接数(使用负值表示没有限制) 默认为8 lettuce: pool: # 连接池最大连接数 max-active: 1000 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1 max-wait: -1 # 连接池中的最大空闲连接 默认为8 max-idle: 200 # 连接池中的最小空闲连接 默认为 0 min-idle: 100
redis配置类
package com.liu.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.liu.redisexpired.RedisMessageListenerFactory;
import io.lettuce.core.TimeoutOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.Arrays;
@Configuration
public class RedisConfig {
@Autowired
private Environment environment;
@Bean(value = "nodes")
@ConfigurationProperties(prefix = "spring.redis.cluster")
public RedisNodes nodes(){
return new RedisNodes();
}
/**
* 配置lettuce连接池
* @author kevin
* @return org.apache.commons.pool2.impl.GenericObjectPoolConfig
* @date 2022/5/26
*/
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.redis.cluster.lettuce.pool")
public GenericObjectPoolConfig<Object> redisPool() {
GenericObjectPoolConfig<Object> poolConfig = new GenericObjectPoolConfig<>();
String maxActive = environment.getProperty("spring.redis.lettuce.pool.max-active");
if(null != maxActive && !"".equals(maxActive)) {
poolConfig.setMaxTotal(Integer.parseInt(maxActive));
}
String maxWait = environment.getProperty("spring.redis.lettuce.pool.max-wait");
if(null != maxWait && !"".equals(maxWait)) {
poolConfig.setMaxWaitMillis(Integer.parseInt(maxWait));
}
String maxIdle = environment.getProperty("spring.redis.lettuce.pool.max-idle");
if(null != maxIdle && !"".equals(maxIdle)) {
poolConfig.setMaxIdle(Integer.parseInt(maxIdle));
}
String minIdle = environment.getProperty("spring.redis.lettuce.pool.min-idle");
if(null != minIdle && !"".equals(minIdle)) {
poolConfig.setMinIdle(Integer.parseInt(minIdle));
}
return new GenericObjectPoolConfig<>();
}
/**
* 配置数据源的
* @author kevin
* @return org.springframework.data.redis.connection.RedisClusterConfiguration
* @date 2022/5/26
*/
@Bean("redisClusterConfig")
@Primary
public RedisClusterConfiguration redisClusterConfig() {
RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(
Arrays.asList(nodes().getNodes()));
redisClusterConfiguration.setPassword(environment.getProperty("spring.redis.password"));
String maxRedirects = environment.getProperty("spring.redis.cluster.max-redirects");
if(null != maxRedirects && !"".equals(maxRedirects)) {
redisClusterConfiguration.setMaxRedirects(Integer.parseInt(maxRedirects));
}
return redisClusterConfiguration;
}
/**
* 配置数据源的连接工厂
* 这里注意:需要添加@Primary 指定bean的名称,目的是为了创建不同名称的LettuceConnectionFactory
* @author kevin
* @param redisPool :
* @param redisClusterConfig :
* @return org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
* @date 2022/5/26
*/
@Bean("lettuceConnectionFactory")
@Primary
public LettuceConnectionFactory lettuceConnectionFactory(GenericObjectPoolConfig<Object> redisPool,
@Qualifier("redisClusterConfig") RedisClusterConfiguration redisClusterConfig) {
// LettuceClientConfiguration clientConfiguration = LettucePoolingClientConfiguration.builder()
// .poolConfig(redisPool).build();
LettuceClientConfiguration clientConfiguration = getClientConfiguration(redisPool);
return new LettuceConnectionFactory(redisClusterConfig, clientConfiguration);
}
/**
* 配置数据源的RedisTemplate
* 注意:这里指定使用名称=factory 的 RedisConnectionFactory
* @author kevin
* @param redisConnectionFactory :
* @return org.springframework.data.redis.core.RedisTemplate
* @date 2022/5/26
*/
@Bean("redisTemplate")
@Primary
public RedisTemplate<String, Object> redisTemplate(@Qualifier("lettuceConnectionFactory")
RedisConnectionFactory redisConnectionFactory) {
return getRedisTemplate(redisConnectionFactory);
}
/**
* lettuce配置信息构建获取--redis集群高可用
* @author kevin
* @param redisPool :
* @return org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration
* @date 2022/5/27 12:00
*/
private LettuceClientConfiguration getClientConfiguration(GenericObjectPoolConfig<Object> redisPool) {
//支持自适应集群拓扑刷新和静态刷新源
ClusterTopologyRefreshOptions clusterTopologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
//.enablePeriodicRefresh(Duration.ofSeconds(10)) // 启用定期集群拓扑更新
.enableAllAdaptiveRefreshTriggers() // 自适应拓扑刷新
.adaptiveRefreshTriggersTimeout(Duration.ofSeconds(10)) // 自适应拓扑更新的超时时间
.build();
ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder().timeoutOptions(
TimeoutOptions.enabled(Duration.ofSeconds(30))) // 超时修改为30秒
// .autoReconnect(false) //启用或禁用连接丢失时的自动重新连接--默认true
// .pingBeforeActivateConnection(Boolean.TRUE) //在激活连接标志之前设置 PING
// .cancelCommandsOnReconnectFailure(Boolean.TRUE) //允许在重新连接失败的情况下取消排队的命令。默认为 false
// .disconnectedBehavior(ClientOptions.DisconnectedBehavior.REJECT_COMMANDS) //设置连接处于断开状态时的命令调用行为
.topologyRefreshOptions(clusterTopologyRefreshOptions) //设置ClusterTopologyRefreshOptions拓扑更新的详细控制
.build();
return LettucePoolingClientConfiguration.builder()
.poolConfig(redisPool) //设置GenericObjectPoolConfig驱动程序使用的连接池配置
//.readFrom(ReadFrom.NEAREST) //配置ReadFrom
.clientOptions(clusterClientOptions) //配置ClientOptions
.build();
}
/**
* redisTemplate redis操作工具获取
* @author kevin
* @param factory : redis连接工厂
* @return org.springframework.data.redis.core.RedisTemplate<java.lang.String,java.lang.Object>
* @date 2022/5/27 12:00
*/
private RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL) //过期方法
om.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance ,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
RedisNodes类:
package com.liu.config;
public class RedisNodes {
private String[] nodes;
public String[] getNodes() {
return nodes;
}
public void setNodes(String[] nodes) {
this.nodes = nodes;
}
}
1.使用lua脚本
此种方式,需要自己编写锁的获取以及释放,且需要自己定义锁的续期
1.1.编写redis工具类:
package com.liu.utils;
import io.lettuce.core.RedisException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* redis工具类
*
* @author kevin
* @date 2021/3/30
*/
@Slf4j
@Component
@SuppressWarnings({"unused"})
public class RedisUtils {
//分布式锁,业务流程是否完成(因为支持多线程,所以需要使用线程安全的map)
private final ConcurrentHashMap<String, Boolean> businessMap = new ConcurrentHashMap<>();
private static final Long SUCCESS = 1L;
/*
* 注入redisTemplate bean(可配置不同的template secondaryRedisTemplate)
*/
@Autowired
@Qualifier(value = "redisTemplate") //指定注入的template模版
private RedisTemplate<String, Object> redisTemplate;
public boolean getBusinessStatus(String key) {
return businessMap.get(key);
}
public void setBusinessStatus(String key, boolean businessDone) {
businessMap.put(key, businessDone);
}
//==================================分布式锁 开始==================================
/**
* 获取分布式锁
*
* @param key : 键
* @param value : 值
* @param releaseTime : 过期释放时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @param waitTime : 获取锁等待的时间(秒)
* @return boolean true成功 false失败
* @author kevin
* @date 2022/5/31
*/
public boolean getLock(String key, Object value, int releaseTime, int waitTime) {
boolean locked = false;
int tryTimes = 0;
//自动重新取锁,超过设置时间的秒数后未取到人为取锁失败
while (!locked && tryTimes < waitTime) {
try {
//lua 脚本,进行setNx设置,如果返回结果为1表示成功(当key存在的时候不会成功),成功了后给锁设置过期时间;否则代表失败--返回0
String script = "if redis.call('setNx', KEYS[1], ARGV[1])==1 then return redis" +
".call('expire', KEYS[1], ARGV[2]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
// execute使用的redis的默认的序列化方式,需要设置参数--arg的序列化方式,以及result结果的序列化方式
// 此处传参只要能转为Object就行(因为数字不能直接强转为String,所以不能用String序列化)
// 结果的类型需要根据脚本定义,此处是数字--定义的是Long类型
Long result = redisTemplate.execute(redisScript, new GenericToStringSerializer<>(Object.class),
new GenericToStringSerializer<>(Long.class), Collections.singletonList(key), value, releaseTime);
if (SUCCESS.equals(result)) {
//获取锁成功后,添加一个看门狗,实现锁自动续期
startWatchDog(key, value, releaseTime);
locked = true;
}else{
tryTimes++;
locked = false;
Thread.sleep(1000);
}
} catch (Exception e) {
log.info("获取锁异常:", e);
throw new RedisException("获取锁异常!");
}
}
return locked;
}
/**
* 自定义看门狗,监测redis分布式锁过期,自动续期(针对使用lua自定义加锁,释放锁的自定义)
*
* @param key : 键
* @param value : 值
* @param releaseTime : 过期释放时间,单位为秒
* @author kevin
* @date 2022/6/1 21:45
*/
public void startWatchDog(String key, Object value, int releaseTime){
//当前线程的线程ID,看门狗加上线程是为了防止其他线程来对本线程获得的锁进行续期操作
long threadId = Thread.currentThread().getId();
String threadName = Thread.currentThread().getName();
setBusinessStatus(key + threadId, true);
long oneThird = releaseTime / 3; //单位为秒
log.info(LocalDateTime.now() + ": " + threadName + "看门狗开启");
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
" else return 0 end";
//如果加追加第二个参数为true则开启守护线程
Timer timer = new Timer("lockWatchDogTimer", true);
//schedule 第一个参数为timertask--要执行的任务、第二个参数为delay--等待响应时间后第一次执行、第三个参数为period--每次执行的间隔
timer.schedule(new TimerTask() {
@Override
public void run() {
if(getBusinessStatus(key + threadId)) {
//如果锁正在被使用,且剩余过期时间小于1/3超时时间 ,则将其重置
if (Boolean.TRUE.equals(hasKey(key)) && getExpire(key) <= oneThird) {
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
//设置的时候需要的是毫秒数,所以expire需要乘以1000
// execute使用的redis的默认的序列化方式,需要设置参数--arg的序列化方式,以及result结果的序列化方式
// 此处传参只要能转为Object就行(因为数字不能直接强转为String,所以不能用String序列化)
// 结果的类型需要根据脚本定义,此处是数字--定义的是Long类型
Long result = redisTemplate.execute(redisScript, new GenericToStringSerializer<>(Object.class),
new GenericToStringSerializer<>(Long.class), Collections.singletonList(key),
value, releaseTime * 1000);
log.info(LocalDateTime.now() + ": " + threadName + "看门狗自动续期,结果:" + result + ", 续期后过期时间:" +getExpire(key));
}
}else{
cancel();
log.info(LocalDateTime.now() + ": " + threadName + "看门狗关闭");
}
}
}, 0, oneThird * 1000);
}
/**
* 释放分布式锁
*
* @param key 键
* @param value 值
* @return true成功 false 失败
* @author kevin
* @date 2022/5/31
*/
public boolean releaseLock(String key, Object value) {
try {
//lua脚本,判断是否能够获得锁的值,如果能够能够获得且值是自己加锁时候的值,则进行删除,否则返回0
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) " +
"else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
// execute使用的redis的默认的序列化方式,需要设置参数--arg的序列化方式,以及result结果的序列化方式
// 此处传参只要能转为Object就行(因为数字不能直接强转为String,所以不能用String序列化)
// 结果的类型需要根据脚本定义,此处是数字--定义的是Long类型
Long result = redisTemplate.execute(redisScript, new GenericToStringSerializer<>(Object.class),
new GenericToStringSerializer<>(Long.class), Collections.singletonList(key), value);
if (SUCCESS.equals(result)) {
long threadId = Thread.currentThread().getId();
setBusinessStatus(key + threadId, false);
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
//==================================分布式锁 结束==================================
//==================================基础操作 开始==================================
/**
* 根据key获取过期时间
*
* @param key 键 不能为null
* @return java.lang.Long 时间(秒) 返回0代表为永久有效
* @author kevin
* @date 2021/3/31 16:49
*/
public Long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key : 键
* @return java.lang.Boolean true 存在 false不存在
* @author kevin
* @date 2021/3/31 16:50
*/
public Boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key : 可以传一个值 或多个
* @author kevin
* @date 2021/3/31 17:30
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
/**
* 获取key总数量
*
* @return int
* @author kevin
* @date 2021/3/31 17:44
*/
public int getSize() {
int size = 0;
Set<String> keySet = redisTemplate.keys("*");
if (!CollectionUtils.isEmpty(keySet)) {
size = keySet.size();
}
return size;
}
//==================================基础操作 结束==================================
// =================================普通对象操作 开始===============================
/**
* 普通缓存获取
*
* @param key : 键
* @return java.lang.Object
* @author kevin
* @date 2021/3/31 17:33
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key : 键
* @param value : 值
* @return boolean true成功 false失败
* @author kevin
* @date 2021/3/31 17:35
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// =================================普通对象操作 结束===============================
}
1.2.使用锁
package com.liu;
import com.liu.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringRedisLockTest {
@Autowired
private RedisUtils redisUtils;
@Test
public void testA(){
Assert.assertNotNull(redisUtils);
redisUtils.set("test_lock_count", 100);
//lua脚本实现分布式锁(需要锁的key的超时时间比业务处理的时间长,否则会存在问题;最好不要设置用不超时,因为出现异常的时候,容易造成死锁)
for (int i = 0; i < 4; i++) {
new Thread(() -> {
boolean locked = false;
log.info(Thread.currentThread().getName() + "--开始获取锁......");
while (!locked) {//超过等待时间未获取到后,从新获取,直到获取到为止
locked = redisUtils.getLock("test_lock", 1, 5, 100);
if (locked) {
try {
log.info(Thread.currentThread().getName() + "--我获得了锁,开始处理业务逻辑......");
int stock = Integer.parseInt(redisUtils.get("test_lock_count").toString());
if (stock > 0) {
redisUtils.set("test_lock_count", (stock - 1) + "");
} else {
log.info(Thread.currentThread().getName() + "--处理业务逻辑出错,数量不足!");
}
Thread.currentThread().sleep(10000);
}catch (InterruptedException e){
e.printStackTrace();
log.info(Thread.currentThread().getName() + "--处理业务逻辑处理异常!");
}finally {
log.info(Thread.currentThread().getName() + "--处理业务逻辑处理结束,开始释放锁......");
redisUtils.releaseLock("test_lock", 1);
log.info(Thread.currentThread().getName() + "--处理业务逻辑处理结束,锁释放完成!");
}
} else {
try {
log.info(Thread.currentThread().getName() + "--我未获得锁,不能处理业务逻辑,一秒后重新获取!");
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}, "Thread" + i).start();
}
try {
Thread.sleep(60000);
System.out.println(redisUtils.get("test_lock_count"));
redisUtils.del("test_lock_count");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
1.3. 测试运行查看结果
2022-06-06 15:52:07.821 INFO 7288 --- [ Thread0] com.liu.SpringRedisLockTest : Thread0--开始获取锁......
2022-06-06 15:52:07.821 INFO 7288 --- [ Thread2] com.liu.SpringRedisLockTest : Thread2--开始获取锁......
2022-06-06 15:52:07.821 INFO 7288 --- [ Thread1] com.liu.SpringRedisLockTest : Thread1--开始获取锁......
2022-06-06 15:52:07.821 INFO 7288 --- [ Thread3] com.liu.SpringRedisLockTest : Thread3--开始获取锁......
2022-06-06 15:52:07.898 INFO 7288 --- [ Thread3] com.liu.utils.RedisUtils : 2022-06-06T15:52:07.898: Thread3看门狗开启
2022-06-06 15:52:07.900 INFO 7288 --- [ Thread3] com.liu.SpringRedisLockTest : Thread3--我获得了锁,开始处理业务逻辑......
2022-06-06 15:52:11.125 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:11.122: Thread3看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 15:52:15.122 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:15.120: Thread3看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 15:52:17.110 INFO 7288 --- [ Thread3] com.liu.SpringRedisLockTest : Thread3--处理业务逻辑处理结束,开始释放锁......
2022-06-06 15:52:17.119 INFO 7288 --- [ Thread3] com.liu.SpringRedisLockTest : Thread3--处理业务逻辑处理结束,锁释放完成!
2022-06-06 15:52:17.130 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:17.127: Thread3看门狗自动续期,结果:0, 续期后过期时间:-2
2022-06-06 15:52:17.140 INFO 7288 --- [ Thread0] com.liu.utils.RedisUtils : 2022-06-06T15:52:17.140: Thread0看门狗开启
2022-06-06 15:52:17.141 INFO 7288 --- [ Thread0] com.liu.SpringRedisLockTest : Thread0--我获得了锁,开始处理业务逻辑......
2022-06-06 15:52:18.115 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:18.115: Thread3看门狗关闭
2022-06-06 15:52:20.153 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:20.150: Thread0看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 15:52:24.155 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:24.153: Thread0看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 15:52:25.146 INFO 7288 --- [ Thread0] com.liu.SpringRedisLockTest : Thread0--处理业务逻辑处理结束,开始释放锁......
2022-06-06 15:52:25.150 INFO 7288 --- [ Thread0] com.liu.SpringRedisLockTest : Thread0--处理业务逻辑处理结束,锁释放完成!
2022-06-06 15:52:25.158 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:25.155: Thread0看门狗自动续期,结果:0, 续期后过期时间:-2
2022-06-06 15:52:25.177 INFO 7288 --- [ Thread1] com.liu.utils.RedisUtils : 2022-06-06T15:52:25.177: Thread1看门狗开启
2022-06-06 15:52:25.177 INFO 7288 --- [ Thread1] com.liu.SpringRedisLockTest : Thread1--我获得了锁,开始处理业务逻辑......
2022-06-06 15:52:26.146 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:26.146: Thread0看门狗关闭
2022-06-06 15:52:28.191 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:28.188: Thread1看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 15:52:32.194 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:32.191: Thread1看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 15:52:33.181 INFO 7288 --- [ Thread1] com.liu.SpringRedisLockTest : Thread1--处理业务逻辑处理结束,开始释放锁......
2022-06-06 15:52:33.184 INFO 7288 --- [ Thread1] com.liu.SpringRedisLockTest : Thread1--处理业务逻辑处理结束,锁释放完成!
2022-06-06 15:52:33.208 INFO 7288 --- [ Thread2] com.liu.utils.RedisUtils : 2022-06-06T15:52:33.207: Thread2看门狗开启
2022-06-06 15:52:33.209 INFO 7288 --- [ Thread2] com.liu.SpringRedisLockTest : Thread2--我获得了锁,开始处理业务逻辑......
2022-06-06 15:52:34.184 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:34.184: Thread1看门狗关闭
2022-06-06 15:52:36.222 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:36.220: Thread2看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 15:52:40.225 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:40.222: Thread2看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 15:52:41.216 INFO 7288 --- [ Thread2] com.liu.SpringRedisLockTest : Thread2--处理业务逻辑处理结束,开始释放锁......
2022-06-06 15:52:41.219 INFO 7288 --- [ Thread2] com.liu.SpringRedisLockTest : Thread2--处理业务逻辑处理结束,锁释放完成!
2022-06-06 15:52:41.225 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:41.223: Thread2看门狗自动续期,结果:0, 续期后过期时间:-2
2022-06-06 15:52:42.216 INFO 7288 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T15:52:42.216: Thread2看门狗关闭
96
2.使用redisson实现:
2.1.添加maven依赖
<!-- redis分布式锁工具 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
2.2. 编写redisson配置类:
Properties配置类
package com.liu.redislockconfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class RedisConfigProperties {
private String password;
private cluster cluster;
public static class cluster {
private List<String> nodes;
public List<String> getNodes() {
return nodes;
}
public void setNodes(List<String> nodes) {
this.nodes = nodes;
}
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public RedisConfigProperties.cluster getCluster() {
return cluster;
}
public void setCluster(RedisConfigProperties.cluster cluster) {
this.cluster = cluster;
}
}
redissonclient配置类:
package com.liu.redislockconfig;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.ClusterServersConfig;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(RedisConfigProperties redisConfigProperties) {
//集群的ip前面要加上“redis://”,不然会报错,3.2版本可不加
List<String> clusterNodes = new ArrayList<>();
for (int i = 0; i < redisConfigProperties.getCluster().getNodes().size(); i++) {
clusterNodes.add("redis://" + redisConfigProperties.getCluster().getNodes().get(i));
}
Config config = new Config();
// 添加集群地址
ClusterServersConfig clusterServersConfig = config.useClusterServers().addNodeAddress(
clusterNodes.toArray(new String[0]));
// 设置密码
clusterServersConfig.setPassword(redisConfigProperties.getPassword());
return Redisson.create(config);
}
}
2.3.使用redisson分布式锁
需要注意tryLock的时候,未设置锁的超时时间,此处会使用redisson默认的超时时间--30s。且会自动为锁加一个看门狗,实现锁的自动续期功能。
package com.liu;
import com.liu.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringRedisLockTest {
@Resource
private Redisson redisson;
@Autowired
private RedisUtils redisUtils;
@Test
public void testA(){
Assert.assertNotNull(redisUtils);
Assert.assertNotNull(redisson);
redisUtils.set("test_lock_count", 100);
//redission实现分布式锁
for (int i = 0; i < 4; i++) {
new Thread(() -> {
//获取一个可重入锁
log.info(Thread.currentThread().getName() + "--开始获取锁......");
RLock lock = redisson.getLock("test_lock");
boolean result = false;
while (!result) {//超过等待时间未获取到后,从新获取,直到获取到为止
try {
// 最多等待30秒(第一个参数)加锁,加锁后60秒(第二个参数)自动过期--释放锁(默认是30秒自动过期--释放锁);
// 设置了过期时间后,锁不会自动续期,需要自定义续期
//(需要锁的key的过期时间比业务处理的时间长,否则会存在问题;最好不要设置永不超时,因为出现异常的时候,容易造成死锁)
Future<Boolean> res = lock.tryLockAsync(100, TimeUnit.SECONDS);
result = res.get();
String field = redisson.getConnectionManager().getId() + ":" + Thread.currentThread().getId();
if (result) {
try{
log.info(lock.getName() + "----" + Thread.currentThread().getName() + "--我获得了锁,开始处理业务逻辑......");
int stock = Integer.parseInt(redisUtils.get("test_lock_count").toString());
if (stock > 0) {
redisUtils.set("test_lock_count", (stock - 1) + "");
} else {
log.info(Thread.currentThread().getName() + "--处理业务逻辑出错,数量不足!");
}
Thread.sleep(2000); //模拟业务耗时6s
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
log.info(Thread.currentThread().getName() + "--处理业务逻辑处理结束,开始释放锁......");
lock.unlock(); //释放锁
log.info(Thread.currentThread().getName() + "--处理业务逻辑处理结束,锁释放完成!");
}
}else {
log.info(Thread.currentThread().getName() + "--我未获得锁,不能处理业务逻辑,一秒后重新获取!");
Thread.sleep(1000); //模拟业务耗时6s
}
} catch (Exception e) {
e.printStackTrace();
}
}
}, "Thread" + i).start();
}
try {
Thread.sleep(10000);
System.out.println(redisUtils.get("test_lock_count"));
redisUtils.del("test_lock_count");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
2.4. 运行查看结果:
2022-06-06 16:03:14.288 INFO 5432 --- [ Thread0] com.liu.SpringRedisLockTest : Thread0--开始获取锁......
2022-06-06 16:03:14.288 INFO 5432 --- [ Thread3] com.liu.SpringRedisLockTest : Thread3--开始获取锁......
2022-06-06 16:03:14.288 INFO 5432 --- [ Thread1] com.liu.SpringRedisLockTest : Thread1--开始获取锁......
2022-06-06 16:03:14.288 INFO 5432 --- [ Thread2] com.liu.SpringRedisLockTest : Thread2--开始获取锁......
2022-06-06 16:03:14.507 INFO 5432 --- [ Thread3] com.liu.SpringRedisLockTest : test_lock----Thread3--我获得了锁,开始处理业务逻辑......
2022-06-06 16:03:16.575 INFO 5432 --- [ Thread3] com.liu.SpringRedisLockTest : Thread3--处理业务逻辑处理结束,开始释放锁......
2022-06-06 16:03:16.575 INFO 5432 --- [ Thread3] com.liu.SpringRedisLockTest : Thread3--处理业务逻辑处理结束,锁释放完成!
2022-06-06 16:03:16.575 INFO 5432 --- [ Thread2] com.liu.SpringRedisLockTest : test_lock----Thread2--我获得了锁,开始处理业务逻辑......
2022-06-06 16:03:18.591 INFO 5432 --- [ Thread2] com.liu.SpringRedisLockTest : Thread2--处理业务逻辑处理结束,开始释放锁......
2022-06-06 16:03:18.591 INFO 5432 --- [ Thread2] com.liu.SpringRedisLockTest : Thread2--处理业务逻辑处理结束,锁释放完成!
2022-06-06 16:03:18.591 INFO 5432 --- [ Thread0] com.liu.SpringRedisLockTest : test_lock----Thread0--我获得了锁,开始处理业务逻辑......
2022-06-06 16:03:20.607 INFO 5432 --- [ Thread0] com.liu.SpringRedisLockTest : Thread0--处理业务逻辑处理结束,开始释放锁......
2022-06-06 16:03:20.607 INFO 5432 --- [ Thread0] com.liu.SpringRedisLockTest : Thread0--处理业务逻辑处理结束,锁释放完成!
2022-06-06 16:03:20.669 INFO 5432 --- [ Thread1] com.liu.SpringRedisLockTest : test_lock----Thread1--我获得了锁,开始处理业务逻辑......
2022-06-06 16:03:22.682 INFO 5432 --- [ Thread1] com.liu.SpringRedisLockTest : Thread1--处理业务逻辑处理结束,开始释放锁......
2022-06-06 16:03:22.682 INFO 5432 --- [ Thread1] com.liu.SpringRedisLockTest : Thread1--处理业务逻辑处理结束,锁释放完成!
3.问题
那么问题来了,我就是不想用redisson默认的30s,想自己设置一个咋办?简单,在tryLock的时候,多加一个参数,如:lock.tryLockAsync(100, 5, TimeUnit.SECONDS) 。但是,这样就会引发另外一个非常严重的问题--redisson看门狗失效了,锁不会自动续期了,这不是扯呢嘛。经过查看源码*(RedissonLock.class)发现还真有这个设置,自己设置了超时时间后,就不会添加看门狗:

那咋整啊?难道我就连自己设置一个锁超时的权利都没有了吗?不,我有的,怎做呢?简单:
3.1.redis工具类中自定义实现一个看门狗:
为什么需要重新定义一个看门狗,而不直接使用上面lua脚本实现分布式锁的看门狗方法呢?这是因为redisson加锁的时候,存放的不是普通的value内容,而是hash的(以这样的方式设值的:hset your_key UUID:ThreadId 1)
/**
* 自定义看门狗,监测redis分布式锁过期,自动续期(针对redisson的自定义)
* 因为redisson的锁是以hash方式存入redis的,所以方式与自定义的锁不一样
* 之所以自定义一个看门狗,是因为redisson在自己设置了锁的过期时间后,不会为锁添加看门狗来对key进行续期
*
* @param key : 键
* @param field : 缓存的hash中的key键
* @param releaseTime : 过期释放时间,单位为秒
* @author kevin
* @date 2022/6/1 21:45
*/
public void startRedissonWatchDog(String key, String field, int releaseTime){
String threadName = Thread.currentThread().getName();
setBusinessStatus(key + field, true);
long oneThird = releaseTime / 3; //单位为秒
log.info(LocalDateTime.now() + ": " + Thread.currentThread().getName() + "看门狗开启");
String script = "if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then return redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
" else return 0 end";
//如果加追加第二个参数为true则开启守护线程
Timer timer = new Timer("lockWatchDogTimer", true);
//schedule 第一个参数为timertask--要执行的任务、第二个参数为delay--等待响应时间后第一次执行、第三个参数为period--每次执行的间隔
timer.schedule(new TimerTask() {
@Override
public void run() {
if(getBusinessStatus(key + field)) {
//如果锁正在被使用,且剩余过期时间小于1/3超时时间 ,则将其重置
if (Boolean.TRUE.equals(hHasKey(key, field)) && getExpire(key) <= oneThird) {
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
//设置的时候需要的是毫秒数,所以expire需要乘以1000
// execute使用的redis的默认的序列化方式,需要设置参数--arg的序列化方式,以及result结果的序列化方式
// 此处传参只要能转为Object就行(因为数字不能直接强转为String,所以不能用String序列化)
// 结果的类型需要根据脚本定义,此处是数字--定义的是Long类型
Long result = redisTemplate.execute(redisScript, new GenericToStringSerializer<>(Object.class),
new GenericToStringSerializer<>(Long.class), Collections.singletonList(key),
field, releaseTime * 1000);
log.info(LocalDateTime.now() + ": " + threadName + "看门狗自动续期,结果:" + result + ", 续期后过期时间:" +getExpire(key));
}
}else{
cancel();
log.info(LocalDateTime.now() + ": " + threadName + "看门狗关闭");
}
}
}, 10, oneThird * 1000);
}
3.2.调整调用代码:
调整内容其实很简单,就是在获取锁后,调用新增加的看门狗方法,在释放锁后调用设置看门狗结束条件的方法
package com.liu;
import com.liu.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.annotation.Resource;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringRedisLockTest {
@Resource
private Redisson redisson;
@Autowired
private RedisUtils redisUtils;
@Test
public void testA(){
Assert.assertNotNull(redisUtils);
Assert.assertNotNull(redisson);
redisUtils.set("test_lock_count", 100);
//redission实现分布式锁
for (int i = 0; i < 3; i++) {
new Thread(() -> {
//获取一个可重入锁
log.info(Thread.currentThread().getName() + "--开始获取锁......");
RLock lock = redisson.getLock("test_lock");
boolean result = false;
while (!result) {//超过等待时间未获取到后,从新获取,直到获取到为止
try {
// 最多等待30秒(第一个参数)加锁,加锁后60秒(第二个参数)自动过期--释放锁(默认是30秒自动过期--释放锁);
// 设置了过期时间后,锁不会自动续期,需要自定义续期
//(需要锁的key的过期时间比业务处理的时间长,否则会存在问题;最好不要设置永不超时,因为出现异常的时候,容易造成死锁)
Future<Boolean> res = lock.tryLockAsync(100, 5, TimeUnit.SECONDS);
result = res.get();
String field = redisson.getConnectionManager().getId() + ":" + Thread.currentThread().getId();
if (result) {
try{
redisUtils.startRedissonWatchDog("test_lock", field, 5);
log.info(lock.getName() + "----" + Thread.currentThread().getName() + "--我获得了锁,开始处理业务逻辑......");
int stock = Integer.parseInt(redisUtils.get("test_lock_count").toString());
if (stock > 0) {
redisUtils.set("test_lock_count", (stock - 1) + "");
} else {
log.info(Thread.currentThread().getName() + "--处理业务逻辑出错,数量不足!");
}
Thread.sleep(8000); //模拟业务耗时6s
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
log.info(Thread.currentThread().getName() + "--处理业务逻辑处理结束,开始释放锁......");
lock.unlock(); //释放锁
redisUtils.setBusinessStatus(lock.getName() + field, false);
log.info(Thread.currentThread().getName() + "--处理业务逻辑处理结束,锁释放完成!");
}
}else {
log.info(Thread.currentThread().getName() + "--我未获得锁,不能处理业务逻辑,一秒后重新获取!");
Thread.sleep(1000); //模拟业务耗时6s
}
} catch (Exception e) {
e.printStackTrace();
}
}
}, "Thread" + i).start();
}
try {
Thread.sleep(60000);
System.out.println(redisUtils.get("test_lock_count"));
redisUtils.del("test_lock_count");
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
3.3. 测试查看结果:
2022-06-06 16:13:23.860 INFO 6360 --- [ Thread1] com.liu.SpringRedisLockTest : Thread1--开始获取锁......
2022-06-06 16:13:23.860 INFO 6360 --- [ Thread0] com.liu.SpringRedisLockTest : Thread0--开始获取锁......
2022-06-06 16:13:23.861 INFO 6360 --- [ Thread3] com.liu.SpringRedisLockTest : Thread3--开始获取锁......
2022-06-06 16:13:23.860 INFO 6360 --- [ Thread2] com.liu.SpringRedisLockTest : Thread2--开始获取锁......
2022-06-06 16:13:23.904 INFO 6360 --- [ Thread0] com.liu.utils.RedisUtils : 2022-06-06T16:13:23.904: Thread0看门狗开启
2022-06-06 16:13:23.906 INFO 6360 --- [ Thread0] com.liu.SpringRedisLockTest : test_lock----Thread0--我获得了锁,开始处理业务逻辑......
2022-06-06 16:13:27.034 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:27.033: Thread0看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 16:13:30.939 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:30.938: Thread0看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 16:13:31.957 INFO 6360 --- [ Thread0] com.liu.SpringRedisLockTest : Thread0--处理业务逻辑处理结束,开始释放锁......
2022-06-06 16:13:31.963 INFO 6360 --- [ Thread0] com.liu.SpringRedisLockTest : Thread0--处理业务逻辑处理结束,锁释放完成!
2022-06-06 16:13:31.969 INFO 6360 --- [ Thread2] com.liu.utils.RedisUtils : 2022-06-06T16:13:31.968: Thread2看门狗开启
2022-06-06 16:13:31.969 INFO 6360 --- [ Thread2] com.liu.SpringRedisLockTest : test_lock----Thread2--我获得了锁,开始处理业务逻辑......
2022-06-06 16:13:32.948 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:32.948: Thread0看门狗关闭
2022-06-06 16:13:35.002 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:35.002: Thread2看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 16:13:38.045 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:38.045: Thread2看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 16:13:39.983 INFO 6360 --- [ Thread2] com.liu.SpringRedisLockTest : Thread2--处理业务逻辑处理结束,开始释放锁......
2022-06-06 16:13:39.983 INFO 6360 --- [ Thread2] com.liu.SpringRedisLockTest : Thread2--处理业务逻辑处理结束,锁释放完成!
2022-06-06 16:13:39.983 INFO 6360 --- [ Thread3] com.liu.utils.RedisUtils : 2022-06-06T16:13:39.983: Thread3看门狗开启
2022-06-06 16:13:39.983 INFO 6360 --- [ Thread3] com.liu.SpringRedisLockTest : test_lock----Thread3--我获得了锁,开始处理业务逻辑......
2022-06-06 16:13:40.046 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:40.046: Thread2看门狗关闭
2022-06-06 16:13:43.009 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:43.009: Thread3看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 16:13:46.048 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:46.048: Thread3看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 16:13:48.002 INFO 6360 --- [ Thread3] com.liu.SpringRedisLockTest : Thread3--处理业务逻辑处理结束,开始释放锁......
2022-06-06 16:13:48.002 INFO 6360 --- [ Thread3] com.liu.SpringRedisLockTest : Thread3--处理业务逻辑处理结束,锁释放完成!
2022-06-06 16:13:48.017 INFO 6360 --- [ Thread1] com.liu.utils.RedisUtils : 2022-06-06T16:13:48.017: Thread1看门狗开启
2022-06-06 16:13:48.017 INFO 6360 --- [ Thread1] com.liu.SpringRedisLockTest : test_lock----Thread1--我获得了锁,开始处理业务逻辑......
2022-06-06 16:13:48.065 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:48.065: Thread3看门狗关闭
2022-06-06 16:13:51.046 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:51.046: Thread1看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 16:13:54.077 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:54.077: Thread1看门狗自动续期,结果:1, 续期后过期时间:4
2022-06-06 16:13:56.030 INFO 6360 --- [ Thread1] com.liu.SpringRedisLockTest : Thread1--处理业务逻辑处理结束,开始释放锁......
2022-06-06 16:13:56.030 INFO 6360 --- [ Thread1] com.liu.SpringRedisLockTest : Thread1--处理业务逻辑处理结束,锁释放完成!
2022-06-06 16:13:56.092 INFO 6360 --- [ckWatchDogTimer] com.liu.utils.RedisUtils : 2022-06-06T16:13:56.092: Thread1看门狗关闭
更多推荐
所有评论(0)