Redisson Aop实现分布式锁
前言
简介
Aop的意义
AOP 旨在从业务逻辑中分离出来通用逻辑,切面实现了跨越多种类型和对象的关注点(例如事务管理、日志记录、权限控制)的模块化。
例子
就以这段代码为例子,这段代码总是回去获取锁之后在执行完解开锁,基本上使用redisson作为分布式锁的代码都会以下几个操作
- 创建锁
- 获取锁
- 执行方法
- 解锁
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
//1、占分布式锁。去redis占坑
//(锁的粒度,越细越快:具体缓存的是某个数据,11号商品) product-11-lock
//RLock catalogJsonLock = redissonClient.getLock("catalogJson-lock");
//创建读锁
RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");
RLock rLock = readWriteLock.readLock();
Map<String, List<Catelog2Vo>> dataFromDb = null;
try {
rLock.lock();
//加锁成功...执行业务
dataFromDb = getDataFromDb();
} finally {
rLock.unlock();
}
return dataFromDb;
}
思路
重复的操作会让业务代码可读性变差,我们本着aop的思路 让业务代码专注于业务,来改造一下redisson锁获取值的方式优化,优化方式如下:
- 自定义注解 作用于方法上
- 用AOP来做redisson的获取锁和解锁还有存储redis的操作
代码实现
依赖
代码语言:javascript复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.17.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
配置
代码语言:javascript复制Application.properties
# 应用名称
spring.application.name=AspectRedisson
# 应用服务 WEB 访问端口
server.port=8080
#redis配置
spring.redis.host=xxxxxx
spring.redis.password=admin
RedisConfig
配置序列化
代码语言:javascript复制/**
* @package: com.hyc.aspectredisson.config.config
* @className: RedisConfig
* @author: 冷环渊 doomwatcher
* @description: TODO
* @date: 2022/11/15 19:21
* @version: 1.0
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
//使用fastjson序列化
FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
代码语言:javascript复制redissonConfig
/**
* @projectName: AspectRedisson
* @package: com.hyc.aspectredisson.config
* @className: redissonConfig
* @author: 冷环渊 doomwatcher
* @description: TODO
* @date: 2022/11/15 19:15
* @version: 1.0
*/
@Configuration
public class redissonConfig {
@Bean
public RedissonClient redissonClient() throws IOException {
Config config = new Config();
config.useSingleServer()
//可以用"rediss://"来启用SSL连接
.setAddress("redis://xxxxxx:6379")
.setPassword("admin");
return Redisson.create(config);
}
}
自定义注解
作用在方法上,有一个前缀参数用于区别缓存名称
代码语言:javascript复制/**
* @projectName: AspectRedisson
* @package: com.hyc.aspectredisson.config.annotation
* @className: AddLock
* @author: 冷环渊 doomwatcher
* @description: TODO
* @date: 2022/11/15 20:29
* @version: 1.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface addSectionLock {
/**
* 缓存key的前缀
* @return
*/
String prefix() default "cache";
}
切面类
为方法上添加了注解的方法做环绕切面织入,实现有缓存就直接返回缓存数据,没有缓存就使用创建锁放入缓存中返回数据
代码语言:javascript复制/**
* @author 冷环渊 Doomwatcher
* @context: 处理切面环绕
* @date: 2022/11/16 13:52
*/
@Component
@Aspect
public class RedissonLockAspect {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
// 切addSectionLock注解
@SneakyThrows
@Around("@annotation(com.hyc.aspectredisson.annotation.addSectionLock)")
public Object cacheAroundAdvice(ProceedingJoinPoint joinPoint){
// 声明一个对象
Object object = new Object();
// 在环绕通知中处理业务逻辑 {实现分布式锁}
// 获取到注解,注解使用在方法上!
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
addSectionLock AddLock = signature.getMethod().getAnnotation(addSectionLock.class);
// 获取到注解上的前缀
String prefix = AddLock.prefix(); // test
// 方法传入的参数
Object[] args = joinPoint.getArgs();
// 方法名
String MethodName = ((MethodSignature) joinPoint.getSignature()).getMethod().getName();
// 组成缓存的key 需要前缀 方法传入的参数
String key = prefix MethodName Arrays.asList(args).toString();
// 防止redis ,redisson 出现问题!
try {
// 从缓存中获取数据
// 类似于 redisTemplate.opsForValue().get(KEY);
object = cacheHit(key,signature);
// 判断缓存中的数据是否为空!
if (object==null){
// 从数据库中获取数据,并放入缓存,防止缓存击穿必须上锁
// key ":lock"
String lockKey = prefix "lock";
// 准备上锁
RLock lock = redissonClient.getLock(lockKey);
boolean result = lock.tryLock(RedisConst.LOCK_EXPIRE_PX1, RedisConst.LOCK_EXPIRE_PX2, TimeUnit.SECONDS);
// 上锁成功
if (result){
System.out.println("上锁成功" result);
try {
// 表示执行方法体
object = joinPoint.proceed(joinPoint.getArgs());
// 判断object 是否为空
if (object==null){
// 防止缓存穿透
Object object1 = new Object();
redisTemplate.opsForValue().set(key, JSON.toJSONString(object1), RedisConst.KEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
// 返回数据
return object1;
}
// 放入缓存
redisTemplate.opsForValue().set(key, JSON.toJSONString(object),RedisConst.KEY_TIMEOUT,TimeUnit.SECONDS);
// 返回数据
return object;
} finally {
lock.unlock();
}
}else{
// 上锁失败,睡眠自旋
Thread.sleep(1000);
return cacheAroundAdvice(joinPoint);
// 理想状态
// return object;
}
}
return object;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
// 如果出现问题数据库兜底
return joinPoint.proceed(joinPoint.getArgs());
}
/**
* 表示从缓存中获取数据
* @param key 缓存的key
* @param signature 获取方法的返回值类型
* @return
*/
private Object cacheHit(String key, MethodSignature signature) {
// 通过key 来获取缓存的数据
String strJson = (String) redisTemplate.opsForValue().get(key);
// 表示从缓存中获取到了数据
if (!StringUtils.isEmpty(strJson)){
// 字符串存储的数据是什么? 就是方法的返回值类型
Class returnType = signature.getReturnType();
// 将字符串变为当前的返回值类型
return JSON.parseObject(strJson,returnType);
}
return null;
}
}
其余代码
代码语言:javascript复制常量类
public class RedisConst {
public static final String KEY_PREFIX = "Test:";
//单位:秒
public static final long KEY_TIMEOUT = 60;
public static final long KEY_TEMPORARY_TIMEOUT = 30;
public static final long SKULOCK_EXPIRE_PX1 = 60;
public static final long SKULOCK_EXPIRE_PX2 = 60;
}
测试
代码语言:javascript复制测试接口
@RestController
public class simpleController {
@GetMapping(value = "/getInfo")
@addSectionLock(prefix = RedisConst.KEY_PREFIX)
public String getinfo(String name)
{
userEntity userEntity = new userEntity();
userEntity.setName(name);
userEntity.setUserWare(100);
return JSON.toJSONString(userEntity);
}
}
测试
我们之前切面类中有设置获取锁成功的输出日志,按照实现逻辑,只需要执行一次存入缓存的时候需要获取锁,所以在这个日志指挥打印一次,简单测试那就开两个浏览器页面反复获取数据
在多次点击之后,还是只输出一次,这个模式就算是实现成功了,之后也有用apifox去测试,结果是没问题的,只要有缓存就不会去刷新方法,业务代码只需要加上注解设置前缀就可以完成加入缓存与存在直接调用。就算多次刷新也不会对数据库造成压力。