Implémentation élégante de l'idempotence de l'interface basée sur des annotations

1. Qu'est-ce que l'idempotence

Pour faire simple, il s'agit d'effectuer plusieurs requêtes répétées sur une interface, et le résultat d'une requête est le même. Cela semble très simple à comprendre, mais il faut être très rigoureux pour vraiment maintenir cet objectif dans le système. , dans l'environnement de production actuel, nous devons nous assurer que toute interface est idempotente, et comment implémenter correctement l'idempotence est ce que discutera cet article.

2. Quelles demandes sont intrinsèquement idempotentes

Tout d'abord, il faut savoir que les requêtes de requête sont généralement idempotentes par nature. De plus, les requêtes de suppression sont également idempotentes dans la plupart des cas, sauf dans les scénarios ABA.

donne un exemple simple

Par exemple, l'opération de suppression de A est demandée en premier, mais en raison du délai de réponse, une autre opération de suppression de A est automatiquement demandée. Si A est à nouveau inséré entre les deux requêtes, mais en fait, le A nouvellement inséré l'est. être supprimé. C'est le problème de l'ABA. Cependant, dans la plupart des scénarios commerciaux, le problème de l'ABA peut être ignoré.

En plus de l'interrogation et de la suppression, il existe également des opérations de mise à jour. La même opération de mise à jour est également naturellement idempotente dans la plupart des scénarios. L'exception est qu'il y aura également des problèmes ABA. Plus important encore, par exemple, l'exécution de telles mises à jour n'est pas idempotente update table set a = a + 1 where v = 1. .

Au final, il n'y a que l'insertion, qui est non idempotente dans la plupart des cas, à moins que l'index unique de la base de données ne soit utilisé pour garantir que les données ne seront pas dupliquées.

3. Pourquoi avez-vous besoin d'idempotence ?

1. Nouvelle tentative d'expiration du délai

Lorsqu'une requête RPC est lancée, il est inévitable que la requête échoue en raison de l'instabilité du réseau. Généralement, lorsque nous rencontrons un tel problème, nous espérons pouvoir la redemander. Normalement, il n'y a pas de problème, mais parfois la requête a effectivement été envoyé, juste dans la demande Le réseau est anormal ou a expiré pendant la réponse.À ce moment, si le demandeur lance à nouveau une demande, la partie requise doit s'assurer de l'idempotence.

2. Rappel asynchrone

Le rappel asynchrone est un moyen courant d'améliorer le débit des interfaces système. De telles interfaces doivent évidemment garantir l'idempotence.

3. File d'attente des messages

Les frameworks de file d'attente de messages désormais couramment utilisés, tels que : Kafka, RocketMQ, RabbitMQ, adopteront le principe Au moins une fois (c'est-à-dire le principe d'au moins une fois, lorsque le message est livré, le message ne peut pas être perdu, mais les messages répétés sont autorisés), puisque la file d'attente des messages ne garantit pas qu'il n'y aura pas de messages en double, le consommateur doit donc naturellement s'assurer de l'idempotence de la logique de traitement.

4. Facteurs clés pour atteindre l’idempotence

facteur clé 1

L'identifiant unique idempotent peut être appelé numéro idempotent ou jeton idempotent ou identifiant global. En bref, c'est l'identifiant unique d'une requête entre le client et le serveur. Généralement, il est généré par le client, ou il peut être unifié par un tiers.

facteur clé 2

Avec l'identifiant unique, le serveur n'a qu'à s'assurer que l'identifiant unique n'est utilisé qu'une seule fois. Une méthode courante consiste à utiliser l'index unique de la base de données.

