公司平台 Redis 规范不允许使用 lua 脚本,所以项目中无法使用 redisson 框架实现 Redis 分布式锁,因为 redisson 使用了大量的 lua 脚本保证部分 Redis 操作的原子性,比如释放锁的原子性。
于是我自己撸了个简易的 Redis 分布式锁,Redis 官方支持原子上锁(set nx px 指令),但不支持原子解锁(需要借助 lua 脚本实现),所以下面的代码存在 lock owner 被误删除的情况,但是没办法,谁让公司平台 Redis 不允许使用 lua 脚本呢。
编写 DistributeLock:
- boolean lock(String lockKey, String lockOwner, int lockTime, int waitTime):上锁,lockKey 表示要被保护的资源名称;lockOwner 表示上锁 Owner,防止解锁时误删其他 Owner 的 lockKey;lockTime 表示锁超时时间,即 set nx px 指令设置的超时时间;waitTime 表示等待时间,如果 waitTime 为 0,获取不到锁会立即如果 false,否则会一直尝试获取锁,直到超过 waitTime 毫秒。
- void unlock(String lockKey, String lockOwner):解锁,因为 RedisUtil 不支持原子解锁操作,所以存在 lock owner 被误删除的情况。
@Slf4j
@Component
public class DistributeLock {
public boolean lock(String lockKey, String lockOwner, int lockTime, int waitTime) {
boolean lockSuccess;
lockTime = Math.max(lockTime, 1000);
waitTime = Math.max(waitTime, 0);
log.debug("distribute lock key: {}, lock owner: {}, lock time: {}, wait time: {}", lockKey, lockOwner, lockTime,
waitTime);
if (waitTime == 0) {
lockSuccess = RedisUtil.tryGetDistributedLock(lockKey, lockOwner, lockTime);
} else {
lockSuccess = acquireBlockingDistributedLock(lockKey, lockOwner, waitTime, lockTime);
}
return lockSuccess;
}
public void unlock(String lockKey, String lockOwner) {
// because the RedisUtil does not support custom atomic lua operation, even if we specify the lock owner
// we still can not ensure the atomicity of get and delete operation
// lock key will be deleted by mistake occasionally especially in high concurrency scenario
String currentOwner = RedisUtil.get(lockKey);
if (StringUtils.equals(lockOwner, currentOwner)) {
RedisUtil.del(lockKey);
log.debug("release distribute lock success, lock key: {}, lock owner: {}", lockKey, lockOwner);
} else {
log.error("distribute lock already expired, lock key: {}, lock owner: {}", lockKey, lockOwner);
}
}
private boolean acquireBlockingDistributedLock(String lockKey, String lockOwner, long waitTimeInMilli,
long lockTimeInMilli) {
long endTime = System.currentTimeMillis() + waitTimeInMilli;
while (System.currentTimeMillis() < endTime) {
if (RedisUtil.tryGetDistributedLock(lockKey, lockOwner, lockTimeInMilli)) {
return true;
}
try {
Thread.sleep(100L);
} catch (InterruptedException exception) {
log.info("Thread interrupted: ", exception);
}
}
return false;
}
}
DistributeLockAspect 作用于 @DistributeLocked 注解上,DistributeLockAspect 中使用 SpelExpressionParser#parseExpression() 方法解析 @DistributeLocked#lockKey() 属性,因此 @DistributeLocked#lockKey() 支持 SpEL 表达式。
@Slf4j
@Aspect
@Component
@AllArgsConstructor
public class DistributeLockAspect {
private final DistributeLock distributeLock;
private final ApplicationContext applicationContext;
@Around("@annotation(com.envisioniot.scorpio.annotation.DistributeLocked)")
public Object invoke(ProceedingJoinPoint pjp) throws Throwable {
ExpressionParser parser = new SpelExpressionParser();
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
Object[] args = pjp.getArgs();
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
DistributeLocked distributeLockProperty = method.getAnnotation(DistributeLocked.class);
String[] params = discoverer.getParameterNames(method);
if (params != null && args != null && params.length == args.length) {
for (int len = 0; len < params.length; len++) {
context.setVariable(params[len], args[len]);
}
}
Expression expression = parser.parseExpression(distributeLockProperty.lockKey());
String lockKey = expression.getValue(context, String.class);
String lockOwner = Thread.currentThread().getName() + "-" + Thread.currentThread().getId() + "-"
+ UUID.randomUUID();
Object result;
try {
boolean lockSuccess = distributeLock.lock(lockKey, lockOwner, distributeLockProperty.lockTime(),
distributeLockProperty.waitTime());
if (!lockSuccess) {
throw new RuntimeException("acquire distribute lock failed, lockKey:" + lockKey);
}
result = pjp.proceed();
} catch (Throwable throwable) {
log.error("when invoking target method, an error or exception occurred, method name: {}", signature);
log.error("cause: ", throwable);
throw throwable;
} finally {
distributeLock.unlock(lockKey, lockOwner);
}
return result;
}
}
与 DistributeLockAspect 配套使用的 @DistributeLock 注解:
package com.oneby.redis.lock.annotation;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributeLocked {
int DEFAULT_WAIT_TIME = 1000;
int DEFAULT_LOCK_TIME = 5 * 1000;
// the key of distributed lock, support SpEL
String lockKey();
// the distributed lock time in milliseconds
int lockTime() default DEFAULT_LOCK_TIME;
// the wait time in milliseconds to acquire the distributed lock, if the value is 0, acquisition will fail fast.
int waitTime() default DEFAULT_WAIT_TIME;
}
@DistributeLock 注解使用示例:
@DistributeLock(lockKey = "'report_data_lock:site_id:' + #request.getSiteId()", lockTime = 2000, waitTime = 10000)
public Boolean reportSiteData(SiteReportRequest request) {