参考    使用redis实现分布式锁和保证接口幂等性(自定义注解 + AOP)_an_zzzzz的博客-CSDN博客_redis分布式锁保证幂等性

原理:利用分布式锁的机制,对请求【url+token】封装成key,然后加锁,导致相同用户的同一个请求只能获取到一把锁,以此来保证接口幂等性

1.解决的问题
由于项目里需要解决幂等性的问题,所以本文介绍使用 redis 分布式锁机制解决接口幂等性问题。
解决幂等性问题的话,首先要知道幂等性是什么意思哈。
幂等性:
通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次

分布式锁:
如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。
如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。

分布式锁实现的关键是在分布式的应用服务器外,搭建一个存储服务器,存储锁信息,这时候我们很容易就想到了Redis。实际上常用的分布式锁的话有三种方式。1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。

2.实现的原理
通过自定义注解 + AOP实现,从而减少代码的入侵

3.具体实现(代码)
redis 分布式锁工具类 使用redisConnection和lua脚本实现reids语句的原子性
 

/**
 * redis 分布式锁工具类
 */
@Component
public class RedisLock {

    private static final Long RELEASE_SUCCESS = 1L;
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    // 当前设置 过期时间单位, EX = seconds; PX = milliseconds
    private static final String SET_WITH_EXPIRE_TIME = "EX";
    //lua
    private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 该加锁方法仅针对单实例 Redis 可实现分布式加锁
     * 对于 Redis 集群则无法使用
     * <p>
     * 支持重复,线程安全
     *
     * @param lockKey  加锁键
     * @param clientId 加锁客户端唯一标识(采用UUID)
     * @param seconds  锁过期时间
     * @return
     */
    public boolean tryLock(String lockKey, String clientId, long seconds) {
        //使用redisConnection可以保证redis的原子性
        return redisTemplate.execute((RedisCallback<Boolean>)  redisConnection -> {

            // Jedis jedis = (Jedis) redisConnection.getNativeConnection();
            Object nativeConnection = redisConnection.getNativeConnection();
            RedisSerializer<String> stringRedisSerializer = (RedisSerializer<String>)redisTemplate.getKeySerializer();
            byte[] keyByte  = stringRedisSerializer.serialize(lockKey);
            byte[] valueByte = stringRedisSerializer.serialize(clientId);

            // lettuce连接包下 redis 单机模式
            if (nativeConnection instanceof RedisAsyncCommands) {
                RedisAsyncCommands connection = (RedisAsyncCommands) nativeConnection;
                RedisCommands commands = connection.getStatefulConnection().sync();
                //set
                String result = commands.set(keyByte, valueByte, SetArgs.Builder.nx().ex(seconds));
                if (LOCK_SUCCESS.equals(result)){
                    return true;
                }
            }
            // lettuce连接包下 redis 集群模式
            if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
                RedisAdvancedClusterAsyncCommands connection = (RedisAdvancedClusterAsyncCommands) nativeConnection;
                RedisAdvancedClusterCommands commands = connection.getStatefulConnection().sync();
                String result = commands.set(keyByte, valueByte, SetArgs.Builder.nx().ex(seconds));
                if (LOCK_SUCCESS.equals(result)) {
                    return true;
                }
            }
            if (nativeConnection instanceof JedisCommands) {
                JedisCommands jedis = (JedisCommands) nativeConnection;
                String result = jedis.set(new String(keyByte), new String(valueByte),SetParams.setParams().nx().ex((int) seconds));
                //String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
                if (LOCK_SUCCESS.equals(result)) {
                    return true;
                }
            }
            return false;
        });
    }

    /**
     * 与 tryLock 相对应,用作释放锁
     *
     * @param lockKey
     * @param clientId
     * @return
     */
    public boolean releaseLock(String lockKey, String clientId) {
        DefaultRedisScript<Integer> redisScript = new DefaultRedisScript<>();
        //使用lua脚本可以保证redis的原子性
        redisScript.setScriptText(RELEASE_LOCK_SCRIPT);
        redisScript.setResultType(Integer.class);

        Object execute = redisTemplate.execute((RedisConnection connection) ->connection.eval(
                RELEASE_LOCK_SCRIPT.getBytes(),
                ReturnType.INTEGER,
                1,
                lockKey.getBytes(),
                clientId.getBytes()));
        if (RELEASE_SUCCESS.equals(execute)) {
            return true;
        }
        return false;
    }
}

@ApiIdempotent 自定义注解

/**
 * 在需要保证 接口幂等性 的Controller的方法上使用此注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
    /**
     * 设置请求锁定时间,超时后自动释放锁
     *
     * @return
     */
    int lockTime() default 10;
}

AOP切面类 对标记有@ApiIdempotent的方法进行动态代理

/**
 * redis 分布式锁处理接口幂等性
 * 使用redis生成全局唯一的key,生成key的接口才可进入
 * key为 'reidsLock:' + token + requestURI , value为 uuid
 */
@Slf4j
@Aspect
@Component
public class ApidempotentAspect {

    @Autowired
    private RedisLock redisLock;

    @Pointcut("@annotation(com.hgsoft.ecip.framework.common.annotation.ApiIdempotent)")
    public void apidempotent(){}

    @Around("apidempotent()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 使用分布式锁 机制-实现
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        ApiIdempotent apiIdempotent = method.getAnnotation(ApiIdempotent.class);
        int lockSeconds = apiIdempotent.lockTime();

//        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
//        AssertUtils.notNull(request, "request can not null");

        // 获取请求的凭证,本项目中使用的JWT,可对应修改
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("token");
        String requestURI = request.getRequestURI();

        String key = getIdeKey(token, requestURI);
        String clientId = UuidWorkerInit.getUUID();

        // 获取锁
        boolean lock = redisLock.tryLock(key, clientId, lockSeconds);
        log.info("tryLock key = [{}], clientId = [{}]", key, clientId);

        if (lock) {
            log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId);
            // 获取锁成功
            Object result;
            try {
                // 执行进程
                result = joinPoint.proceed();
            } finally {
                // 解锁
                redisLock.releaseLock(key, clientId);
                log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId);
            }
            return result;
        } else {
            // 获取锁失败,认为是重复提交的请求
            log.info("tryLock fail, key = [{}]", key);
            throw  new RuntimeException("重复请求,请稍后再试!");
        }
    }

    private String getIdeKey(String token, String requestURI) {
        return "redisLock:" + token + requestURI;
    }
}

4.方法的优点和缺点
优点:代码侵入性小
缺点:1.并不能解决幂等性的所有问题,只能解决保证一个人的请求只有一个被调用,其他的被视为重复请求。(相当于对接口加锁!)如果要保证幂等性,还需要在业务处理时,查询数据库对业务状态判断是否处理过。2.该加锁方法仅针对单实例 Redis 可实现分布式加锁。果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁。
 

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