spring-data-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秒。可以看到锁被正常续费了,确保了业务的正常执行不会抢占资源。
到此这篇关于spring-data-redis自定义实现看门狗机制的文章就介绍到这了,更多相关spring-data-redis 看门狗内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!