Contents

redis使用分布式锁记

使用redis分布式锁记

背景介绍

在项目中使用封装的redis锁注解中,会出现异常信息。抛出拿分布式锁异常。

跟源码

  • 其中lock对象为false的情况下会抛出Get redis lock failed的业务异常

@Around("redisLockPoint() && @annotation(redisLock)")
    public Object around(ProceedingJoinPoint pjp, RedisLock redisLock) throws Throwable {
        String key = redisLock.key();
        if (StringUtils.isBlank(key)) {
            Object[] args = pjp.getArgs();
            if (redisLock.bindType().equals(RedisLock.BindType.DEFAULT)) {
                key = StringUtils.join(args);
            } else if (redisLock.bindType().equals(RedisLock.BindType.ARGS_INDEX)) {
                key = getArgsKey(redisLock, args);
            } else if (redisLock.bindType().equals(RedisLock.BindType.OBJECT_PROPERTIES)) {
                key = getObjectPropertiesKey(redisLock, args);
            }
        }
        Assert.hasText(key, "key does not exist");
        String prefix = redisLock.prefix()+"_";
        boolean lock = distributedRedisLock.lock(prefix + key, redisLock.expire(), redisLock.retryTimes(), redisLock.retryInterval());
        if (!lock) {
            log.warn("get lock failed : " + key);
            if (redisLock.errorStrategy().equals(RedisLock.ErrorStrategy.THROW_EXCEPTION)) {
                throw new RedisException("Get redis lock failed");

            }
            return null;
        }
        log.info("get lock success : {}", key);
        try {
            return pjp.proceed();
        } finally {
            boolean result = distributedRedisLock.unLock(prefix + key);
            log.info("release lock : {} {}", prefix + key, result ? " success" : " failed");
        }
    }
  • 加锁的方法,通过redisTemplate执行execute方法,然后其中的SetOption参数为SET_IF_ABSENT

/**
  * 加锁
  * @param key 锁key
  * @param expire 过期时间
  * @param retryTimes 重试次数
  * @param retryInterval 重试间隔
  * @return true 加锁成功, false 加锁失败
  */
public boolean lock(String key, long expire, int retryTimes, long retryInterval) {
    boolean result = setRedisLock(key, expire);
    /**
      * 如果获取锁失败,进行重试
      */
    while((!result) && retryTimes-- > 0){
        try {
            log.info("lock failed, retrying..." + retryTimes);
            Thread.sleep(retryInterval);
        } catch (InterruptedException e) {
            return false;
        }
        result = setRedisLock(key, expire);
    }
    return result;
}

/**
  * 设置redis锁
  * @param key  锁key
  * @param expire 过期时间
  * @return true 设置成功,false 设置失败
  */
private boolean setRedisLock(String key, long expire) {
    try {
        RedisCallback<Boolean> callback = (connection) -> {
            String uuid = UUID.randomUUID().toString();
            lockKey.set(uuid);
            return connection.set(key.getBytes(), uuid.getBytes(), Expiration.milliseconds(expire), RedisStringCommands.SetOption.SET_IF_ABSENT);
        };
        return redisTemplate.execute(callback);
    }catch (Exception e){
        log.error("set redis error", e);
    }
    return false;
}
  • 其中connection.set方法的释意如下,参数为 key,value,过期时间,和option

/**
    * Set {@code value} for {@code key} applying timeouts from {@code expiration} if set and inserting/updating values
    * depending on {@code option}.
    *
    * @param key must not be {@literal null}.
    * @param value must not be {@literal null}.
    * @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} to not set any ttl.
    * @param option must not be {@literal null}. Use {@link SetOption#upsert()} to add non existing.
    * @return {@literal null} when used in pipeline / transaction.
    * @since 1.7
    * @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
    */
@Nullable
Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption option);
  • 其中option的值如下所示例
  • 当前程序使用的是SET_IF_ABSENT
  • 等同于redis的SETNX命令

/**
    * {@code SET} command arguments for {@code NX}, {@code XX}.
    *
    * @author Christoph Strobl
    * @since 1.7
    */
enum SetOption {

    /**
        * Do not set any additional command argument.
        *
        * @return
        */
    UPSERT,

    /**
        * {@code NX}
        *
        * @return
        */
    SET_IF_ABSENT,

    /**
        * {@code XX}
        *
        * @return
        */
    SET_IF_PRESENT;

