基于redisson实现接口幂等性

发布于:2025-03-12 ⋅ 阅读:(11) ⋅ 点赞:(0)

说明

实现幂等性的方法有很多种,本次仅基于redisson锁进行处理
本次开发基于自行封装的redis开发组件,有兴趣的可以看下redis组件

代码编写

pom.xml引入redisson

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.39.0</version>
</dependency>

redisson相关配置

  • CusRedissonConfiguration(此次仅单体)
@Configuration
public class CusRedissonConfiguration {
    private final static String REDIS_ADDRESS_PATTERN = "%s://%s:%s";

    @Bean
    public RedissonClient singleRedissonClient(SingleProperties singleProperties) {
        Config config = new Config();
        config.useSingleServer().setAddress(String.format(REDIS_ADDRESS_PATTERN,
                        BooleanUtils.isTrue(singleProperties.getEncryptEnabled()) ?
                                RedisConstants.redisProtocol.ENCRYPT_REDIS : RedisConstants.redisProtocol.REDIS,
                        singleProperties.getHost(), singleProperties.getPort()))
                .setUsername(singleProperties.getUsername()).setPassword(singleProperties.getPassword());
        return Redisson.create(config);
    }

    @Bean
    @ConditionalOnProperty(prefix = "cus.redisson", name = "idempotent-default", havingValue = "true", matchIfMissing = true)
    public Idempotent idempotent(){
        return new DefaultIdempotentUtil();
    }

}
  • CusRedissonConfiguration
@Configuration
public class CusRedissonConfiguration {
    private final static String REDIS_ADDRESS_PATTERN = "%s://%s:%s";

    @Bean
    public RedissonClient singleRedissonClient(SingleProperties singleProperties) {
        Config config = new Config();
        config.useSingleServer().setAddress(String.format(REDIS_ADDRESS_PATTERN,
                        BooleanUtils.isTrue(singleProperties.getEncryptEnabled()) ?
                                RedisConstants.redisProtocol.ENCRYPT_REDIS : RedisConstants.redisProtocol.REDIS,
                        singleProperties.getHost(), singleProperties.getPort()))
                .setUsername(singleProperties.getUsername()).setPassword(singleProperties.getPassword());
        return Redisson.create(config);
    }

    @Bean
    @ConditionalOnProperty(prefix = "cus.redisson", name = "idempotent-default", havingValue = "true", matchIfMissing = true)
    public Idempotent idempotent(){
        return new DefaultIdempotentUtil();
    }

}
  • SingleProperties
@Component
@ConfigurationProperties(prefix = "cus.redisson.single-properties")
public class SingleProperties {
    private String host = RedisConstants.defaultRedisInfo.DEFAULT_IP;
    private String port = RedisConstants.defaultRedisInfo.DEFAULT_PORT;
    private String username;
    private String password;

    private Integer database = RedisConstants.defaultRedisInfo.DEFAULT_DATABASE;

    private Boolean encryptEnabled = false;

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public String getPort() {
        return port;
    }

    public void setPort(String port) {
        this.port = port;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Boolean getEncryptEnabled() {
        return encryptEnabled;
    }

    public void setEncryptEnabled(Boolean encryptEnabled) {
        this.encryptEnabled = encryptEnabled;
    }

    public Integer getDatabase() {
        return database;
    }

    public void setDatabase(Integer database) {
        this.database = database;
    }
}

lock代码编写

  • Lock
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Lock {
    //锁键
    String lockKey();

    //等待时间
    long waitTime() default 60L;

    //自动释放时间
    long leaseTime();

    //时间单位
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    //锁类型
    String lockType() default "FAIR";

    //校验lockKey是否存在
    boolean lockKeyExistFlag() default false;
}
  • LockAspect
    此处为核心处理代码,如果需要检验lockKey是否是被伪造的,可以将lockKeyExistFlag设置为true,由idempotent执行相关校验逻辑
    getLockKey可以视为通用方法,后续考虑放到core中
@Aspect
@Component
public class LockAspect {

    private final static Logger LOGGER = LoggerFactory.getLogger(LockAspect.class);

    private LockServiceFactory lockServiceFactory;

    private Idempotent idempotent;

    @Autowired
    public void setLockServiceFactory(LockServiceFactory lockServiceFactory) {
        this.lockServiceFactory = lockServiceFactory;
    }

    @Autowired
    public void setIdempotent(Idempotent idempotent) {
        this.idempotent = idempotent;
    }

