最近因为业务需要用到加锁,所以就想用redis锁,因为对于业务来说,redis锁已经能够满足需求了。
但是,因为需要很多地方需要用到加锁,项目又是基于springboot,所以,就想写个springboot-starter,然后封装个注解,需要的项目中只要引入starter,并且在需要加锁的方法上加上注解就可以了。
1、添加pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2、定义锁注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
/**
* 锁key
*/
String key() default "";
/**
* key前缀
*/
String prefix() default "";
/**
* 过期时间,单位毫秒
*/
long expire() default 15000;
/**
* 重试次数
*/
int retryTimes() default 0;
/**
* 重试间隔,单位毫秒
*/
int retryInterval() default 1000;
/**
* 绑定类型(作用于key的生成)
*/
BindType bindType() default BindType.DEFAULT;
/**
* 绑定参数索引,从0开始,与 bindType.ARGS_INDEX 组合使用
*/
int[] bindArgsIndex() default 0;
/**
* 对象参数属性 示例:ClassName.field, 与bingType.OBJECT_PROPERTIES 组合使用
*/
String[] properties() default "";
/**
* 失败策略
*/
ErrorStrategy errorStrategy() default ErrorStrategy.THROW_EXCEPTION;
/**
* 参数key绑定类型
*/
enum BindType {
/**
* 默认,将所有参数toString
*/
DEFAULT,
/**
* 参数索引,从0开始
*/
ARGS_INDEX,
/**
* 对象属性
*/
OBJECT_PROPERTIES;
}
/**
* 获取锁失败策略
*/
enum ErrorStrategy {
/**
* 抛异常
*/
THROW_EXCEPTION,
/**
* 返回NULL
*/
RETURN_NULL;
}
}
3、定义加锁逻辑
@Slf4j
public class DistributedRedisLock {
private RedisTemplate<Object, Object> redisTemplate;
private ThreadLocal<String> lockKey = new ThreadLocal<>();
public static final String UNLOCK_LUA;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}
public DistributedRedisLock(RedisTemplate<Object, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 加锁
* @param key 锁key
* @param expire 过期时间
* @param retryTimes 重试次数
* @param retryInterval 重试间隔
* @return true 加锁成功, false 加锁失败
*/
public boolean lock(String key, long expire, int retryTimes, long retryInterval) {
boolean result = setRedisLock(key, expire);
/**
* 如果获取锁失败,进行重试
*/
while((!result) && retryTimes-- > 0){
try {
log.info("lock failed, retrying..." + retryTimes);
Thread.sleep(retryInterval);
} catch (InterruptedException e) {
return false;
}
result = setRedisLock(key, expire);
}
return result;
}
/**
* 释放锁
* @param key 锁key
* @return true 释放成功, false 释放失败
*/
public boolean unLock(String key) {
/**
* 释放锁的时候,有可能因为持锁之后方法执行时间大于锁的有效期,此时有可能已经被另外一个线程持有锁,所以不能直接删除
* 使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁
*/
try {
RedisCallback<Boolean> callback = (connection) -> {
String value = lockKey.get();
return connection.eval(UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN ,1, key.getBytes(), value.getBytes());
};
return redisTemplate.execute(callback);
} catch (Exception e) {
log.error("release lock occured an exception", e);
} finally {
lockKey.remove();
}
return false;
}
/**
* 设置redis锁
* @param key 锁key
* @param expire 过期时间
* @return true 设置成功,false 设置失败
*/
private boolean setRedisLock(String key, long expire) {
try {
RedisCallback<Boolean> callback = (connection) -> {
String uuid = UUID.randomUUID().toString();
lockKey.set(uuid);
return connection.set(key.getBytes(), uuid.getBytes(), Expiration.milliseconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT);
};
return redisTemplate.execute(callback);
}catch (Exception e){
log.error("set redis error", e);
}
return false;
}
}
4、定义redis锁切面
@Slf4j
@Aspect
public class DistributedRedisLockAspect {
private DistributedRedisLock distributedRedisLock;
public DistributedRedisLockAspect(DistributedRedisLock distributedRedisLock){
this.distributedRedisLock = distributedRedisLock;
}
@Pointcut("@annotation(com.springboot.starter.redis.annotation.RedisLock)")
private void redisLockPoint(){}
@Around("redisLockPoint() && @annotation(redisLock)")
public Object around(ProceedingJoinPoint pjp, RedisLock redisLock) throws Throwable {
String key = redisLock.key();
if(StringUtils.isBlank(key)){
Object[] args = pjp.getArgs();
if(redisLock.bindType().equals(RedisLock.BindType.DEFAULT)){
key = StringUtils.join(args);
}else if(redisLock.bindType().equals(RedisLock.BindType.ARGS_INDEX)){
key = getArgsKey(redisLock, args);
}else if(redisLock.bindType().equals(RedisLock.BindType.OBJECT_PROPERTIES)){
key = getObjectPropertiesKey(redisLock, args);
}
}
Assert.hasText(key, "key does not exist");
String prefix = redisLock.prefix();
boolean lock = distributedRedisLock.lock(prefix + key, redisLock.expire(), redisLock.retryTimes(), redisLock.retryInterval());
if(!lock) {
log.error("get lock failed : " + key);
if(redisLock.errorStrategy().equals(RedisLock.ErrorStrategy.THROW_EXCEPTION)){
throw new RedisLockException("Get redis lock failed");
}
return null;
}
log.info("get lock success : {}" ,key);
try {
return pjp.proceed();
}finally {
boolean result = distributedRedisLock.unLock(prefix + key);
log.info("release lock : {} {}", prefix + key ,result ? " success" : " failed");
}
}
/**
* 通过绑定的args生成key
* @param redisLock redisLock注解
* @param args 所有参数
* @return key
*/
private String getArgsKey(RedisLock redisLock, Object[] args){
int[] index = redisLock.bindArgsIndex();
Assert.notEmpty(Arrays.asList(index), "ArgsIndex is empty");
int len = index.length;
Object[] indexArgs = new Object[index.length];
for(int i = 0; i < len; i++){
indexArgs[i] = args[index[i]];
}
return StringUtils.join(indexArgs);
}
/**
* 通过绑定的对象属性生成key
* @param redisLock redisLock注解
* @param args 所有参数
* @return key
*/
private String getObjectPropertiesKey(RedisLock redisLock, Object[] args) throws NoSuchFieldException, IllegalAccessException {
String[] properties = redisLock.properties();
List<Object> keylist = new ArrayList<>(properties.length);
// 可以通过className获取args的位置
Map<String, Integer> classNamesArgsIndex = getClassNameArgsIndex(args);
// 可以通过className获取Class类型
Map<String, Class<?>> classNameClass = getClassNameClass(args);
for (String ppts : properties) {
String[] classProperties = StringUtils.split(ppts, ".");
String className = classProperties[0];
String propertiesName = classProperties[1];
Object argObject = args[classNamesArgsIndex.get(className)];
Class<?> clazz = classNameClass.get(className);
Field field = clazz.getDeclaredField(propertiesName);
field.setAccessible(true);
Object object = field.get(argObject);
keylist.add(object);
}
return StringUtils.join(keylist.toArray());
}
/**
* 获取类名和参数位置的对应关系
* @param args 所有参数
* @return Map<类名, 参数位置>
*/
private Map<String, Integer> getClassNameArgsIndex(Object[] args){
int len = args.length;
Map<String, Integer> nameIndex = new HashMap<>();
for(int i = 0; i < len; i++){
String name = StringUtils.substringAfterLast(args[i].getClass().toString(), ".");
nameIndex.put(name, i);
}
return nameIndex;
}
/**
* 获取类名和类的对应关系
* @param args 所有参数
* @return Map<类名, 类>
*/
private Map<String, Class<?>> getClassNameClass(Object[] args){
int len = args.length;
Map<String, Class<?>> nameClass = new HashMap<>();
for(int i = 0; i < len; i++){
Class<?> clazz = args[i].getClass();
String name = StringUtils.substringAfterLast(clazz.toString(), ".");
nameClass.put(name, clazz);
}
return nameClass;
}
5、定义springboot的autoConfiguration配置类
@Slf4j
@Configuration
public class ReidsLockAutoConfiguration {
@Bean
public DistributedRedisLock distributedRedisLock(RedisTemplate redisTemplate){
log.info("init Distributed Redis Lock");
return new DistributedRedisLock(redisTemplate);
}
@Bean
public DistributedRedisLockAspect distributedRedisLockAspect(DistributedRedisLock distributedRedisLock){
log.info("init Distributed Redis Lock Aspect");
return new DistributedRedisLockAspect(distributedRedisLock);
}
}
6、在resources/META-INF/下添加spring.facotries文件
这样一个可以加redis锁注解的springboot-starter就封装完成了,只要引入该starter,配置上redis连接地址,就可以通过@RedisLock() 进行加锁了。