好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

Springboot-Starter造轮子之自动锁组件lock-starter实现

前言

可能有人会有疑问,为什么外面已经有更好的组件,为什么还要重复的造轮子,只能说,别人的永远是别人的,自己不去造一下,就只能知其然,而不知其所以然。(其实就为了卷)

在日常业务开发的过程中,我们经常会遇到存在高并发的场景,这个时候都会选择使用 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&lt;Object&gt; getFiledValues(Class&lt;?&gt; type, Object target, String[] fieldNames) throws IllegalAccessException {

         List&lt;Field&gt; fields = getFields(type, fieldNames);

         List&lt;Object&gt; 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&lt;Field&gt; getFields(Class&lt;?&gt; claszz, String[] fieldNames) {

         if (fieldNames != null &amp;&amp; fieldNames.length != 0 ) {

             List&lt;String&gt; needFieldList = Arrays.asList(fieldNames);

             List&lt;Field&gt; matchFieldList = new ArrayList();

             List&lt;Field&gt; 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&lt;Field&gt; getAllField(Class&lt;?&gt; claszz) {

         if (claszz == null ) {

             return Collections.EMPTY_LIST;

         } else {

             List&lt;Field&gt; list = new ArrayList();

             do {

                 Field[] array = claszz.getDeclaredFields();

                 list.addAll(Arrays.asList(array));

                 claszz = claszz.getSuperclass();

             } while (claszz != null &amp;&amp; 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

查看更多关于Springboot-Starter造轮子之自动锁组件lock-starter实现的详细内容...

  阅读:12次