版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第SpringBoot利用注解来实现Redis分布式锁目录一、业务背景二、分析流程加锁超时问题解决方案:增加一个「续时」三、设计方案四、实操相关属性类配置核心切面拦截的操作五、开始测试六、总结
一、业务背景
有些业务请求,属于耗时操作,需要加锁,防止后续的并发操作,同时对数据库的数据进行操作,需要避免对之前的业务造成影响。
二、分析流程
使用Redis作为分布式锁,将锁的状态放到Redis统一维护,解决集群中单机JVM信息不互通的问题,规定操作顺序,保护用户的数据正确。
梳理设计流程
新建注解@interface,在注解里设定入参标志增加AOP切点,扫描特定注解建立@Aspect切面任务,注册bean和拦截特定方法特定方法参数ProceedingJoinPoint,对方法ceed()前后进行拦截切点前进行加锁,任务执行后进行删除key
核心步骤:加锁、解锁和续时
加锁
使用了RedisTemplate的opsForValue.setIfAbsent方法,判断是否有key,设定一个随机数UUID.random().toString,生成一个随机数作为value。
从redis中获取锁之后,对key设定expire失效时间,到期后自动释放锁。
按照这种设计,只有第一个成功设定Key的请求,才能进行后续的数据操作,后续其它请求由于无法获得资源,将会失败结束。
超时问题
担心ceed()切点执行的方法太耗时,导致Redis中的key由于超时提前释放了。
例如,线程A先获取锁,proceed方法耗时,超过了锁超时时间,到期释放了锁,这时另一个线程B成功获取Redis锁,两个线程同时对同一批数据进行操作,导致数据不准确。
解决方案:增加一个「续时」
任务不完成,锁不释放:
维护了一个定时线程池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)
{
this.code
=
code;
this.desc
=
desc;
}
public
String
getCode()
{
return
code;
}
public
String
getDesc()
{
return
desc;
}
public
String
getUniqueKey(String
key)
{
return
String.format("%s:%s",
this.getCode(),
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;
this.tryCount
=
tryCount;
this.modifyPeriod
=
lockTime
*
1000
/
3;
}
设定被拦截的注解名字
@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
=
method.getAnnotation(RedisLockAnnotation.class);
RedisLockTypeEnum
typeEnum
=
annotation.typeEnum();
Object[]
params
=
pjp.getArgs();
String
ukString
=
params[annotation.lockFiled()].toString();
//
省略很多参数校验和判空
String
businessKey
=
typeEnum.getUniqueKey(ukString);
String
uniqueValue
=
UUID.randomUUID().toString();
//
加锁
Object
result
=
null;
try
{
boolean
isSuccess
=
redisTemplate.opsForValue().setIfAbsent(businessKey,
uniqueValue);
if
(!isSuccess)
{
throw
new
Exception("You
can't
do
it,because
another
has
get
the
lock
=-=");
}
redisTemplate.expire(businessKey,
annotation.lockTime(),
TimeUnit.SECONDS);
Thread
currentThread
=
Thread.currentThread();
//
将本次
Task
信息加入「延时」队列中
holderList.add(new
RedisLockDefinitionHolder(businessKey,
annotation.lockTime(),
System.currentTimeMillis(),
currentThread,
annotation.tryCount()));
//
执行业务操作
result
=
ceed();
//
线程被中断,抛出异常,中断此次请求
if
(currentThread.isInterrupted())
{
throw
new
InterruptedException("You
had
been
interrupted
=-=");
}
}
catch
(InterruptedException
e
)
{
log.error("Interrupt
exception,
rollback
transaction",
e);
throw
new
Exception("Interrupt
exception,
please
send
request
again");
}
catch
(Exception
e)
{
log.error("has
some
error,
please
check
again",
e);
}
finally
{
//
请求结束后,强制删掉
key,释放锁
redisTemplate.delete(businessKey);
("release
the
lock,
businessKey
is
["
+
businessKey
+
"]");
}
return
result;
上述流程简单总结一下:
解析注解参数,获取注解值和方法上的参数值redis加锁并且设置超时时间将本次Task信息加入「延时」队列中,进行续时,方式提前释放锁加了一个线程中断标志结束请求,finally中释放锁
续时操作
这里用了ScheduledExecutorService,维护了一个线程,不断对任务队列中的任务进行判断和延长超时时间:
//
扫描的任务队列
private
static
ConcurrentLinkedQueueRedisLockDefinitionHolder
holderList
=
new
ConcurrentLinkedQueue();
*
线程池,维护keyAliveTime
private
static
final
ScheduledExecutorService
SCHEDULER
=
new
ScheduledThreadPoolExecutor(1,
new
BasicThreadFactory.Builder().namingPattern("redisLock-schedule-pool").daemon(true).build());
//
两秒执行一次「续时」操作
SCHEDULER.scheduleAtFixedRate(()
-
{
//
这里记得加
try-catch,否者报错后定时任务将不会再执行=-=
IteratorRedisLockDefinitionHolder
iterator
=
holderList.iterator();
while
(iterator.hasNext())
{
RedisLockDefinitionHolder
holder
=
iterator.next();
//
判空
if
(holder
==
null)
{
iterator.remove();
continue;
}
//
判断
key
是否还有效,无效的话进行移除
if
(redisTemplate.opsForValue().get(holder.getBusinessKey())
==
null)
{
iterator.remove();
continue;
}
//
超时重试次数,超过时给线程设定中断
if
(holder.getCurrentCount()
holder.getTryCount())
{
holder.getCurrentTread().interrupt();
iterator.remove();
continue;
}
//
判断是否进入最后三分之一时间
long
curTime
=
System.currentTimeMillis();
boolean
shouldExtend
=
(holder.getLastModifyTime()
+
holder.getModifyPeriod())
=
curTime;
if
(shouldExtend)
{
holder.setLastModifyTime(curTime);
redisTemplate.expire(holder.getBusinessKey(),
holder.getLockTime(),
TimeUnit.SECONDS);
("businessKey
:
["
+
holder.getBusinessKey()
+
"],
try
count
:
"
+
holder.getCurrentCount());
holder.setCurrentCount(holder.getCurrentCount()
+
1);
}
}
},
0,
2,
TimeUnit.SECONDS);
这段代码,用来实现设计图中虚线框的思想,避免一个请求十分耗时,导致提前释放了锁。
这里加了「线程中断」Thread#interrupt,希望超过重试次数后,能让线程中断(未经严谨测试,仅供参考哈哈哈哈)
不过建议如果遇到这么耗时的请求,还是能够从根源上查找,分析耗时路径,进行业务优化或其它处理,避免这些耗时操作。
所以记得多打点Log,分析问题时可以更快一点。记录项目日志,一个注解搞定
五、开始测试
在一个入口方法中,使用该注解,然后在业务中模拟耗时请求,使用了Thread#sleep
@GetMapping("/testRedisLock")
@RedisLockAnnotation(typeEnum
=
RedisLockTypeEnum.ONE,
lockTime
=
3)
public
Book
testRedisLock(@RequestParam("userId")
Long
userId)
{
try
{
("睡眠执行前");
Thread.sleep(10000);
("睡眠执行后");
}
catch
(Exception
e)
{
//
log
error
("has
some
error",
e);
}
return
null;
使用时,在方法上添加该注解,然后设定相应参数即可,根据typeEnum可以区分多种业务,限制该业务被同时操作。
测试结果:
2025-04-0414:55:50.864INFO9326---[nio-8081-exec-1]c.s.demo.controller.BookController:睡眠执行前
2025-04-0414:55:52.855INFO9326---[k-schedule-pool]c.s.demo.aop.lock.RedisLockAspect:businessKey:[Business1:1024],trycount:0
2025-04-0414:55:54.851INFO9326---[k-schedule-pool]c.s.demo.aop.lock.RedisLockAspect:businessKey:[Business1:1024],trycount:1
2025-04-0414:55:56.851INFO9326---[k-schedule-pool]c.s.demo.aop.lock.RedisLockAspect:businessKey:[Business1:1024],trycount:2
2025-04-0414:55:58.852INFO9
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2026年内科主治医师考试历年模拟题及答案详解
- 2026年国际志愿者日志愿服务条例模拟试题(含答案)
- 2026年电大遗传学模拟试题(含答案)
- 第八版药理学习题及答案详解
- 硬质合金制品烧结工安全专项能力考核试卷含答案
- 2026年事业单位a类考模拟题目及答案详解
- 2026年肥乡区事业编模拟试题(含答案)
- 2026年国际金融交易员专业技能培训模拟试卷及答案详解
- 易货师岗前道德考核试卷含答案
- 2026年包装设计师高级资格证模拟试题(含答案)
- 船舶新能源技术
- 2025国家基层糖尿病防治管理指南解读课件
- 林木居间协议合同范本
- 现浇梁蒸养棚施工方案
- 地下室顶板加固施工方案范本
- 智能停车场道闸系统维护与保养服务策略及实施方案
- 金融服务基金尽职调查报告范文
- 英语二必考500词
- 工程地质及水文地质课件
- 初三差生班会课件
- C++解析wav文件方法介绍
评论
0/150
提交评论