基于Redis的分布式锁:Redission
需求场景:执行推送任务,保证每个任务只推送一次。
处理流程:将需要幂等性的接口加上自定义注解。然后编写一个切面,在Around方法里逻辑:尝试获取分布式锁(带过期时间),成功表明没重复提交,否则就是重复提交了。
具体的redis已经配置在了nacos中,这里不做赘述。
1.添加Redission依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.5</version>
</dependency>
2.自定义注解类型实现分布式锁:@Lock
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Lock {
String prefix() default "";
String key() default "";
long expire() default 60L;
int tryCount() default 3;
}
3.分布式锁切面:LockAspect
@Slf4j
@Aspect
@Order(1000)
@Component
public class LockAspect {
public static final String TASK_PUSH_LOCK_PREFIX = "cms:";
@Autowired
private RedissonClient redissonClient;
private static DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
private static SpelExpressionParser parser = new SpelExpressionParser();
//增强带有Lock注解的方法
@Pointcut("@annotation(com.xxx.cms.service.annotation.Lock)")
public void LockAspect() {
}
@Around("LockAspect()")
public Object redisLock(ProceedingJoinPoint point) throws Throwable {
Object result;
Class<?> clazz = point.getTarget().getClass();
MethodSignature methodSignature = (MethodSignature) point.getSignature();
Method method = clazz.getDeclaredMethod(methodSignature.getName(), methodSignature.getParameterTypes());
Lock redisLockAnnotation = method.getAnnotation(Lock.class);
// 拼接 key
final String key = generateKeyBySpEL(redisLockAnnotation.key(), method, point.getArgs());
String prefix = StringUtils.isEmpty(redisLockAnnotation.prefix()) ? TASK_PUSH_LOCK_PREFIX + ":"+ method.getName()+":" : redisLockAnnotation.prefix();
String lockKey = StringUtils.isEmpty(key)
? prefix
: prefix + ":" + key;
log.info("分布式锁,创建key:{}", lockKey);
//redisson核心步骤1 getLock 获取锁
final RLock lock = redissonClient.getLock(lockKey);
try {
lockKeyList.add(new RedisLockInfo(lockKey, redisLockAnnotation.expire(), redisLockAnnotation.tryCount(), Thread.currentThread()));
//redisson核心步骤2 lock 加锁
lock.lock(redisLockAnnotation.expire(), TimeUnit.SECONDS);
//需要加锁的具体业务逻辑
result = point.proceed();
//redisson核心步骤3 unlock 释放锁
lock.unlock();
log.info("# [END]分布式锁,删除key:{}", lockKey);
return result;
} catch (CommonException ie) {
log.error("分布式锁,请求超时", ie.getMessage());
throw new Exception(ie.getMessage());
} catch (Exception e) {
log.error("分布式锁,创建失败", e);
//redisson核心步骤4 unlock 最后记得释放锁
lock.unlock();
log.info("# [END]分布式锁,删除key:{}", lockKey);
}
return false;
}
private static String generateKeyBySpEL(String spELString, Method method, Object[] args) {
String[] paramNames = discoverer.getParameterNames(method);
Expression expression = parser.parseExpression(spELString);
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < args.length; i++) {
assert paramNames != null;
context.setVariable(paramNames[i], args[i]);
}
return Objects.requireNonNull(expression.getValue(context)).toString();
}
@Data
class RedisLockInfo{
private String key;
private int tryNums;
private int tryCount;
private Thread thread;
private long expireTime;
public RedisLockInfo(String key,long expireTime,int tryCount,Thread thread){
this.key = key;
this.tryCount = tryCount;
this.thread = thread;
this.expireTime = expireTime;
}
}
}
4.任务推送执行器TaskPushExecutor
@Component("taskPushExecutor")
public class TaskPushExecutor extends AbstractExecutor<UserInfo> {
public static final String PUSH_TASK_PREFIX = "cms:push:task:scheduler:";
//只需要在保证幂等性的接口上,加上自定义的@Lock注解就好了
@Lock(prefix=PUSH_TASK_PREFIX,key="#task.taskSign+':'+#task.taskId+':'+#task.userGroupId",expire = 300L,tryCount = 3)
@Override
public void execute(ITaskHandler taskHandler, Task task) {
//执行具体的业务逻辑
executeTask(taskHandler, task);
}
}
5.查询执行日志状况
可以通过Kibana查看日志,发现加锁和释放锁,均是成对出现,分布式锁正常!
参考文章
拜托,面试请不要再问我Redis分布式锁的实现原理
Spring Boot + Redis实战-利用自定义注解+分布式锁实现接口幂等性