Springboot-Starter reinvents the wheel: automatic lock component

Preface

Some people may have questions, why do we have to repeatedly reinvent the wheel when there are already better components outside? I can only say that what belongs to others will always belong to others. If you don’t build it yourself, you will only know what it is, but not what it is. So. (actually just for the volume)

In the process of daily business development, we often encounter scenarios with high concurrency. At this time, we will choose to use redisa lock to prevent concurrency.

But many times, after the business is completed, we may need to release the lock for the next thread to use. However, if we forget to release the lock, there may be a deadlock problem. (For those who are not very skilled in using locks, this situation often happens. Although many times our locks have an expiration time, if we forget to release them, there will still be big losses during this expiration time).

Another point is that when we use redis to implement a lock, we need to import redisClient, set the key, set the expiration time, set whether to lock, and other repeated operations. Many of the previous steps are repeated, so we can think of a way to abstract the repeated things into unified processing, and at the same time provide a setting entrance for the changed values.

We can also encapsulate the extracted things into a spring-boot-stater, so that we only need to write one copy and can use it in different projects. Just do it, let's use redisson to complete one 自动锁的starter.

accomplish

First, let's analyze what we need to merge and what we need to provide to users. Get some questions below

  • We need to merge the locking and lock releasing processes
  • Lock key, lock time... these need to be injected into the user
  • How to generate the key of the lock (many times, we need to construct a key based on the business field, such as user:{userId}), then how to obtain this userId?

We start from the problems that need to be solved above and think about how to implement them. We need to encapsulate some public logic and provide some configuration storage. In this case, we can try a method to 注解+AOPcomplete locking and unlocking through annotations. (Many times, if you need to extract some public methods, you will use them 注解+AOPto implement them)

Define annotations

AutoLock annotation

The information required for a lock includes key, locking time, time unit, whether to try to lock, lock waiting time, etc. (If you have other business needs, you can add an extension and parse and process it yourself) Then you can know what the attributes of this annotation are.

/**
 * 锁的基本信息
 */
@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 annotation

This annotation is added to the parameter attribute to solve the problem mentioned above of obtaining different business parameter contents to construct keys. So we need to provide a method to obtain which fields to construct this key configuration. There are two issues to consider here:

  • 1. Parameters are basic types
  • 2. The parameter is a reference type - this type needs to get the attribute value of the object from the object
/**
 * 构建锁的业务数据
 * @author huangle
 * @date 2023/5/5 15:01
 */
@Target({ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LockField {

    String[] fieldNames() default {};

}
复制代码

Define aspects

The focus is on this aspect, where we need to complete key synthesis, lock acquisition and release. The whole process can be divided into the following steps

  • Obtain the basic information of the lock and build the key
  • Lock and execute business
  • The business is completed and the lock is released
/**
 * 自动锁切面
 * 处理加锁解锁逻辑
 *
 * @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);
            }
        }
    }


}
复制代码

Get business attributes

This is a tool class for obtaining fields in objects. It is also implemented in some commonly used tool classes. You can use it directly or implement one yourself.

/**
 * @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;
        }
    }
}
复制代码

Configure automatic injection

When we use starter, we use this method to tell spring to complete the initialization of the bean when loading. This process is basically doomed. Just write the configuration class and complete the injection through springBoot EnableAutoConfiguration. After injection, we can directly use the encapsulated lock.

/**
 * @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
复制代码

test

We first package this Sarter, and then import it into a project (I won’t go into the process of packaging and importing, just check it out yourself). Go directly to the test class. After executing the following, you can see that the lock has been released. If the business throws an exception and causes an interruption, there is no need to worry about the lock not being released, because we release the lock in 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();
    }

}
复制代码

Summarize

Many times, some public business logic can be abstracted and exist as an independent component. In the daily development process, we can constantly think and look for what can be abstracted and what can be simplified. Then try to abstract a component. In this way, you can not only exercise your abilities, but also get some useful tools. Of course, there may be problems with the components you extract, but as you exercise slowly, you will always become better and better. The better. How should I put it? Try it and see if you can do it well. If you can’t do it well, do it again and again.

Guess you like

Origin blog.csdn.net/2301_76607156/article/details/130557979