redis使用分布式锁记
Contents
使用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导致失败
。