由于分布式锁的业务逻辑大致都相似,我们选择通过注解来实现分布式锁,类似于spring中的@Transactional。
首先,我们先自定义一个注解叫做gmallCache,自定义一个注解时需要指定两个元注解,@Target:注解标志的位置 @Rention:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。接着我们在注解中定义了一个属性prefix,用来指定分布式锁中key的前缀。
/**
* @author lilg
* @date 2022/11/25
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GmallCache {
/**
* 分布式锁的前缀
* @return
*/
String prefix() default "cache";
}
接着,开始进行切的编写,首先我们需要将这个切方法交给spring管理,所以要加上@compent,并且,由于它是切方法,所以要加上@Aspect注解。经过思考后,我觉得这里应该使用环绕增强,@around注解。并对我们之前自定义的注解进行了切的操作。
@Around(value = "@annotation(com.atguigu.gmall.common.cache.GmallCache)")
接着,进行具体业务逻辑的编写,首先我们通过joinPoint对象的getSignature方法获取到MethodSignature对象,在该对象中获取到GMallCache这个注解对象来获得key的前缀。接着通过joinpoint的getArgs方法来获取参数,用来拼接redisKey。
@Around(value = "@annotation(com.atguigu.gmall.common.cache.GmallCache)")
public Object cacheAroundAdvice(ProceedingJoinPoint joinPoint){
/**
* 1.获取方法上的注解
* 2.获取注解上的参数
* 3.拼接key
* 4.redis中操作(分布式锁)
*/
Object object = new Object();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
//前缀
String prefix = gmallCache.prefix();
//获取到方法上的参数
Object[] args = joinPoint.getArgs();
//拼接key
String key = prefix + Arrays.asList(args) + RedisConst.SKUKEY_SUFFIX;
接着,开始走缓存常见的三种情况的逻辑。缓存穿透(通过返回空值来解决),缓存雪崩(通过随机过期时间来解决),缓存击穿(通过分布式锁来解决)。
在这里,我们抽出了一个方法gitCache来判定缓存中是否有对象。这里通过之前的MethodSignature对象来获取到返回值Class对象。并在返回时进行转化。为通用性考虑,这里返回值为Object类型。并且由于自定义了redis的序列化器,返回值必定为String类型。
private Object gitCache(String key,MethodSignature signature) {
String sObject = (String) redisTemplate.opsForValue().get(key);
Class returnType = signature.getReturnType();
if (!StringUtils.isEmpty(sObject)){
return JSON.parseObject(sObject,returnType);
}
return null;
}
如果 gitCache返回值为空,开始走分布式锁的逻辑,这里我们通过redission来实现分布式锁,通过getLock的方法获取锁对象,通过tryLock的方法获取锁。
// 判断是否获取到数据
if (null == object){
//分布式锁业务逻辑
//从数据库中获取数据(执行代码块中的代码)
//先获得锁
RLock lock = redissonClient.getLock(key + ":lock");
//尝试获取锁
boolean flag = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
如果获取锁成功。 通过joinPoint.proceed(args);这句话代码运行之前方法体中的代码,而方法体中的代码为查询数据库中是否存在。
如果数据库中返回值为空,走缓存穿透逻辑。这里,为防止意外情况发送,我们还是调用了之前的MethodSignature对象来获取他的返回值(Class),最后通过Class的无参构造来创建一个Object类型(具体参考我上一篇博客)。
//防止缓存穿透
if (null == object){
//通过分布式锁调用无参构造
Class returnType = signature.getReturnType();
Object o = returnType.getConstructor().newInstance();
redisTemplate.opsForValue().set(key, JSON.toJSONString(o),
RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
return o;
}
如果不为空,则直接将其放入缓存中。最后通过try finally进行释放锁操作,防止死锁。如果没有获取到锁,则等待一秒后重写获取,实现自旋操作。
else {
Thread.sleep(1000);
//自旋转
return cacheAroundAdvice(joinPoint);
}
接着,需要对整个方法内容进行try catch,如果其中出现问题,直接从数据库中查询,用做兜底方案。完整代码如下
**
* @author lilg
* @date 2022/11/25
*/
@Component
@Aspect
public class GmallCacheAspect {
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisTemplate redisTemplate;
//切注解
@SneakyThrows
@Around(value = "@annotation(com.atguigu.gmall.common.cache.GmallCache)")
public Object cacheAroundAdvice(ProceedingJoinPoint joinPoint){
/**
* 1.获取方法上的注解
* 2.获取注解上的参数
* 3.拼接key
* 4.redis中操作(分布式锁)
*/
Object object = new Object();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
//前缀
String prefix = gmallCache.prefix();
//获取到方法上的参数
Object[] args = joinPoint.getArgs();
//拼接key
String key = prefix + Arrays.asList(args) + RedisConst.SKUKEY_SUFFIX;
try {
// 从缓存获取
object = gitCache(key,signature);
// 判断是否获取到数据
if (null == object){
//分布式锁业务逻辑
//从数据库中获取数据(执行代码块中的代码)
//先获得锁
RLock lock = redissonClient.getLock(key + ":lock");
//尝试获取锁
boolean flag = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
//如果获取锁成功
if (flag){
try {
object = joinPoint.proceed(args);
//防止缓存穿透
if (null == object){
//通过分布式锁调用无参构造
Class returnType = signature.getReturnType();
Object o = returnType.getConstructor().newInstance();
redisTemplate.opsForValue().set(key, JSON.toJSONString(o),
RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
return o;
}
//不为空 将数据存入缓存
redisTemplate.opsForValue().set(key, JSON.toJSONString(object),
RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
return object;
} finally {
lock.unlock();
}
} else {
Thread.sleep(1000);
//自旋转
return cacheAroundAdvice(joinPoint);
}
}else {
return object;
}
} catch (Throwable e) {
e.printStackTrace();
}
//如果最后出现问题 数据库方案兜底
return joinPoint.proceed(args);
}
private Object gitCache(String key,MethodSignature signature) {
String sObject = (String) redisTemplate.opsForValue().get(key);
Class returnType = signature.getReturnType();
if (!StringUtils.isEmpty(sObject)){
return JSON.parseObject(sObject,returnType);
}
return null;
}
}