Springboot-Starter は車輪を再発明します: 自動ロック コンポーネント

序文

「すでに外部に優れたコンポーネントがあるのに、なぜ何度も車輪の再発明をしなければならないのですか?」と疑問を持つ人もいるかもしれませんが、私が言えるのは、他人のものは常に他人のものであるということだけです。それが何であるかは知っていますが、それが何であるかはわかりません。(実際は音量だけです)

日々のビジネス開発のプロセスでは、同時実行性が高いシナリオに遭遇することがよくありますが、このときは同時redis実行性を防ぐためにロックを使用することを選択します。

ただし、多くの場合、業務の完了後、次のスレッドが使用できるようにロックを解放する必要がありますが、ロックの解放を忘れると、デッドロックの問題が発生する可能性があります。(ロックの使用にあまり慣れていない人にとって、この状況はよく起こります。多くの場合、ロックには有効期限がありますが、ロックを解除するのを忘れると、この有効期限の間に大きな損失が発生します)。

もう1つのポイントは、redisを使用してロックを実装する場合、redisClientのインポート、キーの設定、有効期限の設定、ロックするかどうかの設定などの繰り返しの操作が必要であることです。前のステップの多くは繰り返されるため、繰り返されたものを統合処理に抽象化し、同時に変更された値の設定入り口を提供する方法を考えることができます。

また、抽出したものを spring-boot-stater にカプセル化することもできるため、コピーを 1 つ作成するだけで済み、それをさまざまなプロジェクトで使用できます。さあ、redisson を使って 1 つを完成させましょう自动锁的starter

成し遂げる

まず、何をマージする必要があるのか​​、何をユーザーに提供する必要があるのか​​を分析しましょう。以下で質問を受け付けます

  • ロックプロセスとロック解放プロセスをマージする必要があります
  • ロックキー、ロック時間...これらはユーザーに注入する必要があります
  • ロックのキーを生成する方法 (多くの場合、user:{userId} などのビジネス フィールドに基づいてキーを作成する必要があります)、次にこの userId を取得する方法は?

上記の解決すべき問題から出発し、それをどのように実装するかを考えます。いくつかのパブリック ロジックをカプセル化し、構成ストレージを提供する必要がある場合、注解+AOPアノテーションを通じてロックとロック解除を完了する方法を試すことができます。(多くの場合、いくつかのパブリック メソッドを抽出する必要がある場合、それらを使用し注解+AOPて実装します)

注釈を定義する

自動ロックの注釈

ロックに必要な情報には、キー、ロック時間、時間単位、ロックを試行するかどうか、ロック待ち時間などが含まれます。(他のビジネス ニーズがある場合は、拡張機能を追加し、自分で解析して処理できます。) そうすれば、このアノテーションの属性が何であるかを知ることができます。

/**
 * 锁的基本信息
 */
@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 アノテーション

このアノテーションは、キーを構築するために異なるビジネス パラメーターの内容を取得するという上記の問題を解決するために、パラメーター属性に追加されます。したがって、このキー構成を構築するフィールドを取得するメソッドを提供する必要があります。ここで考慮すべき問題が 2 つあります。

  • 1. パラメータは基本的な型です
  • 2. パラメータは参照型です。この型はオブジェクトからオブジェクトの属性値を取得する必要があります。
/**
 * 构建锁的业务数据
 * @author huangle
 * @date 2023/5/5 15:01
 */
@Target({ElementType.PARAMETER})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LockField {

    String[] fieldNames() default {};

}
复制代码

側面を定義する

焦点は、キーの合成、ロックの取得、解放を完了する必要があるこの側面にあります。プロセス全体は次のステップに分けることができます

  • ロックの基本情報を取得してキーを構築する
  • ビジネスをロックして実行する
  • 業務が完了しロックが解除される
/**
 * 自动锁切面
 * 处理加锁解锁逻辑
 *
 * @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;
        }
    }
}
复制代码

自動インジェクションを構成する

スターターを使用する場合、このメソッドを使用して、ロード時に Bean の初期化を完了するように Spring に指示します。このプロセスは基本的には失敗する運命にあります。構成クラスを記述し、 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 をパッケージ化してから、プロジェクトにインポートします (パッケージ化とインポートのプロセスには立ち入りません。ご自身で確認してください)。直接テスト クラスに移動します。以下を実行すると、次のことがわかります。ロックが解除されました。ビジネスが例外をスローして中断が発生した場合でも、最終的にロックを解放するため、ロックが解放されないことを心配する必要はありません。

/**
 * @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();
    }

}
复制代码

要約する

多くの場合、パブリック ビジネス ロジックの一部は抽象化され、独立したコンポーネントとして存在することができますが、日々の開発プロセスでは、何を抽象化できるか、何を簡素化できるかを常に考え、探すことができます。次に、コンポーネントを抽象化してみます。この方法では、自分の能力を発揮できるだけでなく、いくつかの便利なツールも入手できます。もちろん、抽出したコンポーネントには問題があるかもしれませんが、ゆっくりと練習することで、必ず上達します。より良い、より良い。なんというか、やってみてうまくできるかどうか、うまくできなかったら何度でもやりましょう。

おすすめ

転載: blog.csdn.net/2301_76607156/article/details/130557979