【spring boot】【redis】spring boot基于redis的LUA脚本 实现分布式锁
spring boot基于redis的LUA脚本 实现分布式锁【都是基于redis单点下】
一.spring boot 1.5.X 基于redis 的 lua脚本实现分布式锁
1.pom.xml
<!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
2.RedisLock 工具类 (注入spring)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
import org.springframework.beans.factory.annotation.Autowired; 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.StringRedisSerializer; import org.springframework.stereotype.Component; import java.util.Collections; /** * spring boot 1.5.X * 使用redis 的 lua脚本 基于单点实现分布式锁 * * lua脚本作为原子性操作,保证加锁和设置超时时间 为原子性操作 * @author sxd * @date 2019/5/27 10:52 */ @Component public class RedisLock { @Autowired RedisTemplate redisTemplate; private static final Long SUCCESS = 1L; /** * 获取锁 * * @param lockKey redis的key * @param value redis的value要求是随机串,防止释放其他请求的锁 * @param expireTime redis的key 的过期时间 防止死锁,导致其他请求无法正常执行业务 * @return */ public boolean lock(String lockKey, String value, int expireTime) { String script = "if redis.call('setNx',KEYS[1],ARGV[1]) then " + " if redis.call('get',KEYS[1])==ARGV[1] then " + " return redis.call('expire',KEYS[1],ARGV[2]) " + " else " + " return 0 " + " end " + "end"; RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class); //对非string类型的序列化 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value, String.valueOf(expireTime)); return SUCCESS.equals(result); } /** * 释放锁 * * @param lockKey redis的key * @param value redis的value 只有value比对一致,才能确定是本请求 加的锁 才能正常释放 * @return */ public boolean unlock(String lockKey, String value) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; RedisScript<String> redisScript = new DefaultRedisScript<>(script, String.class); try { Object result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), value); if (SUCCESS.equals(result)) { return true; } } catch (Exception e) { e.printStackTrace(); } return false; } }
3.controller使用
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/** * 使用分布式锁 逻辑 * 1.准备好 key value expireTime * value要求是随机字符串 * expireTime 是根据业务 衡量决定的 锁过期时间 * * 2.获取锁 * 成功获取,则执行业务,执行完成,释放锁 * 失败获取,则重试获取,注意获取锁的时间间隔,直到获取成功,执行业务,最后释放锁 * * 注意: * 对于redis加锁的业务,尽量用在耗时短的业务上。 * */ @RequestMapping("/test") public void test(){ boolean flag = false; //标识 是否正常获取锁 String uuid = UUID.randomUUID().toString(); //redis的value 是一串随机数 flag = lock.lock("mykey1",uuid,5); if (flag){ business(uuid); }else { //如果未正常获取锁 可以通过重试 直到获取锁成功 while (!flag){ try { //重试 时间间隔 减少与redis交互次数 Thread.sleep(3000); System.out.println("重试"); flag = lock.lock("mykey1",uuid,5); if (flag){ business(uuid); }else { continue; } } catch (InterruptedException e) { e.printStackTrace(); } } } } public void business(String uuid){ try { System.out.println("加锁成功,执行业务"); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); }finally { //业务执行完成 正常释放锁 lock.unlock("mykey1",uuid); } }
二.spring boot 2.x 基于redis 的LUA脚本 实现分布式锁
1.pom.xml
<!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--spring2.0集成redis所需common-pool2--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.2</version> </dependency> <!-- 使用redis的LUA脚本 需要序列化操作的jar--> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
2.替代SpringBoot自动配置的RedisTemplate的RedisConfig类
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.sxd.swapping.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.io.Serializable; /** * @author sxd * @date 2019/5/27 16:13 */ /** * @Description Redis配置类,替代SpringBoot自动配置的RedisTemplate,参加RedisAutoConfiguration */ @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) public class RedisConfig { @Bean public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Serializable> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); //Jackson序列化器 Jackson2JsonRedisSerializer<Object> 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); //字符串序列化器 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); //普通Key设置为字符串序列化器 template.setKeySerializer(stringRedisSerializer); //Hash结构的key设置为字符串序列化器 template.setHashKeySerializer(stringRedisSerializer); //普通值和hash的值都设置为jackson序列化器 template.setValueSerializer(jackson2JsonRedisSerializer); template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }
3.RedisLock工具类,自动注入Spring
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
package com.sxd.swapping.utils; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import java.util.Arrays; import java.util.List; /** * * spring boot 2.x版本 * @author sxd * @date 2019/5/27 16:11 */ @Component public class RedisLock2 { Logger logger = Logger.getRootLogger(); static final Long LOCK_SUCCESS = 1L; static final Long LOCK_EXPIRED = -1L; @Autowired RedisTemplate redisTemplate; //定义获取锁的lua脚本 private final static DefaultRedisScript<Long> LOCK_LUA_SCRIPT = new DefaultRedisScript<>( "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end" , Long.class ); //定义释放锁的lua脚本 private final static DefaultRedisScript<Long> UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>( "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return -1 end" , Long.class ); /** * 加锁 * @param key redis键值对 的 key * @param value redis键值对 的 value 随机串作为值 * @param timeout redis键值对 的 过期时间 pexpire 以毫秒为单位 * @param retryTimes 重试次数 即加锁失败之后的重试次数,根据业务设置大小 * @return */ public boolean lock(String key,String value ,long timeout, int retryTimes) { try { logger.debug("加锁信息:lock :::: redisKey = " + key + " requestid = " + value); //组装lua脚本参数 List<String> keys = Arrays.asList(key); //执行脚本 Object result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys,value,timeout); //存储本地变量 if(LOCK_SUCCESS.equals(result)) { logger.info("成功加锁:success to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result); return true; } else if (retryTimes == 0) { //重试次数为0直接返回失败 return false; } else { //重试获取锁 logger.info("重试加锁:retry to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result); int count = 0; while(true) { try { //休眠一定时间后再获取锁,这里时间可以通过外部设置 Thread.sleep(100); result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys); if(LOCK_SUCCESS.equals(result)) { logger.info("成功加锁:success to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result); return true; } else { count++; if (retryTimes == count) { logger.info("加锁失败:fail to acquire lock for " + Thread.currentThread().getName() + ", Status code reply:" + result); return false; } else { logger.warn(count + " times try to acquire lock for " + Thread.currentThread().getName() + ", Status code reply:" + result); continue; } } } catch (Exception e) { logger.error("加锁异常:acquire redis occured an exception:" + Thread.currentThread().getName(), e); break; } } } } catch (Exception e1) { logger.error("加锁异常:acquire redis occured an exception:" + Thread.currentThread().getName(), e1); } return false; } /** * 释放KEY * @param key 释放本请求对应的锁的key * @param value 释放本请求对应的锁的value 是不重复随即串 用于比较,以免释放别的线程的锁 * @return */ public boolean unlock(String key,String value) { try { //组装lua脚本参数 List<String> keys = Arrays.asList(key); logger.debug("解锁信息:unlock :::: redisKey = " + key + " requestid = " + value); // 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁 Object result = redisTemplate.execute(UNLOCK_LUA_SCRIPT, keys, value); //如果这里抛异常,后续锁无法释放 if (LOCK_SUCCESS.equals(result)) { logger.info("解锁成功:release lock success:" + Thread.currentThread().getName() + ", Status code reply=" + result); return true; } else if (LOCK_EXPIRED.equals(result)) { //返回-1说明获取到的KEY值与requestId不一致或者KEY不存在,可能已经过期或被其他线程加锁 // 一般发生在key的过期时间短于业务处理时间,属于正常可接受情况 logger.warn("解锁异常:release lock exception:" + Thread.currentThread().getName() + ", key has expired or released. Status code reply=" + result); } else { //其他情况,一般是删除KEY失败,返回0 logger.error("解锁失败:release lock failed:" + Thread.currentThread().getName() + ", del key failed. Status code reply=" + result); } } catch (Exception e) { logger.error("解锁异常:release lock occured an exception", e); } return false; } }
4.使用
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
@Autowired RedisLock2 lock2; @Autowired RedisTemplate redisTemplate; @RequestMapping("/test3") public void test3(){ ValueOperations vops = redisTemplate.opsForValue(); String uuid = UUID.randomUUID().toString(); //加锁 if (lock2.lock("mykey1",uuid,5000,3)){ try { // 执行业务 System.out.println("加锁成功,做业务"); vops.increment(REDIS_COUNT_KEY,1); Thread.sleep(3000); System.out.println("业务执行结束"); } catch (InterruptedException e) { e.printStackTrace(); }finally { //解锁 lock2.unlock("mykey1",uuid); } } }