Implementação elegante de idempotência de interface baseada em anotações

1. O que é idempotência

Simplificando, é realizar múltiplas solicitações repetidas em uma interface, e o resultado de uma solicitação é o mesmo. Parece muito fácil de entender, mas precisa ser muito rigoroso para realmente manter esse objetivo no sistema. Design , no ambiente de produção real, devemos garantir que qualquer interface seja idempotente, e como implementar corretamente a idempotência é o que este artigo discutirá.

2. Quais solicitações são inerentemente idempotentes

Em primeiro lugar, precisamos de saber que os pedidos de consulta são geralmente idempotentes por natureza.Além disso, os pedidos de eliminação também são idempotentes na maioria dos casos, excepto em cenários ABA.

dê um exemplo simples

Por exemplo, se você primeiro solicitar uma operação para excluir A, mas porque a resposta expirou, você solicitará automaticamente outra operação para excluir A. Se A for inserido novamente entre as duas solicitações, mas na verdade o A recém-inserido, não deverá ser excluído. Este é o problema ABA. No entanto, na maioria dos cenários de negócios, o problema ABA pode ser ignorado.

Além de consultar e excluir, também existem operações de atualização. A mesma operação de atualização também é naturalmente idempotente na maioria dos cenários. A exceção é que também haverá problemas de ABA. Mais importante ainda, por exemplo, realizar tais atualizações não é idempotente. update table set a = a + 1 where v = 1Esperou .

No final, há apenas inserção, que na maioria dos casos não é idempotente, a menos que o índice único do banco de dados seja utilizado para garantir que os dados não serão duplicados.

3. Por que você precisa de idempotência?

1. Nova tentativa de tempo limite

Quando uma solicitação RPC é iniciada, é inevitável que a solicitação falhe devido à instabilidade da rede. Geralmente, ao encontrar esse problema, esperamos poder solicitá-lo novamente. Normalmente, não há problema, mas às vezes a solicitação tem realmente enviado, apenas na solicitação A rede está anormal ou expirou durante a resposta.Neste momento, se o solicitante iniciar uma solicitação novamente, a parte solicitada precisa garantir a idempotência.

2. Retorno de chamada assíncrono

O retorno de chamada assíncrono é uma forma comum de melhorar o rendimento das interfaces do sistema. Obviamente, tais interfaces devem garantir a idempotência.

3. Fila de mensagens

Agora, estruturas de fila de mensagens comumente usadas, como: Kafka, RocketMQ, RabbitMQ adotarão o princípio Pelo menos uma vez (ou seja, o princípio de pelo menos uma vez, quando a mensagem é entregue, a mensagem não pode ser perdida, mas mensagens repetidas são permitidas), uma vez que a fila de mensagens não garante que não haverá mensagens duplicadas, portanto o consumidor deve naturalmente garantir a idempotência da lógica de processamento.

4. Fatores-chave para alcançar a idempotência

fator chave 1

O identificador exclusivo idempotente pode ser chamado de número idempotente ou token idempotente ou ID global. Resumindo, é o identificador exclusivo para uma solicitação entre o cliente e o servidor. Geralmente, é gerado pelo cliente ou pode ser unificado por um terceiro. distribuir.

fator chave 2

Com o identificador exclusivo, o servidor só precisa garantir que o identificador exclusivo seja usado apenas uma vez. Uma forma comum é usar o índice exclusivo do banco de dados.

5. Anotações para alcançar idempotência

  1.  Defina a anotação 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. Definir o aspecto DistributedLockAspect Lock

    @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. Defina a versão da anotação de bloqueio distribuída para iniciar a meta-anotação

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import({DistributedLockAspect.class})
    public @interface EnableDistributedLock {
    }
  4. Definir a interface de bloqueio distribuído 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 de implementação 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. Definir objeto de bloqueio 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. Exemplo de uso 

 A classe de inicialização adiciona @EnableDistributedLock para ativar o suporte a anotações

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

@DistributedLock marca métodos que precisam usar bloqueios distribuídos 

    @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 é uma expressão SpEL. Ele suporta tudo que é suportado no Spring. Por exemplo, chamando métodos estáticos e expressões ternárias. SpEL pode usar qualquer parâmetro do método. Referência de Expressão SpEL

Do princípio à prática, analise vários esquemas de implementação de bloqueios distribuídos Redis (1)

Do princípio à prática, analise o esquema de implementação do bloqueio distribuído Redisson (2)

Spring Boot integra bloqueios distribuídos Redisson


Artigo de referência: Uma anotação para obter idempotência de interface com elegância! 

Acho que você gosta

Origin blog.csdn.net/Ascend1977/article/details/132335058
Recomendado
Clasificación