前语

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

在日常事务开发的进程中,咱们经常会遇到存在高并发的场景,这个时分都会挑选运用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();
    }
}

2、Springboot-Starter造轮子之自动锁组件(lock-starter)
2、Springboot-Starter造轮子之自动锁组件(lock-starter)

总结

许多时分,一些公共的事务逻辑都能够被笼统出来成为一个独立的组件而存在,咱们能够在日常开发进程中,不断的去考虑和寻找看哪些能够被笼统出来,哪些能够更加简化一些。然后测验去笼统出一个组件出来,这样的话不但能够训练自己的能力,还能够得到一些很好用的东西,当然自己抽出的组件能够存在问题,可是渐渐的训练下来,总会变的越来越好。 怎样说呢,测验去做,能不能做好再说,做不好就一次又一次的去做。