    @Around("@annotation(lock)")
    public Object around(ProceedingJoinPoint joinPoint, Lock lock) throws Throwable {
        String lockKey = this.getLockKey(joinPoint, lock);
        if (StringUtils.isEmpty(lockKey)) {
            LOGGER.error("lockKey is empty");
            return null;
        }
        //校验lockKey是否是被伪造的,lockKey的生成和校验逻辑可以自定义
        if (lock.lockKeyExistFlag() && !idempotent.lockKeyExist(lockKey)) {
            LOGGER.error("the lockKey was forged");
            return null;
        }
        LockService lockService = lockServiceFactory.getLockServiceByType(lock.lockType());
        CusLockInfo cusLockInfo = new CusLockInfo();
        cusLockInfo.setLockKey(lockKey);
        cusLockInfo.setWaitTime(lock.waitTime());
        cusLockInfo.setLeaseTime(lock.leaseTime());
        cusLockInfo.setTimeUnit(lock.timeUnit());
        try {
            lockService.lock(cusLockInfo);
            if (!lockService.lock(cusLockInfo)) {
                throw new RuntimeException("Failed to acquire lock");
            }
            return joinPoint.proceed();
        } finally {
            //开启了伪造校验,结束后需进行清除
            if (lock.lockKeyExistFlag() && !idempotent.clear(lockKey)) {
                LOGGER.error("Verification resource lockKey clearance failed, lockKey value is{}", lockKey);
            }
            lockService.unLock(cusLockInfo);
        }
    }

    /**
     * 结合el表达式解析lockKey
     *
     * @param joinPoint 切点
     * @param lock      锁信息
     * @return lockKey
     */
    private String getLockKey(ProceedingJoinPoint joinPoint, Lock lock) {
        // 获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        // 创建表达式解析器
        ExpressionParser parser = new SpelExpressionParser();
        // 创建评估上下文
        EvaluationContext context = new StandardEvaluationContext();

        // 将方法参数绑定到上下文中
        String[] paramNames = signature.getParameterNames();
        Object[] paramValues = joinPoint.getArgs();
        for (int i = 0; i < paramNames.length; i++) {
            context.setVariable(paramNames[i], paramValues[i]);
        }

        // 解析lockKey的值
        return parser.parseExpression(lock.lockKey()).getValue(context, String.class);
    }
}

lockKey相关逻辑

  • Idempotent
    实现类可自定义,yml中指定idempotent-default为false,自行注册Idempotent的自定义实现类即可
public interface Idempotent {
    /**
     * 创建唯一性lockKey
     *
     * @param args 构建lockKey的参数
     * @return lockKey
     */
    String createUniqueLockKey(String... args);

    /**
     * 校验lockKey是否存在(是否是伪造的)
     *
     * @param lockKey lockKey
     * @return lockKey是否存在
     */
    Boolean lockKeyExist(String lockKey);

    /**
     * 清除校验数据
     * @param lockKey lockKey
     * @return 是否清除成功
     */
    Boolean clear(String lockKey);
}
  • DefaultIdempotentUtil
@Component
public class DefaultIdempotentUtil implements Idempotent {
    private final static Logger logger = LoggerFactory.getLogger(DefaultIdempotentUtil.class);

    private final static String PREFIX = "token_%s_%s";

    public final static String DEFAULT_SERVERNAME = "default";

    public final static String UNIQUE_FLAG = "true";

    public RedisHelper redisHelper;


    @Autowired
    public void setRedisHelper(RedisHelper redisHelper) {
        this.redisHelper = redisHelper;
    }



    /**
     * 获取token的接口需要防止疯狂获取导致redis暴库
     *
     * @param serverName 服务名
     * @return token
     */
    public String createUniqueTokenWithServerName(String serverName) {
        serverName = Optional.ofNullable(serverName).orElse(DEFAULT_SERVERNAME);
        String uuid = UUID.randomUUID().toString();
        String uniqueToken = String.format(PREFIX, serverName, uuid);
        logger.debug("{} generate unique_token {}", serverName, uniqueToken);
        redisHelper.strSet(uniqueToken, UNIQUE_FLAG);
        return uniqueToken;
    }

    @Override
    public String createUniqueLockKey(String... args) {
         // 确保至少有一个参数被提供
        if (args == null || args.length == 0) {
            throw new IllegalArgumentException("At least one argument is required.");
        }

        // 使用第一个参数作为serverName
        return createUniqueTokenWithServerName(args[0]);
    }

    @Override
    public Boolean lockKeyExist(String lockKey){
        return StringUtils.isNotEmpty(redisHelper.strGet(lockKey));
    }

    @Override
    public Boolean clear(String lockKey) {
        return redisHelper.del(lockKey);
    }
}

yml相关配置

spring:
  redis:
    host: ip
    port: port
    database: 1
    username: root
    password: root
    jedis:
      pool:
        # 资源池中最大连接数
        # 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整
        max-active: ${SPRING_REDIS_POOL_MAX_ACTIVE:50}
        # 资源池运行最大空闲的连接数
        # 默认8,-1表示无限制;可根据服务并发redis情况及服务端的支持上限调整,一般建议和max-active保持一致,避免资源伸缩带来的开销
        max-idle: ${SPRING_REDIS_POOL_MAX_IDLE:50}
        # 当资源池连接用尽后,调用者的最大等待时间(单位为毫秒)
        # 默认 -1 表示永不超时,设置5秒
        max-wait: ${SPRING_REDIS_POOL_MAX_WAIT:5000}
cus:
  redisson:
    single-properties:
      host: ip
      port: port
      username: root
      password: root
    idempotent-default: true

参考资料

[1].redis代码