在日常业务开发的过程中,我们经常会遇到存在高并发的场景,这个时候都会选择使用redis
来实现一个锁,来防止并发。
但是很多时候,我们可能业务完成后,就需要把锁释放掉,给下一个线程用,但是如果我们忘记了释放锁,可能就会存在死锁的问题。(对于使用锁不太熟练的话,这种情况时常发生,虽然很多时候,我们的锁是有过期时间的,但是如果忘记了释放,那么在这个过期时间内,还是会存在大的损失)。
还有一点就是,在我们使用redis实现一个锁的时候,我们需要导入redisClient,设置key,设置过期时间,设置是否锁等等一些重复的操作。前面的哪些步骤,很多都是重复的,所以我们可以想一个方法,来把重复的东西都抽象出来,做成统一的处理,同时哪些变化的值,提供一个设置的入口。
抽出来的东西,我们还可以封装成一个spring-boot-stater,这样我们只需要写一份,就可以在不同的项目中使用了。说干就干,下面我们使用redisson,完成一个自动锁的starter
。
实现
首先,我们分析一下哪些东西是我们需要进行合并,哪些又是需要提供给使用方的。得到下面的一些问题
-
加锁、释放锁过程 我们需要合并起来
-
锁key,加锁时间......这些需要给使用方注入
-
锁的key该怎么去生成(很多时候,我们需要根据业务字段去构造一个key,比如 user:{userId}),那么这个userId该怎么获取?
我们从上面需要解决的问题,去思考需要怎么去实现。我们需要封装一些公共的逻辑,又需要提供一些配置的入库,这样的话,我们可以尝试一种方法,使用 注解+AOP
,通过注解的方式完成加锁、解锁。(很多时候,如果需要抽出一些公共的方法,会用到注解+AOP
去实现)
定义注解
AutoLock 注解
一个锁需要有的信息有,key,加锁的时间,时间单位,是否尝试加锁,加锁等待时间 等等。(如果还有其他的业务需要,可以添加一个扩展内容,自己去解析处理) 那么这个注解的属性就可以知道有哪些了
/**
* 锁的基本信息
*/
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoLock {
/**
* 锁前缀
*/
String prefix() default "anoxia:lock";
/**
* 加锁时间
*/
long lockTime() default 30;
/**
* 是否尝试加锁
*/
boolean tryLock() default true;
/**
* 等待时间,-1 不等待
*/
long waitTime() default -1;
/**
* 锁时间类型
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
LockField 注解
这个注解添加到参数属性上面,用来解决上面提到获取不同的业务参数内容构造key的问题。所以我们需要提供一个获取哪些字段来构造这个key配置,这里需要考虑两个问题:
-
1、参数是基本类型
-
2、参数是引用类型 - 这种类型需要从对象中拿到对象的属性值
/**
-
构建锁的业务数据
-
@author huangle
-
@date 2023/5/5 15:01
*/
@Target({ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LockField {String[] fieldNames() default {};
}
-
定义切面
重点就在这个切面里面,我们需要在这里完成key的合成,锁的获取与释放。整个过程可以分为以下几步
-
获取锁的基本信息,构建key
-
加锁,执行业务
-
业务完成,释放锁
/**
-
自动锁切面
-
处理加锁解锁逻辑
-
@author huangle
-
@date 2023/5/5 14:50
*/
@Aspect
@Component
public class AutoLockAspect {private final static Logger LOGGER = LoggerFactory.getLogger(AutoLockAspect.class);
@Resource
private RedissonClient redissonClient;private static final String REDIS_LOCK_PREFIX = "anoxiaLock";
private static final String SEPARATOR = ":";
/**
- 定义切点
*/
@Pointcut("@annotation(cn.anoxia.lock.annotation.AutoLock)")
public void lockPoincut() {
}
/**
- 定义拦截处理方式
- @return
*/
@Around("lockPoincut()")
public Object doLock(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取需要加锁的方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 获取锁注解
AutoLock autoLock = method.getAnnotation(AutoLock.class);
// 获取锁前缀
String prefix = autoLock.prefix();
// 获取方法参数
Parameter[] parameters = method.getParameters();
StringBuilder lockKeyStr = new StringBuilder(prefix);
Object[] args = joinPoint.getArgs();
// 遍历参数
int index = -1;
LockField lockField;
// 构建key
for (Parameter parameter : parameters) {
Object arg = args[++index];
lockField = parameter.getAnnotation(LockField.class);
if (lockField == null) {
continue;
}
String[] fieldNames = lockField.fieldNames();
if (fieldNames == null || fieldNames.length == 0) {
lockKeyStr.append(SEPARATOR).append(arg);
} else {
List<Object> filedValues = ReflectionUtil.getFiledValues(parameter.getType(), arg, fieldNames);
for (Object value : filedValues) {
lockKeyStr.append(SEPARATOR).append(value);
}
}
}
String lockKey = REDIS_LOCK_PREFIX + SEPARATOR + lockKeyStr;
RLock lock = redissonClient.getLock(lockKey);
// 加锁标志位
boolean lockFlag = false;
try {
long lockTime = autoLock.lockTime();
long waitTime = autoLock.waitTime();
TimeUnit timeUnit = autoLock.timeUnit();
boolean tryLock = autoLock.tryLock();
try {
if (tryLock) {
lockFlag = lock.tryLock(waitTime, lockTime, timeUnit);
} else {
lock.lock(lockTime, timeUnit);
lockFlag = true;
}
}catch (Exception e){
LOGGER.error("加锁失败!,错误信息", e);
throw new RuntimeException("加锁失败!");
}
if (!lockFlag) {
throw new RuntimeException("加锁失败!");
}
// 执行业务
return joinPoint.proceed();
} finally {
// 释放锁
if (lockFlag) {
lock.unlock();
LOGGER.info("释放锁完成,key:{}",lockKey);
}
}
}
- 定义切点
}
-
获取业务属性
这个是一个获取对象中字段的工具类,在一些常用的工具类里面也有实现,可以直接使用也可以自己实现一个
/**
* @author huangle
* @date 2023/5/5 15:17
*/
public class ReflectionUtil {
public static List<Object> getFiledValues(Class<?> type, Object target, String[] fieldNames) throws IllegalAccessException {
List<Field> fields = getFields(type, fieldNames);
List<Object> valueList = new ArrayList();
Iterator fieldIterator = fields.iterator();
while(fieldIterator.hasNext()) {
Field field = (Field)fieldIterator.next();
if (!field.isAccessible()) {
field.setAccessible(true);
}
Object value = field.get(target);
valueList.add(value);
}
return valueList;
}
public static List<Field> getFields(Class<?> claszz, String[] fieldNames) {
if (fieldNames != null && fieldNames.length != 0) {
List<String> needFieldList = Arrays.asList(fieldNames);
List<Field> matchFieldList = new ArrayList();
List<Field> fields = getAllField(claszz);
Iterator fieldIterator = fields.iterator();
while(fieldIterator.hasNext()) {
Field field = (Field)fieldIterator.next();
if (needFieldList.contains(field.getName())) {
matchFieldList.add(field);
}
}
return matchFieldList;
} else {
return Collections.EMPTY_LIST;
}
}
public static List<Field> getAllField(Class<?> claszz) {
if (claszz == null) {
return Collections.EMPTY_LIST;
} else {
List<Field> list = new ArrayList();
do {
Field[] array = claszz.getDeclaredFields();
list.addAll(Arrays.asList(array));
claszz = claszz.getSuperclass();
} while(claszz != null && claszz != Object.class);
return list;
}
}
}
配置自动注入
在我们使用 starter 的时候,都是通过这种方式,来告诉spring在加载的时候,完成这个bean的初始化。这个过程基本是定死的。就是编写配置类,如果通过springBoot的EnableAutoConfiguration
来完成注入。注入后,我们就可以直接去使用这个封装好的锁了。
/**
* @author huangle
* @date 2023/5/5 14:50
*/
@Configuration
public class LockAutoConfig {
@Bean
public AutoLockAspect autoLockAspect(){
return new AutoLockAspect();
}
}
// spring.factories 中内容
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.anoxia.lock.config.LockAutoConfig
测试
我们先打包这个sarter,然后导入到一个项目里面(打包导入的过程就不说了,自己去看一下就可以) 直接上测试类,下面执行后可以看到锁已经完成了释放。如果业务抛出异常导致中断也不用担心锁不会释放的问题,因为我们是在 finally 中释放锁的
/**
* @author huangle
* @date 2023/5/5 14:28
*/
@RestController
@RequestMapping("/v1/user")
public class UserController {
@AutoLock(lockTime = 3, timeUnit = TimeUnit.MINUTES)
@GetMapping("/getUser")
public String getUser(@RequestParam @LockField String name) {
return "hello:"+name;
}
@PostMapping("/userInfo")
@AutoLock(lockTime = 1, timeUnit = TimeUnit.MINUTES)
public String userInfo(@RequestBody @LockField(fieldNames = {"id", "name"}) UserDto userDto){
return userDto.getId()+":"+userDto.getName();
}
}