    /**
        * Do not set any additional command argument.
        *
        * @return
        */
    public static SetOption upsert() {
        return UPSERT;
    }

    /**
        * {@code XX}
        *
        * @return
        */
    public static SetOption ifPresent() {
        return SET_IF_PRESENT;
    }

    /**
        * {@code NX}
        *
        * @return
        */
    public static SetOption ifAbsent() {
        return SET_IF_ABSENT;
    }
}

大胆假设,小心求证

  • 观点一: redis写会失败
    • 利用多线程模拟写操作
      • 本地模拟10万个线程同时写操作,并未复现
    • 写操作不包含重复的key
    • 业务代码中
  • 观点二: 写重复key导致失败
    • 利用多线程模拟写操作
    • 短时间内重复key
    • 业务代码中有满足的条件
      • 创建订单:key为customerId,表中存在customerId重复
      • 发放优惠券:key为activeId,活动为key,也存在重复

观点一代码,多次模拟,未出现拿锁异常的情况


@PostMapping("/testRedisLock")
public Result testRedisLock(@RequestParam Integer param) {
    final boolean[] flag = {false};
    for(int i = 0 ;i<param;i++){
        //方式1:相当于继承了Thread类,作为子类重写run()实现
        int finalI = i;
        new Thread() {
            @Override
            public void run() {
                try{
                    payV2Service.testRedisLock(finalI);
                }catch (Exception e){
                    e.printStackTrace();

                    flag[0] = true;
                    throw new RuntimeException("失败:");
                }
            };
        }.start();
    }
    return Result.succeed(flag[0],"成功");
}

@Override
@RedisLock(prefix = "CREATORDER:TEST")
public Object testRedisLock(Integer param) {

    log.info("加锁成功:" + param);

    return true;
}

观点二代码,次次都会复线出拿锁失败的情况


@PostMapping("/testRedisLock")
public Result testRedisLock(@RequestParam Integer param) {

    final boolean[] flag = {false};
    for(int i = 0 ;i<param;i++){
        //方式1:相当于继承了Thread类,作为子类重写run()实现
        int finalI = i;
        new Thread() {
            @Override
            public void run() {
                try{
                    payV2Service.testRedisLock(finalI);
                }catch (Exception e){
                    e.printStackTrace();

                    flag[0] = true;
                    throw new RuntimeException("失败:");
                }

                //System.out.println("匿名内部类创建线程方式1...");
            };
        }.start();

    }

    for(int i = 0 ;i<param;i++){
        //方式1:相当于继承了Thread类,作为子类重写run()实现
        int finalI = i;
        new Thread() {
            @Override
            public void run() {
                try{
                    payV2Service.testRedisLock(finalI);
                }catch (Exception e){
                    e.printStackTrace();

                    flag[0] = true;
                    throw new RuntimeException("失败:");
                }
                //System.out.println("匿名内部类创建线程方式1...");
            };
        }.start();
    }
    return Result.succeed(flag[0],"成功");
}


@Override
@RedisLock(prefix = "CREATORDER:TEST")
public Object testRedisLock(Integer param) {

    log.info("加锁成功:" + param);

    return true;
}

假设后,可近似求证的结果

基于以上两种假设,更可能出现的结论是写重复key导致失败,假设带着改结论去论证业务代码中由于数据问题,导致key重复的可能。于是针对加锁的key,去验证数据是否有相同

可验证,拿锁失败的情况,业务代码中数据key都会有重复

业务代码片段一

客户id在数据中有重复


/**
    * 自主续报下单
    * 1. 获取缓存数据
    * 2. 校验数据
    * 优惠券
    * 预存款
    * 3. 创建商品
    * 4. 生成订单
    */
@Override
@RedisLock(prefix = "CREATORDER", bindType = RedisLock.BindType.OBJECT_PROPERTIES, properties = "OrderIn.customerId")
public MergeOrderVO saveRenewOrder(OrderIn orderIn) {
    
}

-- 验证sql
select * from dyjw_aggregate_layer.t_student group by CUSTOMERID having count(CUSTOMERID) > 1;

业务代码片段二

活动id,针对相同活动会出现重复。


@RedisLock(prefix="SEND_COUPON",bindType=RedisLock.BindType.ARGS_INDEX,bindArgsIndex = 0)
@Override
public String sendCouponToCustomer(String activityId, String customerId,String followupId) {

}

上面场景中,观点一反复执行都不会报错,然后都是写重复key导致失败。基本可以确定是写重复key导致失败