SpringBoot利用注解来实现Redis分布式锁_第1页
SpringBoot利用注解来实现Redis分布式锁_第2页
SpringBoot利用注解来实现Redis分布式锁_第3页
SpringBoot利用注解来实现Redis分布式锁_第4页
SpringBoot利用注解来实现Redis分布式锁_第5页
已阅读5页,还剩9页未读, 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第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. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论