5. Annotations pour atteindre l'idempotence

  1.  Définir l'annotation DistributedLock

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface DistributedLock {
     
        /**
         * 保证业务接口的key的唯一性,否则失去了分布式锁的意义 锁key
         * 支持使用spEl表达式
         */
        String key();
     
        /**
         * 保证业务接口的key的唯一性,否则失去了分布式锁的意义 锁key 前缀
         */
        String keyPrefix() default "";
     
        /**
         * 是否在等待时间内获取锁,如果在等待时间内无法获取到锁,则返回失败
         */
        boolean tryLok() default false;
     
        /**
         * 获取锁的最大尝试时间 ,会尝试tryTime时间获取锁,在该时间内获取成功则返回,否则抛出获取锁超时异常,tryLok=true时,该值必须大于0。
         *
         */
        long tryTime() default 0;
     
        /**
         * 加锁的时间,超过这个时间后锁便自动解锁
         */
        long lockTime() default 30;
     
        /**
         * tryTime 和 lockTime的时间单位
         */
        TimeUnit unit() default TimeUnit.SECONDS;
     
        /**
         * 是否公平锁,false:非公平锁,true:公平锁
         */
        boolean fair() default false;
    }
  2. Définir l'aspect du verrouillage DistributedLockAspect

    @Aspect
    @Slf4j
    public class DistributedLockAspect {
     
        @Resource
        private IDistributedLock distributedLock;
     
        /**
         * SpEL表达式解析
         */
        private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
     
        /**
         * 用于获取方法参数名字
         */
        private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
     
        @Pointcut("@annotation(com.yt.bi.common.redis.distributedlok.annotation.DistributedLock)")
        public void distributorLock() {
        }
     
        @Around("distributorLock()")
        public Object around(ProceedingJoinPoint pjp) throws Throwable {
            // 获取DistributedLock
            DistributedLock distributedLock = this.getDistributedLock(pjp);
            // 获取 lockKey
            String lockKey = this.getLockKey(pjp, distributedLock);
            ILock lockObj = null;
            try {
                // 加锁,tryLok = true,并且tryTime > 0时,尝试获取锁,获取不到超时异常
                if (distributedLock.tryLok()) {
                    if(distributedLock.tryTime() <= 0){
                        throw new IdempotencyException("tryTime must be greater than 0");
                    }
                    lockObj = this.distributedLock.tryLock(lockKey, distributedLock.tryTime(), distributedLock.lockTime(), distributedLock.unit(), distributedLock.fair());
                } else {
                    lockObj = this.distributedLock.lock(lockKey, distributedLock.lockTime(), distributedLock.unit(), distributedLock.fair());
                }
     
                if (Objects.isNull(lockObj)) {
                    throw new IdempotencyException("Duplicate request for method still in process");
                }
     
                return pjp.proceed();
            } catch (Exception e) {
                throw e;
            } finally {
                // 解锁
                this.unLock(lockObj);
            }
        }
     
        /**
         * @param pjp
         * @return
         * @throws NoSuchMethodException
         */
        private DistributedLock getDistributedLock(ProceedingJoinPoint pjp) throws NoSuchMethodException {
            String methodName = pjp.getSignature().getName();
            Class clazz = pjp.getTarget().getClass();
            Class<?>[] par = ((MethodSignature) pjp.getSignature()).getParameterTypes();
            Method lockMethod = clazz.getMethod(methodName, par);
            DistributedLock distributedLock = lockMethod.getAnnotation(DistributedLock.class);
            return distributedLock;
        }
     
        /**
         * 解锁
         *
         * @param lockObj
         */
        private void unLock(ILock lockObj) {
            if (Objects.isNull(lockObj)) {
                return;
            }
     
            try {
                this.distributedLock.unLock(lockObj);
            } catch (Exception e) {
                log.error("分布式锁解锁异常", e);
            }
        }
     
        /**
         * 获取 lockKey
         *
         * @param pjp
         * @param distributedLock
         * @return
         */
        private String getLockKey(ProceedingJoinPoint pjp, DistributedLock distributedLock) {
            String lockKey = distributedLock.key();
            String keyPrefix = distributedLock.keyPrefix();
            if (StringUtils.isBlank(lockKey)) {
                throw new IdempotencyException("Lok key cannot be empty");
            }
            if (lockKey.contains("#")) {
                this.checkSpEL(lockKey);
                MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
                // 获取方法参数值
                Object[] args = pjp.getArgs();
                lockKey = getValBySpEL(lockKey, methodSignature, args);
            }
            lockKey = StringUtils.isBlank(keyPrefix) ? lockKey : keyPrefix + lockKey;
            return lockKey;
        }
     
        /**
         * 解析spEL表达式
         *
         * @param spEL
         * @param methodSignature
         * @param args
         * @return
         */
        private String getValBySpEL(String spEL, MethodSignature methodSignature, Object[] args) {
            // 获取方法形参名数组
            String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod());
            if (paramNames == null || paramNames.length < 1) {
                throw new IdempotencyException("Lok key cannot be empty");
            }
            Expression expression = spelExpressionParser.parseExpression(spEL);
            // spring的表达式上下文对象
            EvaluationContext context = new StandardEvaluationContext();
            // 给上下文赋值
            for (int i = 0; i < args.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }
            return expression.getValue(context).toString();
        }
     
        /**
         * SpEL 表达式校验
         *
         * @param spEL
         * @return
         */
        private void checkSpEL(String spEL) {
            try {
                ExpressionParser parser = new SpelExpressionParser();
                parser.parseExpression(spEL, new TemplateParserContext());
            } catch (Exception e) {
                log.error("spEL表达式解析异常", e);
                throw new IdempotencyException("Invalid SpEL expression [" + spEL + "]");
            }
        }
    }
  3. Définir la version de l'annotation de verrouillage distribué pour démarrer la méta-annotation

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import({DistributedLockAspect.class})
    public @interface EnableDistributedLock {
    }
  4. Définir l'interface de verrouillage distribué IDistributedLock

    public interface IDistributedLock {
        /**
         * 获取锁,默认30秒失效,失败一直等待直到获取锁
         *
         * @param key 锁的key
         * @return 锁对象
         */
        ILock lock(String key);
     
        /**
         * 获取锁,失败一直等待直到获取锁
         *
         * @param key      锁的key
         * @param lockTime 加锁的时间,超过这个时间后锁便自动解锁; 如果lockTime为-1,则保持锁定直到显式解锁
         * @param unit     {@code lockTime} 参数的时间单位
         * @param fair     是否公平锁
         * @return 锁对象
         */
        ILock lock(String key, long lockTime, TimeUnit unit, boolean fair);
     
        /**
         * 尝试获取锁,30秒获取不到超时异常,锁默认30秒失效
         *
         * @param key     锁的key
         * @param tryTime 获取锁的最大尝试时间
         * @return
         * @throws Exception
         */
        ILock tryLock(String key, long tryTime) throws Exception;
     
        /**
         * 尝试获取锁,获取不到超时异常
         *
         * @param key      锁的key
         * @param tryTime  获取锁的最大尝试时间
         * @param lockTime 加锁的时间
         * @param unit     {@code tryTime @code lockTime} 参数的时间单位
         * @param fair     是否公平锁
         * @return
         * @throws Exception
         */
        ILock tryLock(String key, long tryTime, long lockTime, TimeUnit unit, boolean fair) throws Exception;
     
        /**
         * 解锁
         *
         * @param lock
         * @throws Exception
         */
        void unLock(Object lock);
     
     
        /**
         * 释放锁
         *
         * @param lock
         * @throws Exception
         */
        default void unLock(ILock lock) {
            if (lock != null) {
                unLock(lock.getLock());
            }
        }
     
     
    }
  5. Classe d'implémentation IDistributedLock

    @Slf4j
    @Component
    public class RedissonDistributedLock implements IDistributedLock {
     
        @Resource
        private RedissonClient redissonClient;
        /**
         * 统一前缀
         */
        @Value("${redisson.lock.prefix:bi:distributed:lock}")
        private String prefix;
     
        @Override
        public ILock lock(String key) {
            return this.lock(key, 0L, TimeUnit.SECONDS, false);
        }
     
        @Override
        public ILock lock(String key, long lockTime, TimeUnit unit, boolean fair) {
            RLock lock = getLock(key, fair);
            // 获取锁,失败一直等待,直到获取锁,不支持自动续期
            if (lockTime > 0L) {
                lock.lock(lockTime, unit);
            } else {
                // 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
                lock.lock();
            }
            return new ILock(lock, this);
        }
     
        @Override
        public ILock tryLock(String key, long tryTime) throws Exception {
            return this.tryLock(key, tryTime, 0L, TimeUnit.SECONDS, false);
        }
     
        @Override
        public ILock tryLock(String key, long tryTime, long lockTime, TimeUnit unit, boolean fair)
                throws Exception {
            RLock lock = getLock(key, fair);
            boolean lockAcquired;
            // 尝试获取锁,获取不到超时异常,不支持自动续期
            if (lockTime > 0L) {
                lockAcquired = lock.tryLock(tryTime, lockTime, unit);
            } else {
                // 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
                lockAcquired = lock.tryLock(tryTime, unit);
            }
            if (lockAcquired) {
                return new ILock(lock, this);
            }
            return null;
        }
     
        /**
         * 获取锁
         *
         * @param key
         * @param fair
         * @return
         */
        private RLock getLock(String key, boolean fair) {
            RLock lock;
            String lockKey = prefix + ":" + key;
            if (fair) {
                // 获取公平锁
                lock = redissonClient.getFairLock(lockKey);
            } else {
                // 获取普通锁
                lock = redissonClient.getLock(lockKey);
            }
            return lock;
        }
     
        @Override
        public void unLock(Object lock) {
            if (!(lock instanceof RLock)) {
                throw new IllegalArgumentException("Invalid lock object");
            }
            RLock rLock = (RLock) lock;
            if (rLock.isLocked()) {
                try {
                    rLock.unlock();
                } catch (IllegalMonitorStateException e) {
                    log.error("释放分布式锁异常", e);
                }
            }
        }
    }
  6. Définir l'objet de verrouillage ILock

    import lombok.AllArgsConstructor;
    import lombok.Getter;
     
    import java.util.Objects;
     
    /**
     * <p>
     * RedissonLock 包装的锁对象 实现AutoCloseable接口,在java7的try(with resource)语法,不用显示调用close方法
     * </p>
     * @since 2023-06-08 16:57
     */
    @AllArgsConstructor
    public class ILock implements AutoCloseable {
        /**
         * 持有的锁对象
         */
        @Getter
        private Object lock;
        /**
         * 分布式锁接口
         */
        @Getter
        private IDistributedLock distributedLock;
     
        @Override
        public void close() throws Exception {
            if(Objects.nonNull(lock)){
                distributedLock.unLock(lock);
            }
        }
    }

