Day08 布隆过滤器与缓存切面
一、答疑
使用HashMap本地缓存,是使用内存还是硬盘?如果使用内存,是不是程序重启了,缓存就丢失了?
二、业务中缓存使用总结&问题&面试题
1、数据一致性问题
数据库修改以后,缓存的数据需要同步过来【缓存中的所有数据都应当有过期时间】
① 缓存的每个数据都必须有过期时间
允许有一段时间数据不一致,但是要保证数据最终要一致【最终一致性】
sku:info:50:后台改了50号数据,30min 分钟过期,数据过期以后,下一次对此数据的查询会触发更新逻辑,最终缓存中还是新数据
(1)双写模式
写数据库+写缓存;本来要同步缓存(改数据的同时给缓存存一份)
问题:为什么不先写缓存再写数据库呢?
答:因为写数据库的时候有可能失败,或者出现问题数据库回滚,回滚完后害得改缓存;也就是说数据库炸了缓存得改两遍,所以我们现在是先写数据库,而不是先写缓存,确定数据库写成功后,再写缓存
(2)失效模式
写数据库+删缓存;下一次查询,由于缓存中没有,自己会查数据库,然后又更新到缓存
运用到项目中(伪代码)
// 假设改完了数据
// 双写模式
redis.set("sku:info:" + skuId,skuInfoJson);
// 失效模式
redis.delete("sku:info:"+skuId);
// 注意!失效模式可能导致缓存击穿
// 假如100万请求查 skuId,但是这个 skuId 正好改了,使用了失效模式,导致缓存中没有,要查数据库,这个时候如果没有加锁解决问题,就会缓存击穿
这个两种方法都可以,99% 代码都是运行成功的,只要成功就能改掉数据,如果失败,就可以寄希望于 30min 分钟过期
② 一劳永逸的解决方案
核心:【把数据库的最后一次修改放到缓存】
(1)Canal
安装Canal;开发同步到binlog以后的代码, sku_info
sku_info_update_5; 拿到5号记录放到缓存
(2)mq
业务操作改了数据库,给mq发消息
id:5 value: xxxx updateTime: 获取到数据库的修改时间 发给mq
有个线程专门订阅mq消息,把数据放到缓存中
如果 mq 消息乱序怎么办?
缓存中此记录的时间如果大于我接下要给缓存中放的数据时间我就放弃此次操作
③ 总结
无论是双写模式还是失效模式,都会导致缓存的不一致问题,但是我们加上过期时间,有一个保底
如果多个实例同时更新会出事,怎么办?
大并发更新操作(经常写的数据,不要放缓存)
1、如果是用户纬度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,缓存数据加上过期时间,每隔一段时间触发读的主动更新即可 user:info:12->json user:info:13->json;
2、如果是菜单,商品介绍等基础数据,也可以去使用canal订阅binlog的方式
3、缓存数据双写或失效+过期时间也足够解决大部分业务对于缓存的要求
4、通过加锁保证并发读写,写写的时候按顺序排好队。读读无所谓。所以适合使用读写锁。(业务不关心脏数据,允许临时脏数据可忽略)
5、那对于读写并发都很高的,数据一致性要求很强的情况该怎么处理呢?
订单、股票(实时查库)
必须加大数据库底层集群维度,通过合适的分库(将数据分流即可)
腾讯:对应自己的数据库 (每秒都在动,一行核心记录(并发很小),额外的东西)
滴滴:对应自己的数据库
总结:
我们能放入缓存的数据本就不应该是实时性、一致性要求超高的,所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可
我们不应该过度设计,增加系统的复杂性,遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点
2、面试题
① 你们怎么用缓存的、什么是缓存一致性问题、你们怎么解决?
答:
1)、我们缓存的数据是实时性一致性要求不高(可以容忍一段时间不一致)
2)、缓存一致性问题: 缓存中的数据没有同步到数据库最新的数据
3)、我们业务代码是 失效模式+缓存数据过期时间。
4)、如果失效模式导致了失败与错误数据,会在一段时间后数据过期,下一次查询触发查询最新的
② 怎么保证大并发下,缓存中没有,查到的数据也是能放到缓存?【怎么解决缓存击穿问题、缓存雪崩问题】
答:加锁,有没有必要用分布式锁????分布式锁性能太低了······
如果直接操作:4000,缓存没有要争分布式锁:50/s,缓存中有:600/s
最终决定:只加本地锁
注意,在我们的项目中,RLock lock = redissonClient.getLock(“lock”); 这样锁是不行的!
// 查50号数据 100w sku:info:50
// 查70号数据 100w sku:info:70
// 这种加锁方式锁了200w,锁的粒度应该精细到一条记录,类似于 mysql 的行锁
RLock lock = redissonClient.getLock("lock");
那要怎么标注给某一个人加锁呢?分布式锁,锁的名字一样就是同一把锁
// 查50号数据 100w sku:info:50 lock:50
// 查70号数据 100w sku:info:70 lock:70
RLock lock = redissonClient.getLock("lock:" + skuId);
③ 分布式数据的一致性问题
raft: (强一致)【所有的操作都会通过领导】
mysql:后台同步
写: 连上master
master上改的东西一定不是立即读出来
读: 连上slaver(大概在1s左右读来)
mysql:(【不要纠结,数据要立刻同步?】量子纠缠?)
1、cpu在1ms的时候给mysql提交保存数据
2、 mysql需要3ms才保存完
3、cpu在2ms的时候要mysql此条数据
4、 mysql肯定返回没有
④ 话术
我们缓存的数据是实时性一致性要求不高的,所以我们给每个缓存数据都设置有过期时间,如果数据修改了我们会使用失效模式,直接把缓存删了,就算失效模式出现了问题,由于我们允许一段时间的不一致,所以数据会在过期后下一次查询到最新的数据,而且为了解决缓存中没有此数据,大并发下要进来查,导致的缓存击穿问题,我们加了锁,但是我们没有加分布式锁,因为我们服务器本来就不多,我们只给每一个加了本地锁,把这些请求哪怕放上十来个给 mysql 也是没有任何问题的
三、布隆过滤器
1、缓存穿透随机值攻击
我们现在为了解决缓存击穿问题用了失效模式+数据过期+本地锁,但是还有一个问题没有解决,那就是缓存穿透攻击
缓存穿透攻击:查一个不存在的值,缓存没有就总查数据库。解决方法:允许缓存null值
随机值攻击:(缓存穿透攻击)
1、/api/item/50 1~10000
2、/api/item/时间戳变成数字
2、核心思想
缓存中有的,基本上就是数据库有的
3、布隆过滤器
4、引入布隆过滤器
service
① pom.xml
<!--就有布隆过滤器-->
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
② 查看 BloomFilter
有两个核心方法 put 和 mightContain
put
mightContain
4、布隆过滤器应该怎么用
不应该用本地布隆过滤器,而是应该用分布式布隆过滤器
在 service-product 的大保存时,只要操作了数据库,赶紧告诉布隆:我插入了数据
工具包里面也引入了redssion,所以我们在 service 包中就可以不用引入了
5、项目初始化布隆&商品保存要告诉布隆
① 移 ItemServiceRedissonConfig
把 ItemServiceRedissonConfig 移到 service-util 的 com.atguigu.gmall.common.config 下
原来 service-item 下就没有 ItemServiceRedissonConfig 了,但是我们想用怎么办呢?
我们可以新建一个 ItemConfig 类,以后跟 item 相关的配置都写在这个类中
② item:ItemConfig
新建 com.atguigu.gmall.item.config.ItemConfig
/**
* 工具类提取的所有自动配置类我们使用 @Import 即可
*/
@Import(ItemServiceRedissonConfig.class)
//让 Spring Boot自己的 RedisAutoConfiguration 配置完以后再启动我们的
@AutoConfigureAfter(RedisAutoConfiguration.class)
@Configuration
public class ItemConfig {
}
重新启动测试即可
根据这个结构图,布隆过滤器不能让每个人都有,因为商品微服务 service-product 可能会被复制无数份,这样的话相同的代码可能会重复无数遍,为了方便我们给 service-product 也引入 redission 的配置
③ product:ProductConfig
新建 com.atguigu.gmall.product.config.ProductConfig
Import({ItemServiceRedissonConfig.class,MybatisPlusConfig.class})
@AutoConfigureAfter(RedisAutoConfiguration.class)
@Configuration
public class ProductConfig {
}
④ product:application.yaml
server:
port: 8000
spring:
main:
allow-bean-definition-overriding: true #允许bean定义信息的重写
datasource:
url: jdbc:mysql://192.168.200.188:3306/gmall_product?characterEncoding=utf-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
zipkin:
base-url: http://192.168.200.188:9411/
sender:
type: web
redis:
host: 192.168.200.188
port: 6379
password: yangfan
⑤ RedissonTest
com.atguigu.gmall.product.RedissonTest
@SpringBootTest
public class RedissonTest {
@Autowired
RedissonClient redissonClient;
@Test
void redissson(){
System.out.println("redissonClient = " + redissonClient);
}
}
测试
⑤ util:RedisConst
每种缓存都用自己的布隆过滤器,比如说,sku:info:XXX 用自己的,spu:info:XXX 用自己的,user:login:XXX 用自己的
因此我们抽取过一个 RedisConst
com.atguigu.gmall.common.constant.RedisConst
/**
* Redis常量配置类
* set name admin
*
* #魔法值
* Constant {
* //登录的用户
* public static final String LOGIN_USER = "aaaa";
*
* }
*
* setkey(){ redis.set(Constant.LOGIN_USER,"xxxxx") }
*
*
* getkey(){ redis.get(Constant.LOGIN_USER) }
*/
public class RedisConst {
// sku:50:info
public static final String SKUKEY_PREFIX = "sku:";
public static final String SKUKEY_SUFFIX = ":info";
//单位:秒
public static final long SKUKEY_TIMEOUT = 24 * 60 * 60;
// 定义变量,记录空对象的缓存过期时间
public static final long SKUKEY_TEMPORARY_TIMEOUT = 10 * 60;
//单位:秒 尝试获取锁的最大等待时间
public static final long SKULOCK_EXPIRE_PX1 = 1;
//单位:秒 锁的持有时间
public static final long SKULOCK_EXPIRE_PX2 = 1;
public static final String SKULOCK_SUFFIX = ":lock";
// user:7:cart
public static final String USER_KEY_PREFIX = "user:";
public static final String CART_PREFIX = "cart:info:";
public static final String CART_TEMP_PREFIX = "cart:temp:";// cart:info:5 //cart:temp:xxxxx
public static final long TEMP_CART_EXPIRE = 60 * 60 * 24 * 356; //临时购物车的过期时间
//登录用户购物车 不过期
//用户登录
//user:login:18
public static final String USER_LOGIN_KEY_PREFIX = "user:login:";
// public static final String userinfoKey_suffix = ":info";
public static final int USERKEY_TIMEOUT = 60 * 60 * 24 * 7;
//防重令牌
public static final String USER_UNREPEAT_TOKEN = "user:token:"; //USER_UNREPEAT_TOKEN+用户id
public static final int USER_UNREPEAT_TOKEN_TTL = 60 * 10;
//秒杀商品前缀
public static final String SECKILL_GOODS = "seckill:goods:";
public static final String SECKILL_ORDERS = "seckill:orders";
public static final String SECKILL_ORDERS_USERS = "seckill:orders:users";
public static final String SECKILL_STOCK_PREFIX = "seckill:stock:";
public static final String SECKILL_USER = "seckill:user:";
//用户锁定时间 单位:秒
public static final int SECKILL__TIMEOUT = 60 * 60 * 1;
}
因为其他微服务可能也要用到布隆过滤器,所以我们应该将布隆过滤器的代码抽取放到 ItemServiceRedissonConfig,
先获取布隆过滤器,注意,布隆过滤器在使用之前需要先初始化
⑥ util:ItemServiceRedissonConfig
/**
* matchIfMissing = true: 没配置就是true,不配就是默认生效
*
* @ConditionalOnProperty: 配置文件中有指定的 prefix.name 属性,、
* 并且值是 havingValue 指定的,则@Configuration 生效。
*
* 1、引入redisson依赖
* 2、给容器中放一个 RedissonClient,以后用他操作redis。
*
* redisson 做出来的锁,API都和JUC一样。
* JUC是本地锁;读写锁、信号量、可重入锁
* Redisson是分布式锁;
*/
@ConditionalOnProperty(prefix = "redisson", name = "enable",
havingValue = "true", matchIfMissing = true)
@Configuration
public class ItemServiceRedissonConfig {
// Lock lock = new ReentrantLock(); 对象一样是同一把锁
/**
* 给容器中放一个操作redis的客户端,redisson工具
* 1、这个方法的参数 RedisProperties ,Spring会自动从容器中获取
*
* @return
*/
@Bean
public RedissonClient redissonClient(RedisProperties redisProperties) {
// RedissonClient redisson = Redisson.create(); //连接本地redis
String host = redisProperties.getHost();
int port = redisProperties.getPort();
String password = redisProperties.getPassword();
// redis:// or rediss://
Config config = new Config();
config.useSingleServer().setAddress("redis://" + host + ":" + port)
.setPassword(password)
;
// long timeout = config.getLockWatchdogTimeout(); //看门狗时间用来自动续期
RedissonClient redisson = Redisson.create(config);
return redisson;
}
/**
* 提前准备的布隆过滤器
* @param redissonClient
* @return
*/
@Primary // Spring 内同样类型的元素有多个,标 Primary 表示不精确指定的话,默认是这个
@Bean // 每个bean的id就是方法名
public RBloomFilter<Object> skuFilter(RedissonClient redissonClient){
//sku的布隆过滤器
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKUKEY_PREFIX);
//用之前要初始化
bloomFilter.tryInit(1000000L,0.01);
return bloomFilter;
}
@Bean // 每个bean的id就是方法名
public RBloomFilter<Object> userFilter(RedissonClient redissonClient){
//user的布隆过滤器
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.USER_KEY_PREFIX);
//用之前要初始化
bloomFilter.tryInit(1000000L, 0.001);
// bloomFilter.migrate(); //重新创建的布隆
return bloomFilter;
}
}
⑦ service-product:SkuInfoServiceImpl
只要插入了数据,赶紧告诉布隆
@Qualifier(BloomName.SKU)
@Autowired
RBloomFilter<Object> skuFilter;
//保存SKU信息
@Transactional
@Override
public boolean bigSaveSkuInfo(SkuInfo skuInfo) {
int insert = skuInfoMapper.insert(skuInfo);
Long skuId = skuInfo.getId();
//只要操作了数据库,赶紧告诉布隆,我插入了数据
//2、保存sku图片信息 sku_image
List<SkuImage> skuImageList = skuInfo.getSkuImageList();
if (!CollectionUtils.isEmpty(skuImageList)) {
for (SkuImage skuImage : skuImageList) {
skuImage.setSkuId(skuId);
skuImageMapper.insert(skuImage);
}
}
//3、保存sku的平台属性对应的所有值 sku_attr_value
//attr_id value_id sku_id
// 1 2 sku_id
List<SkuAttrValue> skuAttrValueList = skuInfo.getSkuAttrValueList();
if (!CollectionUtils.isEmpty(skuAttrValueList)) {
for (SkuAttrValue skuAttrValue : skuAttrValueList) {
skuAttrValue.setSkuId(skuId);
skuAttrValueMappper.insert(skuAttrValue);
}
}
//4、保存sku的销售属性以及值信息 sku_sale_attr_value
// sku_id spu_id sale_attr_value_id来源于spu_sale_attr_value的id
// "saleAttrValueId":"123", spu_sale_attr_value的id
// "baseSaleAttrId ":"1", base_sale_attr的id
List<SkuSaleAttrValue> skuSaleAttrValueList = skuInfo.getSkuSaleAttrValueList();
if (!CollectionUtils.isEmpty(skuSaleAttrValueList)) {
for (SkuSaleAttrValue skuSaleAttrValue : skuSaleAttrValueList) {
skuSaleAttrValue.setSkuId(skuId);
//注意回填spuId
skuSaleAttrValue.setSpuId(skuInfo.getSpuId());
skuSaleAttrValueMapper.insert(skuSaleAttrValue);
}
}
//假设改完了数据
// skuId
// redis.set("sku:info:"+skuId,skuInfoJson) //双写
// redis.delete("sku:info:"+skuId) //失效
// //失效模式可能导致缓存击穿。 100万请求查skuId,正好改了,
// 使用了失效模式。导致缓存中没有要查数据库。(如果没有加锁解决问题),就会缓存击穿
//让bloomfilter保存一下
//缓存按照key:
String cacheKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;
skuFilter.add(cacheKey);
return true;
}
启动测试
sku 的布隆
user 的布隆
接下来看如何防止缓存穿透攻击,首先需要在查询数据库之前设置好布隆过滤器
6、布隆过滤器的使用
① item:ItemServiceImpl
@Service
@Slf4j
public class ItemServiceImpl implements ItemService {
@Qualifier(BloomName.SKU)
@Autowired
RBloomFilter<Object> skuFilter;
// ······
}
这个方法要查 skuInfo,我们不用在这里写布隆过滤器,可以直接在 controller 里面写,让请求一来就直接先问布隆有没有
② item:SkuInfoController
@RestController
@RequestMapping("/api/item")
public class SkuInfoController {
@Autowired
SpuFeignClient spuFeignClient;
@Autowired
ItemService itemService;
@Qualifier(BloomName.SKU) // skuFilter
@Autowired
RBloomFilter<Object> skuFilter;
@GetMapping("/{skuId}")
public Result getItem(@PathVariable Long skuId) {
// 不害怕随机值攻击
String cacheKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;
// 布隆说有
if (skuFilter.contains(cacheKey)) {
// itemService 远程调用 service-product 获取到当前商品的所有信息
Map<String, Object> skuInfo = itemService.getSkuInfo(skuId);
return Result.ok(skuInfo);
}
return Result.ok(skuInfo);
}
}
③ 测试
查询51号数据,因为这个数据是在布隆过滤器之前数据库就有的,压根就没有告诉布隆过滤器,所以肯定查不到
在商品后台管理中重新添加一个新的数据
查询53号商品,为什么51号商品查不到,53号查得到呢?因为添加51号商品时没有告诉布隆,添加53号告诉布隆了
④ BloomFilterAddController(重建布隆)
布隆过滤器添加所有商品
新建 com.atguigu.gmall.product.controller.BloomFilterAddController
@RestController
public class BloomFilterAddController {
@Autowired
SkuInfoService skuInfoService;
@Qualifier(BloomName.SKU)
@Autowired
RBloomFilter<Object> skuFilter;
/**
* 布隆过滤器添加所有商品
*
* 布隆重建
* @return
*/
@GetMapping("/add/bloom/sku")
// 可以设置定时任务,每隔7天晚上3:30,执行一次这个方法
public Result bloomAddAll() {
// 删除旧布隆,redis中的数据旧删除了
skuFilter.delete();
// TODO:服务器时间判断,不是晚上不让做
// 重新初始化一个新bloom,注意这里的布隆要和之前 ItemServiceRedissonConfig 初始化的配置一样
skuFilter.tryInit(1000000L,0.01);
// 获取所有 skuId
List<Long> skuIds = skuInfoService.getAllId();
for (Long skuId : skuIds) {
// 新的布隆
skuFilter.add(RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX);
}
return Result.ok();
}
}
⑤ SkuInfoService
List<Long> getAllId();
⑥ SkuInfoServiceImpl
@Override
public List<Long> getAllId() {
return skuInfoMapper.getAllId();
}
⑦ SkuInfoMapper
List<Long> getAllId();
⑧ SkuInfoMapper.xml
<select id="getAllId" resultType="java.lang.Long">
select id from sku_info
</select>
⑨ 测试
先清空 redis 中的数据
启动微服务
此时的 redis 中
前端发请求给布隆过滤器中添加数据
此时再查询
7、布隆过滤器的总结
① 讨论
② 使用逻辑
三、分布式缓存切面+自定义缓存注解测试
通过规律我们可以发现,缓存任何值都是上面的那一套逻辑,sku 是这么写的,spu 也是,所以既然是一个模式,那我们就能把它抽取出来。我们希望未来的业务是这样的,把缓存代码抽取作为一个AOP切面,自定义注解,在希望使用缓存的方法上可以直接标注该注解 @GmallCache,标注后就可以使用布隆过滤器+本地锁防止各种缓存问题然后把数据缓存到 redis,缓存中有就不用调该注解,缓存中没有才需要调用该注解
1、思路
1、只要方法返回的数据需要放缓存。走缓存的一堆逻辑,还要防止各种穿透
只需要利用AOP
1、切入到加了 @GmallCache 注解的这些方法
伪代码
try{
//前置通知
//1、先问布隆有没有
//1.1)、布隆说没有。直接返回null,目标方法不调用
//1.2)、布隆说有,查缓存
//1.2.1)、缓存中有,直接返回
//1.2.2)、缓存中没有,放行目标方法,此时加锁
//目标方法执行 data = getSkuInfo(skuId) / getUserInfo(userId) / getCart(cartId)
//返回通知
//1.3)、把返回的数据放缓存
}catch(Exeception e){
//异常通知,远程服务没启动
//返回null
}finally{
//后置通知
//解锁
}
哪种通知都能阻拦目标方法?
环绕通知
切入点表达;看谁标注了我们自己定义的注解
参考 spring 官网
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#spring-core
① @annotation 的使用
@annotation 看标注了哪一个注解
② @Around 的使用
ProceedingJoinPoint:程序的连接点
2、自定义缓存切面
① util:GmallCacheAspect
新建 com.atguigu.gmall.common.cache.GmallCacheAspect
@Slf4j
@Aspect
@Component
public class GmallCacheAspect {
/**
* 所有能标了 @GmallCache 的一般都是id查
* 环绕通知 = 前置 + 返回 + 后置 + 异常
*
* @param pjp 连接点
* @return
*/
//哪种通知都能阻拦目标方法??? 环绕通知
//切入点表达;看谁标注了我们自己定义的注解
@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {
// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuId
Object[] args = pjp.getArgs();
log.info("分布式缓存切面,前置通知···");
try {
// 前置通知
// 目标方法的执行,目标方法的异常切面不要吃掉
Object proceed = pjp.proceed(args);
// 返回通知
log.info("分布式缓存切面,返回通知···");
return proceed;
} finally {
// 后置通知
log.info("分布式缓存切面,后置通知···");
}
}
}
// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuId
Object[] args = pjp.getArgs();
3、自定义缓存注解
① util:GmallCache
新建 com.atguigu.gmall.common.cache.GmallCache
public @interface GmallCache {
}
② item:ItemServiceImpl
把 getSkuInfo 改为 getSkuInfoWithRedissonDistributeLock,去掉 @Override,重新再写一个 getSkuInfo
让一切回归最原本的状态
@GmallCache //需要一个切面类,动态切入所有标了这个注解的方法
@Override
public Map<String, Object> getSkuInfo(Long skuId) {
//查数据
log.info("要查数据库了....");
HashMap<String, Object> map = getFromServiceItemFeign(skuId);
return map;
}
③ item:SkuInfoController
@RestController
@RequestMapping("/api/item")
public class SkuInfoController {
@Autowired
ItemService itemService;
@GetMapping("/{skuId}")
public Result getItem(@PathVariable Long skuId) {
Map<String, Object> skuInfo = itemService.getSkuInfo(skuId);
return Result.ok(skuInfo);
}
}
④ item:ItemConfig
/**
* 工具类提取的所有自动配置类我们使用 @Import 即可
* 导入自己的 GmallCacheAspect.class 分布式缓存切面
*/
@Import({ItemServiceRedissonConfig.class, GmallCacheAspect.class})
@EnableAspectJAutoProxy //开启切面功能
/**
* 工具类提取的所有自动配置类我们使用 @Import 即可
* 导入自己的 GmallCacheAspect.class 分布式缓存切面
*/
@Import({ItemServiceRedissonConfig.class, GmallCacheAspect.class})
//让 Spring Boot自己的RedisAutoConfiguration 配置完以后再启动我们的
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableAspectJAutoProxy //开启切面功能
@Configuration
public class ItemConfig {
}
⑤ 测试
启动微服务
给切面打断点
给目标方法打断点
前端发请求
目标方法还没执行,先来到切面,切面里面有个切入点表达式
targetClass:将要执行的类
target:类的对象 + 类的所有属性
方法
Object[] args = pjp.getArgs();拿到目标方法要用的所有参数
目标方法还没执行,切面就先给它拦住了,先执行切面的前置通知
放行后,来到下一个断点,要查数据库了
继续放行
测试完成,我们可以将响应的代码写到各个通知的位置了
如果用 postman 发请求的话
四、分布式缓存切面+自定义缓存注解
1、第一版
① GmallCache
我们先给自定义缓存注解加一些元注解信息,可以抄已有的注解,例如:Transactional 注解
GmallCache
com.atguigu.gmall.common.cache.GmallCache
/**
* @Target({ElementType.METHOD}) 代表标注在方法上
* @Retention(RetentionPolicy.RUNTIME) 代表运行时有效
* @Inherited 代表可继承的
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface GmallCache {
String bloomPrefix() default "";
String bloomSuffix() default "";
}
以前的代码中,布隆过滤器会查询这个 cacheKey,需要用 prefix 和 suffix 与 skuId 拼接,在切面中可以通过 Object[] args = pjp.getArgs();
拿到目标方法要用的所有参数,也就是目标方法 public Map<String, Object> getSkuInfo(Long skuId) {...}
上的参数 skuId,但是 prefix 和 suffix 就比较麻烦,所以我们就可以直接在 GmallCache 中把 prefix 和 suffix 写上
② ItemServiceImpl
观察上图,在切面中要看布隆过滤器中有没有,我们得自己拼上这一串String cacheKey = RedisConst.SKUKEY_PREFIX + skuId + RedisConst.SKUKEY_SUFFIX;
但这个又是动态的,这里思考一下为什么是动态的?因为我们要执行的不只是 skuId 的方法,以后 spu、user 等等可能都要走这段逻辑;那动态的咋拿呢?我们可以在目标方法上的 @GmallCache
注解中写上@GmallCache(bloomPrefix = RedisConst.SKUKEY_PREFIX,bloomSuffix = RedisConst.SKUKEY_SUFFIX
,然后再到 GallCacheAspect 中拼接就可以了
@GmallCache(
bloomPrefix = RedisConst.SKUKEY_PREFIX,
bloomSuffix = RedisConst.SKUKEY_SUFFIX
) //需要一个切面类,动态切入所有标了这个注解的方法
@Override
public Map<String, Object> getSkuInfo(Long skuId) {
//查数据
log.info("要查数据库了....");
HashMap<String, Object> map = getFromServiceItemFeign(skuId);
return map;
}
③ GallCacheAspect
上面说要把 bloomPrefix、bloomSuffix 与 skuId 拼接,步骤如下:
- 拿到当前方法 @GmallCache 标记注解的值
- 拿到当前方法的详细信息
MethodSignature signature = (MethodSignature) pjp.getSignature();
- 拿到标记的注解的值
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
- 拿到当前方法的详细信息
根据前面分析的思路完成缓存切面类 GmallCacheAspect
com.atguigu.gmall.common.cache.GmallCacheAspect
@Slf4j
@Aspect
@Component
public class GmallCacheAspect {
/**
* 所有能标了 @GmallCache 的一般都是id查
* 环绕通知 = 前置 + 返回 + 后置 + 异常
*
* @param pjp 连接点
* @return
*/
//哪种通知都能阻拦目标方法??? 环绕通知
//切入点表达;看谁标注了我们自己定义的注解
@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {
// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuId
Object[] args = pjp.getArgs();
log.info("分布式缓存切面,前置通知···");
// 拿到当前方法 @GmallCache 标记注解的值
// 拿到当前方法的详细信息
MethodSignature signature = (MethodSignature) pjp.getSignature();
// 拿到标记的注解的值
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
try {
// 前置通知
// 目标方法的执行
// 这里的目标方法指的是查数据库,因此返回的 proceed 是从数据库中查询到的数据
Object proceed = pjp.proceed(args);
// 返回通知
log.info("分布式缓存切面,返回通知···");
return proceed;
} finally {
// 后置通知
log.info("分布式缓存切面,后置通知···");
}
}
}
(1)拿到当前方法的详细信息
MethodSignature(org.aspectj.lang.reflect 包下的)
(2)获取当前方法标注的注解
前端发送请求
后端
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
继续完善
2、第二版
① GallCacheAspect
所有能标了 @GmallCache 的一般都是id查 skufilter.contains(bloomPrefix+ args[0] +bloomSuffix);
就不能写成skufilter.contains(bloomPrefix+ Array.asList(args) +bloomSuffix);
@Slf4j
@Aspect
@Component
public class GmallCacheAspect {
@Qualifier(BloomName.SKU)
@Autowired
RBloomFilter<Object> skuFilter;//要把全系统的布隆过滤器都加入进来,进行选择
/**
* 所有能标了 @GmallCache 的一般都是id查
* 环绕通知 = 前置 + 返回 + 后置 + 异常
*
* @param pjp 连接点
* @return
*/
//哪种通知都能阻拦目标方法??? 环绕通知
//切入点表达;看谁标注了我们自己定义的注解
@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {
// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuId
Object[] args = pjp.getArgs();
log.info("分布式缓存切面,前置通知···");
// 拿到当前方法 @GmallCache 标记注解的值
// 拿到当前方法的详细信息
MethodSignature signature = (MethodSignature) pjp.getSignature();
// 拿到标记的注解的值
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
String bloomPrefix = gmallCache.bloomPrefix();
String bloomSuffix = gmallCache.bloomSuffix();
skufilter.contains(bloomPrefix+ args[0] +bloomSuffix);
try {
// 前置通知
// 目标方法的执行
// 这里的目标方法指的是查数据库,因此返回的 proceed 是从数据库中查询到的数据
Object proceed = pjp.proceed(args);
// 返回通知
log.info("分布式缓存切面,返回通知···");
return proceed;
} finally {
// 后置通知
log.info("分布式缓存切面,后置通知···");
}
}
}
② 测试
postman 发送请求
后端
计算 bloomPrefix+ args[0] +bloomSuffix
计算 skufilter.contains(bloomPrefix+ args[0] +bloomSuffix);
3、第三版
① GmallCache
/**
* @Target({ElementType.METHOD}) 代表标注在方法上
* @Retention(RetentionPolicy.RUNTIME) 代表运行时有效
* @Inherited 代表可继承的
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface GmallCache {
String bloomPrefix() default "";
String bloomSuffix() default "";
//以毫秒为单位
long ttl() default -1L;
}
② ItemServiceImpl
@GmallCache(
bloomPrefix = RedisConst.SKUKEY_PREFIX,
bloomSuffix = RedisConst.SKUKEY_SUFFIX,
ttl = 1000 * 60 * 30
) //需要一个切面类,动态切入所有标了这个注解的方法
@Override
public Map<String, Object> getSkuInfo(Long skuId) {
//查数据
log.info("要查数据库了....");
HashMap<String, Object> map = getFromServiceItemFeign(skuId);
return map;
}
③ GallCacheAspect
@Slf4j
@Aspect
@Component
public class GmallCacheAspect {
@Qualifier(BloomName.SKU)
@Autowired
RBloomFilter<Object> skuFilter;//要把全系统的布隆过滤器都加入进来,进行选择
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedissonClient redissonClient;
/**
* 所有能标了 @GmallCache 的一般都是id查
* 环绕通知 = 前置 + 返回 + 后置 + 异常
*
* @param pjp 连接点
* @return
*/
//哪种通知都能阻拦目标方法??? 环绕通知
//切入点表达;看谁标注了我们自己定义的注解
@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {
// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuId
Object[] args = pjp.getArgs();
log.info("分布式缓存切面,前置通知···");
// 拿到当前方法 @GmallCache 标记注解的值
// 拿到当前方法的详细信息
MethodSignature signature = (MethodSignature) pjp.getSignature();
// 拿到标记的注解的值
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
String bloomPrefix = gmallCache.bloomPrefix();
String bloomSuffix = gmallCache.bloomSuffix();
long ttl = gmallCache.ttl();
String redisCacheKey = bloomPrefix + args[0] + bloomSuffix;
String intern = redisCacheKey.intern();
log.info("redisCacheKey:对象地址:",intern);
boolean contains = skufilter.contains(redisCacheKey);
if (!contains){
// 布隆过滤器没有此值
return null;
}
try {
// 前置通知
// 1、先看缓存有没有
Map<String, Object> cache = getFromCache(redisCacheKey);
if (cache == null || cache.keySet().size() == 0){
// 如果缓存中没有,再执行目标方法
log.info("分布式缓存切面,准备加锁执行目标方法······");
// 对象锁,这里只能锁 sku:intern:info 单个,比如说 sku:51:info
synchronized (intern){
// 按照并发,短时间,jvm 会缓存到元空间
// 第一个人 sku:51:info,常量池就有,100w直接涌进来都用这个字符串,同样的字符串内存地址一样
// sku:51:info 只有一个放行了
log.info("抢锁成功···正在双检查···");
Map<String, Object> reCache = getFromCache(redisCacheKey);
if (reCache == null){
// 目标方法的执行,目标方法的异常切面不要吃掉
// 这里的目标方法指的是查数据库,因此返回的 proceed 是从数据库中查询到的数据
Object proceed = pjp.proceed(args);
saveToCache(redisCacheKey,proceed,ttl);
return proceed;
}
return reCache;
}
}
// 返回通知
// 缓存有数据,直接返回
return cache;
} finally {
// 后置通知
log.info("分布式缓存切面,如果用的是分布式锁就要在这里解锁···");
}
}
/**
* 把数据保存到缓存
* @param redisCacheKey
* @param proceed
* @param ttl
*/
private void saveToCache(String redisCacheKey, Object proceed, long ttl) throws JsonProcessingException {
// TODO 可能会缓存空值
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(proceed);
stringRedisTemplate.opsForValue().set(redisCacheKey,json,ttl, TimeUnit.MILLISECONDS);
}
/**
* 把数据从缓存中加载出来
* @param cacheKey
* @return
*/
private Map<String, Object> getFromCache(String cacheKey) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
// 缓存是 JSON
String json = stringRedisTemplate.opsForValue().get(cacheKey);
if (!StringUtils.isEmpty(json)){
Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
});
return map;
}
return null;
}
}
(1)为什么不能用 synchronized(this)
首先先搞清楚这个 this 代表谁?
this 代表当前对象,当前对象就是切面,切面在全系统里面就只有一个,所以用 this 没问题
但是,只要标注了这个 GmallCache 注解的,比如缓存用户、图书,因为缓存用户、图书用的都是一个切面类,相当于它们都加了同一把 this 锁,这就不可以了
所以这里要锁的话就用一个常量,比如 synchronized(redisCacheKey.intern()),字符串是在常量池的,每个 redisCacheKey 进来都有它唯一的字符串,比如说查51号就是 sku:51:info,sku:51:info 在常量池就只有一个地址,百万并发都查51号,51号就锁住了,相当于同一个商品用了一把锁;但是如果用 synchronized(this),百万并发查51、52、53······就全都会锁住,因为一个 this 就是代表这个切面类
(2)synchronized (intern)
//锁用的是同样字符串在池里面的地址,字符串一样底层的intern就一样
String intern = redisCachekey.intern();
// ······
synchronized (intern) {
// ······
}
字符串是常量池的,每一个人进来都有它唯一的字符串,例如查询50,sku:50:info,百万并发进来都查询50号,50号就锁住了
字符串在常量池就一个地址,相当于同一个商品用了一把锁
看这个锁能不能锁住 sku:50:info 只有一个放行了,按照并发,短时间,jvm会缓存到元空间
第一个人sku:50:info,常量池就有,100w直接涌进来都用这个字符串,同样的字符串内存地址一样
在这个应用只有一个副本的情况下,查数据库最多一次;在这个应用有N个副本的情况下,查数据库最多N次
④ 测试切面工作是否正常
清理 redis,数据都清空了,只有布隆的占位和配置信息
postman 发送请求
后端
拿到方法的详情信息、注解的详细信息,根据 redisCacheKey 判断布隆过滤器中有没有
布隆过滤器中有,先看缓存中有没有,step into 到 getFromCache 查询
缓存中没有,getFromCache 返回 null
缓存中没有,准备加锁执行目标方法查询数据库
目标方法远程查询出数据
目标方法执行完,把数据给缓存中存一份
查看 redis 缓存
然后 controller 返回
⑤ 测试是否锁住了
压力测试 synchronized (redisCacheKey.intern())
多次测试发现 String redisCacheKey = bloomPrefix + args[0] + bloomSuffix; redisCacheKey 的值在不断变化
String intern = redisCacheKey.intern(); intern 的值还是 10009
压力测试
只查了一遍数据库
4、第四版
① 分析
现在有个问题,sku 用的是 sku 的布隆过滤器,spu 应该有 spu 的布隆过滤器,XXX 应该有 XXX 的布隆过滤器,未来我们的业务会很多,都应该有自己的布隆过滤器,这样每个布隆过滤器都很小,效率很高
来到切面我们可以看到,一进来用的是 sku 的布隆,我们要怎么才能自己用自己的呢?
② BloomName
新建 com.atguigu.gmall.common.cache.BloomName
public class BloomName {
public static final String SKU = "skubloomFilter";
public static final String SPU = "spubloomFilter";
public static final String USER = "userbloomFilter";
}
③ ItemServiceRedissonConfig
/**
* 提前准备的布隆过滤器
* @param redissonClient
* @return
*/
// @Primary
@Bean(value = BloomName.SKU) //每个bean的id就是方法名
public RBloomFilter<Object> skuFilter(RedissonClient redissonClient){
//sku的布隆过滤器
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.SKUKEY_PREFIX);
//用之前要初始化
bloomFilter.tryInit(1000000L,0.01);
return bloomFilter;
}
@Bean(value = BloomName.SPU)
public RBloomFilter<Object> userFilter(RedissonClient redissonClient){
//sku的布隆过滤器
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(RedisConst.USER_KEY_PREFIX);
//用之前要初始化
bloomFilter.tryInit(1000000L, 0.001);
// bloomFilter.migrate(); //重新创建的布隆
return bloomFilter;
}
④ GmallCache
加 String bloomName() default “”;
/**
* @Target({ElementType.METHOD}) 代表标注在方法上
* @Retention(RetentionPolicy.RUNTIME) 代表运行时有效
* @Inherited 代表可继承的
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface GmallCache {
String bloomPrefix() default "";
String bloomSuffix() default "";
String bloomName() default "";
//以毫秒为单位
long ttl() default -1L;
}
⑤ GmallCacheAspect
我们之前的布隆过滤器用的是这个,但是现在我们要让他动态变化
改进后:
@Slf4j
@Aspect
@Component
public class GmallCacheAspect {
@Qualifier(BloomName.SKU)
@Autowired
RBloomFilter<Object> skufilter;
/**
* Spring 会自动从容器中拿到所有 RBloomFilter 类型的组件
* 给我们封装好map,map的key用的就是组件的名,值就是组件对象
*/
@Autowired
Map<String, RBloomFilter<Object>> blooms;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {
// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuId
Object[] args = pjp.getArgs();
log.info("分布式缓存切面,前置通知···");
// 拿到当前方法 @GmallCache 标记注解的值
// 拿到当前方法的详细信息
MethodSignature signature = (MethodSignature) pjp.getSignature();
// 拿到标记的注解的值
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
String bloomPrefix = gmallCache.bloomPrefix();
String bloomSuffix = gmallCache.bloomSuffix();
long ttl = gmallCache.ttl();
// 布隆过滤器的名字
String bloomName = gmallCache.bloomName();
String redisCacheKey = bloomPrefix + args[0] + bloomSuffix;
String intern = redisCacheKey.intern();
log.info("redisCacheKey:对象地址:",intern);
// 通过布隆过滤器的名字拿到我们要用的布隆过滤器
RBloomFilter<Object> bloomFilter = blooms.get(bloomName);
// 此时的布隆过滤器就是全动态的了
boolean contains = bloomFilter.contains(redisCacheKey);
if (!contains){
// 布隆过滤器没有此值
return null;
}
try {
// 前置通知
// 1、先看缓存有没有
Map<String, Object> cache = getFromCache(redisCacheKey);
if (cache == null || cache.keySet().size() == 0){
// 如果缓存中没有,再执行目标方法
log.info("分布式缓存切面,准备加锁执行目标方法······");
// 对象锁,这里只能锁 sku:intern:info 单个,比如说 sku:51:info
synchronized (intern){
// 按照并发,短时间,jvm 会缓存到元空间
// 第一个人 sku:51:info,常量池就有,100w直接涌进来都用这个字符串,同样的字符串内存地址一样
// sku:51:info 只有一个放行了
log.info("抢锁成功···正在双检查···");
Map<String, Object> reCache = getFromCache(redisCacheKey);
if (reCache == null){
// 目标方法的执行,目标方法的异常切面不要吃掉
Object proceed = pjp.proceed(args);
saveToCache(redisCacheKey,proceed,ttl);
return proceed;
}
return reCache;
}
}
// 返回通知
// 缓存有数据,直接返回
return cache;
} finally {
// 后置通知
log.info("分布式缓存切面,如果用的是分布式锁就要在这里解锁···");
}
}
/**
* 把数据保存到缓存
* @param redisCacheKey
* @param proceed
* @param ttl
*/
private void saveToCache(String redisCacheKey, Object proceed, long ttl) throws JsonProcessingException {
// TODO 可能会缓存空值
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(proceed);
stringRedisTemplate.opsForValue().set(redisCacheKey,json,ttl, TimeUnit.MILLISECONDS);
}
/**
* 把数据从缓存中加载出来
* @param cacheKey
* @return
*/
private Map<String, Object> getFromCache(String cacheKey) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
// 缓存是 JSON
String json = stringRedisTemplate.opsForValue().get(cacheKey);
if (!StringUtils.isEmpty(json)){
Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
});
return map;
}
return null;
}
}
此时启动会报错,要把之前用的 skuiflter 改成 BloomName.SKU
⑥ BloomFilterAddController
@Qualifier(BloomName.SKU)
⑦ SkuInfoServiceImpl
把之前用的 skuiflter 改成 BloomName.SKU
@Qualifier(BloomName.SKU)
⑧ SkuInfoController
把之前用的 skuiflter 改成 BloomName.SKU
@Qualifier(BloomName.SKU)
@Autowired
RBloomFilter<Object> skuFilter;
⑨ ItemServiceImpl
AOP+注解完成的
分布式布隆过滤防击穿高性能本地锁数据缓存切面
@Qualifier(BloomName.SKU)
// ······
@GmallCache(
bloomPrefix = RedisConst.SKUKEY_PREFIX,
bloomSuffix = RedisConst.SKUKEY_SUFFIX,
ttl = 1000 * 60 * 30
) //需要一个切面类,动态切入所有标了这个注解的方法
@Override
public Map<String, Object> getSkuInfo(Long skuId) {
//查数据
log.info("要查数据库了....");
HashMap<String, Object> map = getFromServiceItemFeign(skuId);
return map;
}
⑩ 测试
postman 发送请求
先看这个 map,key 是布隆过滤器的名字,值是对象,以后想用哪个用哪个
XI 未解决的问题
saveToCache 可能会缓存空值
5、第五版
① GmallCache
/**
* @Target({ElementType.METHOD}) 代表标注在方法上
* @Retention(RetentionPolicy.RUNTIME) 代表运行时有效
* @Inherited 代表可继承的
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface GmallCache {
String bloomPrefix() default "";
String bloomSuffix() default "";
String bloomName() default "";
//以毫秒为单位,真正的数据缓存时长
long ttl() default -1L;
//缓存空值,不宜太长,这里设置3分钟
long missDataTtl() default 1000 * 60 * 3L;
}
② ItemServiceImpl
@Qualifier(BloomName.SKU)
// ······
@GmallCache(
bloomPrefix = RedisConst.SKUKEY_PREFIX,
bloomSuffix = RedisConst.SKUKEY_SUFFIX,
ttl = 1000 * 60 * 30,
missDataTtl = 1000 * 60 * 10
) //需要一个切面类,动态切入所有标了这个注解的方法
@Override
public Map<String, Object> getSkuInfo(Long skuId) {
//查数据
log.info("要查数据库了....");
HashMap<String, Object> map = getFromServiceItemFeign(skuId);
return map;
}
③ GmallCacheAspect
@Slf4j
@Aspect
@Component
public class GmallCacheAspect {
@Qualifier(BloomName.SKU)
@Autowired
RBloomFilter<Object> skufilter;
/**
* Spring 会自动从容器中拿到所有 RBloomFilter 类型的组件
* 给我们封装好map,map的key用的就是组件的名,值就是组件对象
*/
@Autowired
Map<String, RBloomFilter<Object>> blooms;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {
// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuId
Object[] args = pjp.getArgs();
log.info("分布式缓存切面,前置通知···");
// 拿到当前方法 @GmallCache 标记注解的值
// 拿到当前方法的详细信息
MethodSignature signature = (MethodSignature) pjp.getSignature();
// 拿到标记的注解的值
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
String bloomPrefix = gmallCache.bloomPrefix();
String bloomSuffix = gmallCache.bloomSuffix();
String bloomName = gmallCache.bloomName();
long ttl = gmallCache.ttl();
long missDataTtl = gmallCache.missDataTtl();
String redisCacheKey = bloomPrefix + args[0] + bloomSuffix;
String intern = redisCacheKey.intern();
log.info("redisCacheKey:对象地址:", intern);
// 拿到我们要用的布隆过滤器
RBloomFilter<Object> bloomFilter = blooms.get(bloomName);
boolean contains = bloomFilter.contains(redisCacheKey);
if (!contains) {
// 布隆过滤器没有此值
return null;
}
try {
// 前置通知
// 1、先看缓存有没有
Map<String, Object> cache = getFromCache(redisCacheKey);
if (cache == null || cache.keySet().size() == 0) {
// 如果缓存中没有,再执行目标方法
log.info("分布式缓存切面,准备加锁执行目标方法······");
// 对象锁,这里只能锁 sku:intern:info 单个,比如说 sku:51:info
synchronized (intern) {
// 按照并发,短时间,jvm 会缓存到元空间
// 第一个人 sku:51:info,常量池就有,100w直接涌进来都用这个字符串,同样的字符串内存地址一样
// sku:51:info 只有一个放行了
log.info("抢锁成功···正在双检查···");
Map<String, Object> reCache = getFromCache(redisCacheKey);
if (reCache == null) {
// 目标方法的执行,目标方法的异常切面不要吃掉
Object proceed = pjp.proceed(args);
saveToCache(redisCacheKey, proceed, ttl, missDataTtl);
return proceed;
}
return reCache;
}
}
// 返回通知
// 缓存有数据,直接返回
return cache;
} finally {
// 后置通知
log.info("分布式缓存切面,如果用的是分布式锁就要在这里解锁···");
}
}
/**
* 把数据保存到缓存
*
* @param redisCacheKey
* @param proceed
* @param ttl
*/
private void saveToCache(String redisCacheKey, Object proceed, long ttl, long missDataTtl) throws JsonProcessingException {
//可能会缓存空值
if (proceed != null) {
ObjectMapper objectMapper = new ObjectMapper();
String jsonStr = objectMapper.writeValueAsString(proceed);
// 较久的缓存
stringRedisTemplate.opsForValue().set(redisCacheKey, jsonStr, ttl, TimeUnit.MILLISECONDS);
} else {
// 较短的缓存
stringRedisTemplate.opsForValue().set(redisCacheKey, "miss", missDataTtl, TimeUnit.MILLISECONDS);
}
}
/**
* 把数据从缓存中加载出来
*
* @param cacheKey
* @return
*/
private Map<String, Object> getFromCache(String cacheKey) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
// 缓存是 JSON
String json = stringRedisTemplate.opsForValue().get(cacheKey);
if ("miss".equals(json)) {
return null;
}
if (!StringUtils.isEmpty(json)) {
Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
});
return map;
}
return null;
}
}
④ getFromCache 和判断缓存中有没有可以改进
我们现在从缓存中拿到的都是Map类型的数据,直接写死了,如果缓存中有的数据它就是一个对象,这个时候就不合适了,从缓存中返回到的应该是目标方法的返回值类型
// ···
// 前置通知
// 1、先看缓存有没有
Object cache = getFromCache(redisCacheKey);
if (cache == null) {
// ···
/**
* 把数据从缓存加载
* <p>
* 别把这个写死 Map<String, Object>
*
* @param cacheKey
* @return
*/
private Map<String, Object> getFromCache(String cacheKey,Method method) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
// 缓存是 JSON
String json = stringRedisTemplate.opsForValue().get(cacheKey);
if ("miss".equals(json)) {
return null;
}
if (!StringUtils.isEmpty(json)) {
Object readValue = objectMapper.readValue(json, method.getReturnType());
return readValue;
}
return null;
}
⑤ 改进后的 GmallCacheAspect
@Slf4j
@Aspect
@Component
public class GmallCacheAspect {
@Qualifier(BloomName.SKU)
@Autowired
RBloomFilter<Object> skufilter;
/**
* Spring 会自动从容器中拿到所有 RBloomFilter 类型的组件
* 给我们封装好map,map的key用的就是组件的名,值就是组件对象
*/
@Autowired
Map<String, RBloomFilter<Object>> blooms;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")
public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {
// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuId
Object[] args = pjp.getArgs();
log.info("分布式缓存切面,前置通知···");
// 拿到当前方法 @GmallCache 标记注解的值
// 拿到当前方法的详细信息
MethodSignature signature = (MethodSignature) pjp.getSignature();
// 使用目标方法的返回值类型,序列化和反序列化 redis 的数据
// 为了方便将当前方法全部信息往下传递
Method method = signature.getMethod();
// 拿到标记的注解的值
GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
String bloomPrefix = gmallCache.bloomPrefix();
String bloomSuffix = gmallCache.bloomSuffix();
String bloomName = gmallCache.bloomName();
long ttl = gmallCache.ttl();
long missDataTtl = gmallCache.missDataTtl();
String redisCacheKey = bloomPrefix + args[0] + bloomSuffix;
String intern = redisCacheKey.intern();
log.info("redisCacheKey:对象地址:", intern);
// 拿到我们要用的布隆过滤器
RBloomFilter<Object> bloomFilter = blooms.get(bloomName);
boolean contains = bloomFilter.contains(redisCacheKey);
if (!contains) {
// 布隆过滤器没有此值
return null;
}
try {
// 前置通知
// 1、先看缓存有没有
Object cache = getFromCache(redisCacheKey,method);
if (cache == null) {
// 如果缓存中没有,再执行目标方法
log.info("分布式缓存切面,准备加锁执行目标方法······");
// 对象锁,这里只能锁 sku:intern:info 单个,比如说 sku:51:info
synchronized (intern) {
// 按照并发,短时间,jvm 会缓存到元空间
// 第一个人 sku:51:info,常量池就有,100w直接涌进来都用这个字符串,同样的字符串内存地址一样
// sku:51:info 只有一个放行了
log.info("抢锁成功···正在双检查···");
Map<String, Object> reCache = getFromCache(redisCacheKey,method);
if (reCache == null) {
// 目标方法的执行,目标方法的异常切面不要吃掉
Object proceed = pjp.proceed(args);
saveToCache(redisCacheKey, proceed, ttl, missDataTtl);
return proceed;
}
return reCache;
}
}
// 返回通知
// 缓存有数据,直接返回
return cache;
} finally {
// 后置通知
log.info("分布式缓存切面,如果用的是分布式锁就要在这里解锁···");
}
}
/**
* 把数据保存到缓存
*
* @param redisCacheKey
* @param proceed
* @param ttl
*/
private void saveToCache(String redisCacheKey, Object proceed, long ttl, long missDataTtl) throws JsonProcessingException {
//可能会缓存空值
if (proceed != null) {
ObjectMapper objectMapper = new ObjectMapper();
String jsonStr = objectMapper.writeValueAsString(proceed);
// 较久的缓存
stringRedisTemplate.opsForValue().set(redisCacheKey, jsonStr, ttl, TimeUnit.MILLISECONDS);
} else {
// 较短的缓存
stringRedisTemplate.opsForValue().set(redisCacheKey, "miss", missDataTtl, TimeUnit.MILLISECONDS);
}
}
/**
* 把数据从缓存加载
* <p>
* 别把这个写死 Map<String, Object>
*
* @param cacheKey
* @return
*/
private Map<String, Object> getFromCache(String cacheKey,Method method) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
// 缓存是 JSON
String json = stringRedisTemplate.opsForValue().get(cacheKey);
if ("miss".equals(json)) {
return null;
}
if (!StringUtils.isEmpty(json)) {
Object readValue = objectMapper.readValue(json, method.getReturnType());
return readValue;
}
return null;
}
}
ObjectMapper 的 readValue 中可以指定把红框部分转换成什么样的类型
6、debug 第一次
① postman 第一次请求
这个时候的 redis 中无数据
② 后台
首先来到切面类
缓存中有就 step into 进入到 getFromCache 方法中获取数据
缓存中为 null,就 return null
getFromCache 走完后返回为 null,接着就开始加锁
双检查缓存,这时会除了 redisCacheKey 外,还传递了 method
// 使用目标方法的返回值类型,序列化和反序列化 redis 的数据
// 为了方便将当前方法全部信息往下传递
Method method = signature.getMethod();
双检查发现 cacheTemp 确实为 null,此时执行目标方法
目标方法处理完得到 proceed 值,然后把它保存到缓存中
step into 到 saveToCache 中
保存完后 redis 中就会有数据
7、debug 第二次
① postman 第一次请求
这个时候的 redis 中有 51 号数据
② 后台
前面都是一样的,判断缓存中有没有就不一样了,此时将 method 传到 getFromCache 中
step into 到 getFromCache 中
从缓存中查到了 JSON 字符串数据,判断是否为 miss
点击 objectMapper 的 readValue 方法进来
ObjectMapper 的 readValue 中可以指定把红框部分转换成什么样的类型,我们可以设置成目标方法的返回值作为转换的类型
此时反序列化的值类型为 LinkedHashMap,原因就是 method.getReturnType() 我们设置了类型为目标方法的返回值类型
我们来验证一下看看 method.getReturnType() 的类型到底是什么
计算得到
同理如果目标方法的返回类型是其他的,那么反序列化的类型和其是一致的