java中锁的注解_【完美】SpringBoot中使⽤注解来实现Redis
分布式锁
⼀、业务背景
有些业务请求,属于耗时操作,需要加锁,防⽌后续的并发操作,同时对数据库的数据进⾏操作,需要避免对之前的业务造成影响。
⼆、分析流程
使⽤ Redis 作为分布式锁,将锁的状态放到 Redis 统⼀维护,解决集中单机 JVM 信息不互通的问题,规定操作顺序,保护⽤户的数据正确。
梳理设计流程
新建注解 @interface,在注解⾥设定⼊参标志
增加 AOP 切点,扫描特定注解
建⽴ @Aspect 切⾯任务,注册 bean 和拦截特定⽅法
特定⽅法参数 ProceedingJoinPoint,对⽅法 pjp.proceed() 前后进⾏拦截
切点前进⾏加锁,任务执⾏后进⾏删除 key
核⼼步骤:加锁、解锁和续时
加锁
使⽤了 RedisTemplate 的 opsForValue.setIfAbsent ⽅法,判断是否有 key,设定⼀个随机数 UUID.random().toString,⽣成⼀个随机数作为 value。
从 redis 中获取锁之后,对 key 设定 expire 失效时间,到期后⾃动释放锁。
按照这种设计,只有第⼀个成功设定 Key 的请求,才能进⾏后续的数据操作,后续其它请求由于⽆法获得 资源,将会失败结束。
超时问题
担⼼ pjp.proceed() 切点执⾏的⽅法太耗时,导致 Redis 中的 key 由于超时提前释放了。
例如,线程 A 先获取锁,proceed ⽅法耗时,超过了锁超时时间,到期释放了锁,这时另⼀个线程 B
成功获取 Redis 锁,两个线程同时对同⼀批数据进⾏操作,导致数据不准确。
springboot aop
解决⽅案:增加⼀个「续时」
任务不完成,锁不释放:
维护了⼀个定时线程池 ScheduledExecutorService,每隔 2s 去扫描加⼊队列中的 Task,判断是否失效时间是否快到了,公式为:【失效时间】<= 【当前时间】+【失效间隔(三分之⼀超时)】
/**
* 线程池,每个 JVM 使⽤⼀个线程去维护 keyAliveTime,定时执⾏ runnable
*/
private static final ScheduledExecutorService SCHEDULER =
new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("redisLock-schedule-pool").daemon(true).build());
static {
SCHEDULER.scheduleAtFixedRate(() -> {
// do something to extend time
}, 0,  2, TimeUnit.SECONDS);
}
三、设计⽅案
经过上⾯的分析,同事⼩ 设计出了这个⽅案:
前⾯已经说了整体流程,这⾥强调⼀下⼏个核⼼步骤:拦截注解 @RedisLock,获取必要的参数
续时操作
结束业务,释放锁
四、实操
之前也有整理过 AOP 使⽤⽅法,可以参考⼀下
相关属性类配置
业务属性枚举设定
public enum RedisLockTypeEnum {
/**
* ⾃定义 key 前缀
*/
ONE("Business1", "Test1"),
TWO("Business2", "Test2");
private String code;
private String desc;
RedisLockTypeEnum(String code, String desc) { de = code;
this.desc = desc;
}
public String getCode(){
return code;
}
public String getDesc(){
return desc;
}
public String getUniqueKey(String key){
return String.format("%s:%s", Code(), key); }
}
任务队列保存参数
public class RedisLockDefinitionHolder{
/**
* 业务唯⼀ key
*/
private String businessKey;
/
**
* 加锁时间 (秒 s)
*/
private Long lockTime;
/**
* 上次更新时间(ms)
*/
private Long lastModifyTime;
/**
* 保存当前线程
*/
private Thread currentTread;
private int tryCount;
/**
* 当前尝试次数
*/
private int currentCount;
/**
* 更新的时间周期(毫秒),公式 = 加锁时间(转成毫秒) / 3
*/
private Long modifyPeriod;
public RedisLockDefinitionHolder(String businessKey, Long lockTime, Long lastModifyTime, Thread currentTread, int tryCount {
this.businessKey = businessKey;
this.lockTime = lockTime;
this.lastModifyTime = lastModifyTime;
this.currentTread = currentTread;
}
}
设定被拦截的注解名字
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RedisLockAnnotation {
/**
* 特定参数识别,默认取第 0 个下标
*/
int lockFiled() default 0;
/**
* 超时重试次数
*/
int tryCount() default 3;
RedisLockTypeEnum typeEnum();
/
**
* 释放时间,秒 s 单位
*/
long lockTime() default 30;
}
核⼼切⾯拦截的操作
RedisLockAspect.java 该类分成三部分来描述具体作⽤
Pointcut 设定
/**
* @annotation 中的路径表⽰拦截特定注解
*/
@Pointcut("@annotation(cn.sevenyuan.demo.aop.lock.RedisLockAnnotation)")
public void redisLockPC(){
}
Around 前后进⾏加锁和释放锁
前⾯步骤定义了我们想要拦截的切点,下⼀步就是在切点前后做⼀些⾃定义操作:
@Around(value = "redisLockPC()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
// 解析参数
Method method = resolveMethod(pjp);
RedisLockAnnotation annotation = Annotation(RedisLockAnnotation.class); RedisLockTypeEnum typeEnum = peEnum();
Object[] params = Args();
String ukString = params[annotation.lockFiled()].toString();
// 省略很多参数校验和判空
String businessKey = UniqueKey(ukString);
String uniqueValue = UUID.randomUUID().toString();
// 加锁
Object result = null;
try {