Aop的意义
AOP 旨在从业务逻辑中分离出来通用逻辑,切面实现了跨越多种类型和对象的关注点(例如事务管理、日志记录、权限控制)的模块化。
例子
就以这段代码为例子,这段代码总是回去获取锁之后在执行完解开锁,基本上使用redisson作为分布式锁的代码都会以下几个操作
public Map> getCatalogJsonFromDbWithRedissonLock() {//1、占分布式锁。去redis占坑//(锁的粒度,越细越快:具体缓存的是某个数据,11号商品) product-11-lock//RLock catalogJsonLock = redissonClient.getLock("catalogJson-lock");//创建读锁RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("catalogJson-lock");RLock rLock = readWriteLock.readLock();Map> dataFromDb = null;try {rLock.lock();//加锁成功...执行业务dataFromDb = getDataFromDb();} finally {rLock.unlock();}return dataFromDb;}
重复的操作会让业务代码可读性变差,我们本着aop的思路 让业务代码专注于业务,来改造一下redisson锁获取值的方式优化,优化方式如下:
org.springframework.boot spring-boot-starter-data-redis
org.springframework.boot spring-boot-starter-web
org.apache.commons commons-pool2
org.springframework.boot spring-boot-starter-aop
com.alibaba fastjson 1.2.79
org.springframework.boot spring-boot-devtools runtime true
org.redisson redisson-spring-boot-starter 3.17.7
org.springframework.boot spring-boot-configuration-processor true
org.projectlombok lombok true
Application.properties
# 应用名称
spring.application.name=AspectRedisson
# 应用服务 WEB 访问端口
server.port=8080#redis配置
spring.redis.host=xxxxxx
spring.redis.password=admin
RedisConfig
配置序列化
/*** @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 {@Beanpublic RedisTemplate
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 {@Beanpublic RedissonClient redissonClient() throws IOException {Config config = new Config();config.useSingleServer()//可以用"rediss://"来启用SSL连接.setAddress("redis://xxxxxx:6379").setPassword("admin");return Redisson.create(config);}
}
作用在方法上,有一个前缀参数用于区别缓存名称
/*** @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";
}
为方法上添加了注解的方法做环绕切面织入,实现有缓存就直接返回缓存数据,没有缓存就使用创建锁放入缓存中返回数据
/*** @author 冷环渊 Doomwatcher* @context: 处理切面环绕* @date: 2022/11/16 13:52*/
@Component
@Aspect
public class RedissonLockAspect {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate 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;}
}
常量类
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;
}
测试接口
@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去测试,结果是没问题的,只要有缓存就不会去刷新方法,业务代码只需要加上注解设置前缀就可以完成加入缓存与存在直接调用。就算多次刷新也不会对数据库造成压力。