6. Exemple d'utilisation 

 La classe de démarrage ajoute @EnableDistributedLock pour activer la prise en charge des annotations

@SpringBootApplication
@EnableDistributedLock
public class BiCenterGoodsApplication {
 
    public static void main(String[] args) {
        
        SpringApplication.run(BiCenterGoodsApplication.class, args);
        
    }
}

@DistributedLock marque les méthodes qui doivent utiliser des verrous distribués 

    @ApiOperation("编辑SKU供应商供货信息")
    
    @PostMapping("/editSupplierInfo")
    //@DistributedLock(key = "#dto.sku + '-' + #dto.skuId", lockTime = 10L, keyPrefix = "sku-")
    @DistributedLock(key = "#dto.sku", lockTime = 10L, keyPrefix = "sku-")
    public R<Boolean> editSupplierInfo(@RequestBody @Validated ProductSkuSupplierInfoDTO dto) {
        return R.ok(productSkuSupplierMeasureService.editSupplierInfo(dto));
    }
#dto.sku est une expression SpEL. Il prend en charge tout ce qui est pris en charge au printemps. Par exemple, appeler des méthodes statiques et des expressions ternaires. SpEL peut utiliser n’importe quel paramètre de la méthode. Référence des expressions SpEL

Du principe à la pratique, analysez divers schémas d'implémentation des verrous distribués Redis (1)

Du principe à la pratique, analysez le schéma de mise en œuvre du verrou distribué Redisson (2)

Spring Boot intègre les verrous distribués Redisson


Article de référence : Une annotation pour atteindre l'idempotence de l'interface avec élégance ! 

Je suppose que tu aimes

Origine blog.csdn.net/Ascend1977/article/details/132335058
conseillé
Classement