前言
可能有人会有疑问,为什么外面已经有更好的组件,为什么还要重复的造轮子,只能说,别人的永远是别人的,自己不去造一下,就只能知其然,而不知其所以然。(其实就为了卷)
在日常业务开发的过程中,我们经常会遇到存在高并发的场景,这个时候都会选择使用redis
来实现一个锁,来防止并发。
但是很多时候,我们可能业务完成后,就需要把锁释放掉,给下一个线程用,但是如果我们忘记了释放锁,可能就会存在死锁的问题。(对于使用锁不太熟练的话,这种情况时常发生,虽然很多时候,我们的锁是有过期时间的,但是如果忘记了释放,那么在这个过期时间内,还是会存在大的损失)。
还有一点就是,在我们使用redis实现一个锁的时候,我们需要导入redisClient,设置key,设置过期时间,设置是否锁等等一些重复的操作。前面的哪些步骤,很多都是重复的,所以我们可以想一个方法,来把重复的东西都抽象出来,做成统一的处理,同时哪些变化的值,提供一个设置的入口。
抽出来的东西,我们还可以封装成一个spring-boot-stater,这样我们只需要写一份,就可以在不同的项目中使用了。 说干就干,下面我们使用redisson,完成一个自动锁的starter
。
实现
首先,我们分析一下哪些东西是我们需要进行合并,哪些又是需要提供给使用方的。得到下面的一些问题
- 加锁、释放锁过程 我们需要合并起来
- 锁key,加锁时间......这些需要给使用方注入
- 锁的key该怎么去生成(很多时候,我们需要根据业务字段去构造一个key,比如 user:{userId}),那么这个userId该怎么获取?
我们从上面需要解决的问题,去思考需要怎么去实现。我们需要封装一些公共的逻辑,又需要提供一些配置的入库,这样的话,我们可以尝试一种方法,使用 注解+AOP
,通过注解的方式完成加锁、解锁。(很多时候,如果需要抽出一些公共的方法,会用到注解+AOP
去实现)
定义注解
AutoLock 注解
一个锁需要有的信息有,key,加锁的时间,时间单位,是否尝试加锁,加锁等待时间 等等。(如果还有其他的业务需要,可以添加一个扩展内容,自己去解析处理) 那么这个注解的属性就可以知道有哪些了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
/** * 锁的基本信息 */ @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、参数是引用类型 - 这种类型需要从对象中拿到对象的属性值
1
2
3
4
5
6
7
8
9
10
11
|
/** * 构建锁的业务数据 * @author huangle * @date 2023/5/5 15:01 */ @Target ({ElementType.PARAMETER}) @Documented @Retention (RetentionPolicy.RUNTIME) public @interface LockField { String[] fieldNames() default {}; } |
定义切面
重点就在这个切面里面,我们需要在这里完成key的合成,锁的获取与释放。整个过程可以分为以下几步
- 获取锁的基本信息,构建key
- 加锁,执行业务
- 业务完成,释放锁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
/** * 自动锁切面 * 处理加锁解锁逻辑 * * @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); } } } } |
获取业务属性
这个是一个获取对象中字段的工具类,在一些常用的工具类里面也有实现,可以直接使用也可以自己实现一个
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
/** * @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
来完成注入。注入后,我们就可以直接去使用这个封装好的锁了。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/** * @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 中释放锁的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/** * @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(); } } |
总结
很多时候,一些公共的业务逻辑都可以被抽象出来成为一个独立的组件而存在,我们可以在日常开发过程中,不断的去思考和寻找看哪些可以被抽象出来,哪些可以更加简化一些。然后尝试去抽象出一个组件出来,这样的话不但可以锻炼自己的能力,还可以得到一些很好用的工具,当然自己抽出的组件可以存在问题,但是慢慢的锻炼下来,总会变的越来越好。 怎么说呢,尝试去做,能不能做好再说,做不好就一次又一次的去做。
以上就是Springboot-Starter造轮子之自动锁组件(lock-starter)的详细内容,更多关于Springboot-Starter自动锁组件(lock-starter)的资料请关注服务器之家其它相关文章!
原文链接:https://juejin.cn/post/7229615985807982649