原理概述
利用aop特性,编制一个环绕切面给加了注解的方法体上,每次执行该方法时,首先进入切面进行加锁,执行完毕后回到切面,进行解锁。
redisson本身支持redis的集群部署,而redisson的缺陷就是,当线程A对master的节点加锁成功,二master节点会异步复制到slave节点,而恰在此时,还没有复制到slave节点时,master节点发生奔溃宕机,slave节点切换为主节点的时候,线程B也加锁成功,此时,两个线程都获得了锁,都去执行一段代码,会产生一些问题。
适用场景
分布式部署的情况下,单个服务启动多实例的情况下,需要对相同数据进行操作。
在定时任务没有单独分离出一个服务,而定时任务所在的服务启动了多实例,要保证定时任务只会被一个示例执行。
。。。。
引入依赖
<!-- 引入redisson的springboot启动器 -->
<!-- 版本需要格外注意,需要去mvn仓库里查询redisson支持的starter-data-redis版本
本次使用的starter-parent是2.1.3.RELEASE,最高到3.9.1支持才支持,主要是适配redis的版本-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.9.1</version>
</dependency>
<!-- aop切面依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
编写注解
加入enable是为了控制定时任务是否执行,而不是控制是否要加锁处理,具体可以根据业务需求,修改切面类的逻辑。
package com.ai.sx.modules.yitiji.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {
//锁的名称
String lockName();
//是否生效,1生效,0不生效-即使加锁成功,扔不执行代码
String enable();
//锁的失效时间,默认3秒,单位秒
long leaseTime() default 3;
}
编写切面
进入切面后,加锁成功,则执行任务,加锁失败,则什么也不干。
具体加锁失败是需要循环尝试加锁,还是中断,需要修改切面的逻辑;
需要循环尝试加锁,则使用do…while循环,进行循环尝试,为了缓解redis压力,降低多个线程抢锁的可能,可以在每次尝试加锁失败后,让线程睡眠一定的时间。
package com.ai.sx.modules.yitiji.aspect;
import com.ai.sx.modules.yitiji.annotation.RedisLock;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class RedissonLockAspect {
@Autowired
private RedissonClient redissonClient;
@Around("@annotation(redisLock)")
public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
if (!"1".equals(redisLock.enable())){
return null;
}
String lockName = redisLock.lockName();
long leaseTime = redisLock.leaseTime();
RLock lock = redissonClient.getLock(lockName);
Object result = null;
if (lock.tryLock(0,leaseTime, TimeUnit.SECONDS)){
try {
//执行方法
result = joinPoint.proceed();
} finally {
if (lock != null && lock.isHeldByCurrentThread()){
lock.unlock();
}
}
}
return result;
}
}
使用
模拟了定时删除临时目录的定时任务,enable控制定时任务是否执行。
package com.ai.sx.modules.yitiji.task;
import com.ai.sx.modules.yitiji.annotation.RedisLock;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.File;
@Component
@Slf4j
public class ImageTask {
@Value("${yitiji.img.base.path}")
private String IMG_BASE_PATH;
@RedisLock(lockName = "deletedImagesTask",enable = "0")
@Scheduled(cron = "0/5 * * * * ?")
public void deletedImagesTask(){
if(!StringUtils.isBlank(IMG_BASE_PATH)){
File file = new File(IMG_BASE_PATH);
if(file.exists()){
for (File f : file.listFiles()){
deleteFiles(f);
}
}
}
}
private void deleteFiles(File file){
if (file.isDirectory()){
File[] files = file.listFiles();
for (File f : files){
deleteFiles(f);
}
}
if (file.delete()){
log.info(file.getAbsolutePath()+"已被删除");
}else {
log.info(file.getAbsolutePath()+"删除失败");
}
}
}
参考阅读
注解式 redisson分布式锁
Redis锁机制的几种实现方式
Redis 分布式锁的正确实现方式讨论
Redission分布式锁原理