最近因爲業務需要用到加鎖,所以就想用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() 進行加鎖了。