文章目录
- 前言
- redission分布式锁看门狗机制简单流程图
- spring-data-redis实现看门狗机制指南开始
- 引入依赖
- 配置redis连接以及基础配置
- 实现redis分布式锁工具类
- 直接失败和锁重试机制实现
- 效果图展示
前言
项目中使用redis分布式锁解决了点赞和楼层排序得问题,所以这里就对这个redis得分布式锁进行了学习,一般使用得是redission提供得分布式锁解决得这个问题,但是知其然更要知其所以然,所以自己就去找了一些资料以及也实践了一下就此记录分享一下。
redission分布式锁看门狗机制简单流程图
spring-data-redis实现看门狗机制指南开始
引入依赖
<!--redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--json工具包-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
配置redis连接以及基础配置
spring:
redis:
host: localhost
port: 6379
lettuce:
timeout: 200000
database: 1
在Spring Boot的配置类中创建一个RedisTemplate的Bean:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
// 我们为了自己开发方便,一般直接使用 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(connectionFactory);
// Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String 的序列化
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;
}
}
实现redis分布式锁工具类
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
@Component
public class RedisLockUtils {
@Resource
private RedisTemplate redisTemplate;
private volatile static Map<String, LockInfo> lockInfoMap = new ConcurrentHashMap<>();
private static final Long SUCCESS = 1L;
public static class LockInfo {
private String key;
private String value;
private int expireTime;
//更新时间
private long renewalTime;
//更新间隔
private long renewalInterval;
public static LockInfo getLockInfo(String key, String value, int expireTime) {
LockInfo lockInfo = new LockInfo();
lockInfo.setKey(key);
lockInfo.setValue(value);
lockInfo.setExpireTime(expireTime);
lockInfo.setRenewalTime(System.currentTimeMillis());
lockInfo.setRenewalInterval(expireTime*1000 *2 / 3);
return lockInfo;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public int getExpireTime() {
return expireTime;
}
public void setExpireTime(int expireTime) {
this.expireTime = expireTime;
}
public long getRenewalTime() {
return renewalTime;
}
public void setRenewalTime(long renewalTime) {
this.renewalTime = renewalTime;
}
public long getRenewalInterval() {
return renewalInterval;
}
public void setRenewalInterval(long renewalInterval) {
this.renewalInterval = renewalInterval;
}
}
/**
* 使用lua脚本更新redis锁的过期时间
* @param lockKey
* @param value
* @return 成功返回true, 失败返回false
*/
public boolean renewal(String lockKey, String value, int expireTime) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Boolean.class);
redisScript.setScriptText(luaScript);
List<String> keys = new ArrayList<>();
keys.add(lockKey);
Object result = redisTemplate.execute(redisScript, keys, value, expireTime);
log.info("更新redis锁的过期时间:{}", result);
return (boolean) result;
}
/**
* @param lockKey 锁
* @param value 身份标识(保证锁不会被其他人释放)
* @param expireTime 锁的过期时间(单位:秒)
* @return 成功返回true, 失败返回false
*/
public boolean lock(String lockKey, String value, int expireTime) {
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(lockKey, value, expireTime, TimeUnit.SECONDS);
if(aBoolean){
lockInfoMap.put(String.valueOf(Thread.currentThread().getId()),LockInfo.getLockInfo(lockKey,value,expireTime));
}
return aBoolean;
}
/**
* redisTemplate解锁
* @param key
* @param value
* @return 成功返回true, 失败返回false
*/
public boolean unlock2(String key, String value) {
Object currentValue = redisTemplate.opsForValue().get(key);
boolean result = false;
if (StringUtils.isNotEmpty(String.valueOf(currentValue)) && currentValue.equals(value)) {
lockInfoMap.remove(String.valueOf(Thread.currentThread().getId()));
result = redisTemplate.opsForValue().getOperations().delete(key);
}
return result;
}
/**
* 定时去检查redis锁的过期时间
*/
@Scheduled(fixedRate = 1000)
@Async("redisExecutor")
public void renewal() {
long now = System.currentTimeMillis();
for (Map.Entry<String, LockInfo> lockInfoEntry : lockInfoMap.entrySet()) {
LockInfo lockInfo = lockInfoEntry.getValue();
System.out.println("++"+lockInfo.key);
if (lockInfo.getRenewalTime() + lockInfo.getRenewalInterval() < now) {
renewal(lockInfo.getKey(), lockInfo.getValue(), lockInfo.getExpireTime());
lockInfo.setRenewalTime(now);
log.info("lockInfo {}", JSON.toJSONString(lockInfo));
}
}
}
/**
* 分布式锁设置单独线程池
* @return
*/
@Bean("redisExecutor")
public ThreadPoolTaskExecutor redisExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(1);
executor.setQueueCapacity(1);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("redis-renewal-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());
executor.initialize();
return executor;
}
}
这个类是一个用于实现分布式锁的工具类,主要提供了以下功能:
-
renewal()
方法:定时检查并更新 Redis 锁的过期时间。该方法使用@Scheduled
注解进行定时执行,通过遍历lockInfoMap
中保存的锁信息,判断是否需要更新锁的过期时间,并调用renewal()
方法进行更新。 -
renewal(String lockKey, String value, int expireTime)
方法:使用 Lua 脚本更新 Redis 锁的过期时间。该方法首先定义了一个 Lua 脚本,然后使用redisTemplate.execute()
方法执行该脚本,并传入相应的参数。如果执行成功,则返回 true,否则返回 false。 -
lock(String lockKey, String value, int expireTime)
方法:获取分布式锁。该方法使用 Redis 的setIfAbsent()
方法尝试将锁的键值对存储到 Redis 中,并设置相应的过期时间。如果存储成功,则返回 true,表示获取锁成功;否则返回 false,表示获取锁失败。 -
unlock2(String key, String value)
方法:释放分布式锁。该方法首先获取 Redis 中当前的锁值,然后判断锁值是否和传入的 value 相等。如果相等,则从lockInfoMap
中移除锁信息,并调用 Redis 的delete()
方法删除锁的键值对。最后返回删除结果,表示是否成功释放锁。 -
redisExecutor()
方法:配置一个单独的线程池用于执行renewal()
方法。该方法创建一个ThreadPoolTaskExecutor
对象,并设置相关的属性,如核心线程数、最大线程数、队列容量等。
直接失败和锁重试机制实现
直接失败的方式,就是调用获取锁的方法判断是否加锁成功,失败则直接中断方法执行返回
@GetMapping("/test2")
public Object test2(){
boolean a = redisLockUtils.lock("A", "111", 5);
if(!a){
return "获取锁失败";
}
try{
System.out.println("执行开始-------------------test2");
TimeUnit.SECONDS.sleep(12);
System.out.println("执行结束-------------------test2");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
redisLockUtils.unlock2("A","111");
System.out.println("释放锁-----------------------test2");
}
return null;
}
锁重试,这里是封装了获取锁的方法,加入了一个重试次数的限制,通过使用while循环去尝试获取锁。
private Boolean getLock(int tryNum, String key, String value, int exp) throws InterruptedException {
int i = 0; // 初始化计数器,记录尝试获取锁的次数
boolean flag = false; // 初始化标志变量,表示获取锁的结果
while (true) { // 循环进行尝试获取锁的操作
if (i == tryNum) { // 判断是否达到尝试获取锁的最大次数
return flag; // 返回当前的获取锁结果
}
flag = redisLockUtils.lock(key, value, exp); // 尝试获取锁,返回是否成功获取到锁的结果
if (flag) { // 如果成功获取到锁
return flag; // 直接返回获取锁结果为 true
} else { // 如果未能成功获取到锁
i++; // 计数器加一,表示已经尝试了一次获取锁的操作
TimeUnit.SECONDS.sleep(1); // 暂停一秒钟,等待一段时间后再进行下一次获取锁的尝试
}
}
}
效果图展示
这里设置的锁过期是5秒每隔2/3的时间也就是4秒进行一次续期一共续了3次,因为我中间让线程睡了12秒。可以看到锁被正常续费了,确保了业务的正常执行不会抢占资源。