随着业务中缓存及分布式锁的加入,业务代码变的复杂起来,除了需要考虑业务逻辑本身,还要考虑缓存及分布式锁的问题,增加了程序员的工作量及开发难度。而缓存的玩法套路特别类似于事务,而声明式事务就是用了aop的思想实现的。
我们的思想就是模拟事务的实现方式,缓存可以这样实现:
在这里将使用AOP思想和不适用AOP思想做一个对比
假设现在我的业务是根据skuId查询skuInfo对象,未使用分布式锁时的代码如下:
//根据skuId查询skuInfo信息和图片列表
@Override
public SkuInfo getSkuInfo(Long skuId) {
//查询数据库mysql获取数据
return getSkuInfoDB(skuId);
}
//查询数据库获取skuInfo信息
private SkuInfo getSkuInfoDB(Long skuId) {
//查询skuInfo
SkuInfo skuInfo = skuInfoMapper.selectById(skuId);
//根据skuId查询图片列表
LambdaQueryWrapper<SkuImage> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SkuImage::getSkuId, skuId);
List<SkuImage> skuImages = skuImageMapper.selectList(wrapper);
//设置当前图片列表
if(skuInfo!=null){
skuInfo.setSkuImageList(skuImages);
}
return skuInfo;
}
步骤如下:
1、定义获取sku信息的key–skuKey
2、根据skuKey从Redis中获取数据:
有数据就直接返回结果
没有数据执行下一步
3、定义skuLock
4、获取锁:如果没有获取到锁,设置睡眠时间继续自旋获取锁。
如果获取到了锁,执行下一步。
5、查询数据库获取sku数据,如果数据库中有数据,则存储数据到缓存,返回数据。
如果数据库中没有数据,存储null到缓存,返回数据(这样做的目的是防止缓存穿透)
6、释放锁
7、写一个兜底的方式(其实就是查询数据库),目的是上面的代码发生异常的时候,也能正常返回数据。
//根据skuId查询skuInfo信息和图片列表
@Override
public SkuInfo getSkuInfo(Long skuId) {
//使用redis实现分布式锁缓存数据
return getSkuInfoRedis(skuId);
}
/**
* 获取skuInfo,从缓存中获取数据
* Redis实现分布式锁
* 实现步骤:
* 1、定义存储skuInfo的key
* 2、根据skyKey获取skuInfo的缓存数据
* 3、判断
* 有:直接返回结束
* 没有:定义锁的key,尝试加锁(失败:睡眠,重试自旋;成功:查询数据库,判断是否有值,有的话直接返回,缓存到数据库,没有,创建空值,返回数据)
*/
private SkuInfo getSkuInfoRedis(Long skuId) {
try {
//定义存储skuKey sku:1314:info
String skuKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;
//尝试获取缓存中的数据
SkuInfo skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
//判断是否有值
if (skuInfo == null) {
//说明缓存中没有数据
//定义锁的key
String lockKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKULOCK_SUFFIX;
//生成uuid标识
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
//获取锁
Boolean flag = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
//判断是否获取到了锁
if (flag) {//获取到了锁
//查询数据库
SkuInfo skuInfoDB = getSkuInfoDB(skuId);
//判断数据库中是否有值
if (skuInfoDB == null) {
SkuInfo skuInfo1 = new SkuInfo();
redisTemplate.opsForValue().set(skuKey, skuInfo1, RedisConst.SKUKEY_TEMPORARY_TIMEOUT, TimeUnit.SECONDS);
return skuInfo1;
}
//数据库查询的数据不为空
//存储到缓存
redisTemplate.opsForValue().set(skuKey, skuInfoDB, RedisConst.SKUKEY_TIMEOUT, TimeUnit.SECONDS);
//释放锁-lua脚本
//定义lua脚本
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
//创建脚本对象
DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
//设置脚本
defaultRedisScript.setScriptText(script);
//设置返回值类型
defaultRedisScript.setResultType(Long.class);
//执行删除
redisTemplate.execute(defaultRedisScript, Arrays.asList(lockKey), uuid);
//返回数据
return skuInfoDB;
} else {
Thread.sleep(100);
return getSkuInfoRedis(skuId);
}
} else {
return skuInfo;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
//兜底,在上面从缓存中获取的过程中出现异常,这行代码也必须执行
return getSkuInfoDB(skuId);
}
这个步骤和2.2是一样的
//根据skuId查询skuInfo信息和图片列表
@Override
public SkuInfo getSkuInfo(Long skuId) {
//使用Redisson实现分布式锁
return getSkuInfoRedisson(skuId);
}
/**
*使用Redisson改造skuInfo信息
*/
private SkuInfo getSkuInfoRedisson(Long skuId) {
try {
//定义sku数据获取的Key
String skuKey=RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKUKEY_SUFFIX;
//尝试从缓存中获取数据
SkuInfo skuInfo = (SkuInfo) redisTemplate.opsForValue().get(skuKey);
//判断缓存中是否有数据
if(skuInfo==null){
//定义锁的key
String skuLock=RedisConst.SKUKEY_PREFIX+skuId+RedisConst.SKULOCK_SUFFIX;
//获取锁
RLock lock = redissonClient.getLock(skuLock);
//加锁
boolean res = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
//判断
if(res){
try {
//获取到了锁,查询数据库
skuInfo= getSkuInfoDB(skuId);
//判断
if(skuInfo==null){
//存储null,避免缓存穿透
skuInfo=new SkuInfo();
redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
return skuInfo;
}else{
//存储
redisTemplate.opsForValue().set(skuKey,skuInfo,RedisConst.SKUKEY_TIMEOUT,TimeUnit.SECONDS);
// redisTemplate.opsForValue().set(skuKey,skuInfo);
//返回
return skuInfo;
}
} finally {
//释放锁
lock.unlock();
}
}else{
//没有获取到锁
Thread.sleep(100);
return getSkuInfoRedisson(skuId);
}
}else{
//缓存中有数据
return skuInfo;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
//兜底方法,前面代码异常,这里会执行
return getSkuInfoDB(skuId);
}
这里直接在Swagger中测试,该接口格式如下:
第一次点击发送,从响应可以看出请求时成功的
观察该服务的控制台,发现第一次是查询了控制台的。
观察Redis中的数据:
然后清空该服务的控制台之后,再次发送同样的请求再观察控制台的输出
可以看到,此时已经不用查数据库了,而是直接取的Redis中的数据
每次实现分布式锁的时候都需要写一大段重复代码,增加了工作量,代码也不优雅。
解决方案:借助AOP思想,用自定义注解封装下这段重复的代码,这样后面需要分布式锁的时候我们直接加个注解再修改个参数就行。
import java.lang.annotation.*;
/**
* 元注解:简单理解就是修饰注解的注解
* @Target:用于描述注解的使用范围,简单理解就是当前注解可以用在什么地方
* @Retention:表示注解的生命周期
* SOURCE:只存在类文件中,在class字节码不存在
* CLASS:存在到字节码文件中
* RUNTIME:运行时
* @Inherited:表示被GmallCache修饰的类的子类会不会继承GmallCache
* @Documented:表明这个注解应该被javadoc工具记录,因此可悲javadoc类的工具文档化
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface GmallCache {
//缓存的前缀
String prefix() default "cache:";
//缓存的后缀
String suffix() default ":info";
}
参考文档:https://docs.spring.io/spring-framework/docs/5.3.27/reference/html/core.html#aop-ataspectj-example
实现步骤和2.2中的一样,不过我们需要借助反射获取一些参数和方法返回值等。
@Component
@Aspect
public class GmallCacheAspect {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
/**
* 使用AOP实现分布式锁和缓存
* Around:环绕通知
* value:切入的位置
* 1、定义获取数据的key
* 例如获取skuInfo key === sku:skuId
* (1)获取添加了@GmallCache注解的方法
* 可以获取注解、注解的属性、方法的参数
* (2)可以尝试获取数据
*/
@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
public Object cacheGmallAspect(ProceedingJoinPoint joinPoint) throws Throwable {
//创建返回对象
Object object=new Object();
//获取添加了注解的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取注解
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
//获取属性前缀
String prefix = gmallCache.prefix();
//获取属性后缀
String suffix = gmallCache.suffix();
//获取方法传入的参数
Object[] args = joinPoint.getArgs();
//组合获取数据的key
String key=prefix+ Arrays.asList(args).toString()+suffix;
//从缓存中获取数据
object=cacheHit(key,signature);
try {
//判断
if(object==null){
//缓存中没有数据,需要从数据库查询
//定义锁的key
String lockKey=prefix+":lock";
//准备上锁 redis/redisson
RLock lock = redissonClient.getLock(lockKey);
//上锁
boolean flag = lock.tryLock(RedisConst.SKULOCK_EXPIRE_PX1, RedisConst.SKULOCK_EXPIRE_PX2, TimeUnit.SECONDS);
//判断是否成功
if(flag){
try {
//获取到了锁
//查询数据库,执行切入的方法体实际上就是查询数据库
object= joinPoint.proceed(args);
//判断是否从mysql查询到了数据
if(object==null){
//反射
Class aClass = signature.getReturnType();
//创建对象
object= aClass.newInstance();
//存储
redisTemplate.opsForValue().set(key,JSON.toJSONString(object),RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
return object;
}else{
//存储
redisTemplate.opsForValue().set(key,JSON.toJSONString(object),RedisConst.SKUKEY_TEMPORARY_TIMEOUT,TimeUnit.SECONDS);
return object;
}
} finally {
//释放锁
lock.unlock();
}
}else{
//睡眠
Thread.sleep(100);
//自旋
return cacheGmallAspect(joinPoint);
}
}else{
//从缓存中获取了数据
return object;
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
//兜底的方法--查询数据库,实际上执行方法体就是查询数据库
return joinPoint.proceed(args);
}
//从缓存中获取数据
private Object cacheHit(String key, MethodSignature signature) {
//获取数据--存储的时候,转换成JSON字符串,所以从Redis取出来的时候是个字符串
String strJson = (String) redisTemplate.opsForValue().get(key);
//判断
if(!StringUtils.isEmpty(strJson)){
//获取当前方法的返回值类型
Class returnType = signature.getReturnType();
//将字符串转换成指定的类型
return JSON.parseObject(strJson,returnType);
}
return null;
}
}
此时实现类如下:
//根据skuId查询skuInfo信息和图片列表
@Override
@GmallCache(prefix ="sku:") //key: sku:1314:info
public SkuInfo getSkuInfo(Long skuId) {
//查询数据库mysql获取数据
return getSkuInfoDB(skuId);
//使用redis实现分布式锁缓存数据
// return getSkuInfoRedis(skuId);
//使用Redisson实现分布式锁
// return getSkuInfoRedisson(skuId);
}
现在这个方法中写的是调用数据库查询的代码,不过我们在这里加了一个@GmallCache
自定义注解,其中参数prefix
是缓存中key的前缀,可以自定义。
这样每次在进入到这个方法的时候会执行我们定义的那个切面类,把分布式锁的步骤走一遍,可以看到,这样代码侵入性就比较低了,如果在其他地方也想使用分布式锁,那就直接加上这个注解,再给个前缀参数即可。