谷粒商城 Day08 布隆过滤器与缓存切面

Day08 布隆过滤器与缓存切面

一、答疑

使用HashMap本地缓存,是使用内存还是硬盘?如果使用内存,是不是程序重启了,缓存就丢失了?

image-20210924100529635

二、业务中缓存使用总结&问题&面试题

1、数据一致性问题

数据库修改以后,缓存的数据需要同步过来【缓存中的所有数据都应当有过期时间】

① 缓存的每个数据都必须有过期时间

允许有一段时间数据不一致,但是要保证数据最终要一致【最终一致性】

sku:info:50:后台改了50号数据,30min 分钟过期,数据过期以后,下一次对此数据的查询会触发更新逻辑,最终缓存中还是新数据

(1)双写模式

写数据库+写缓存;本来要同步缓存(改数据的同时给缓存存一份)

问题:为什么不先写缓存再写数据库呢?

答:因为写数据库的时候有可能失败,或者出现问题数据库回滚,回滚完后害得改缓存;也就是说数据库炸了缓存得改两遍,所以我们现在是先写数据库,而不是先写缓存,确定数据库写成功后,再写缓存

image-20210924105138346
(2)失效模式

写数据库+删缓存;下一次查询,由于缓存中没有,自己会查数据库,然后又更新到缓存

image-20210924105230677

运用到项目中(伪代码)

// 假设改完了数据

// 双写模式
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号记录放到缓存

image-20210925195541103
(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: (强一致)【所有的操作都会通过领导】

image-20210924144505882

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、布隆过滤器

image-20210912101138584

4、引入布隆过滤器

image-20210924153654961 image-20210924153817009

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

image-20210924154410962

mightContain

image-20210924154637068

4、布隆过滤器应该怎么用

不应该用本地布隆过滤器,而是应该用分布式布隆过滤器

image-20210924155034877

在 service-product 的大保存时,只要操作了数据库,赶紧告诉布隆:我插入了数据

image-20210924155535516

工具包里面也引入了redssion,所以我们在 service 包中就可以不用引入了

image-20210925095419151

5、项目初始化布隆&商品保存要告诉布隆

① 移 ItemServiceRedissonConfig

把 ItemServiceRedissonConfig 移到 service-util 的 com.atguigu.gmall.common.config 下

image-20210925095901624

原来 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 的配置

image-20210925102207328
③ 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);
    }
}

测试

image-20210925111035463
⑤ 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,

先获取布隆过滤器,注意,布隆过滤器在使用之前需要先初始化

image-20210925115401246
⑥ 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 的布隆

image-20210925145922484

user 的布隆

image-20210925150026108

接下来看如何防止缓存穿透攻击,首先需要在查询数据库之前设置好布隆过滤器

6、布隆过滤器的使用

① item:ItemServiceImpl
@Service
@Slf4j
public class ItemServiceImpl implements ItemService {

    @Qualifier(BloomName.SKU)
    @Autowired
    RBloomFilter<Object> skuFilter;
    
    // ······
}

这个方法要查 skuInfo,我们不用在这里写布隆过滤器,可以直接在 controller 里面写,让请求一来就直接先问布隆有没有

image-20210925150938523
② 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号数据,因为这个数据是在布隆过滤器之前数据库就有的,压根就没有告诉布隆过滤器,所以肯定查不到

image-20210925152657445

在商品后台管理中重新添加一个新的数据

image-20210925153005096

image-20210925153040410

查询53号商品,为什么51号商品查不到,53号查得到呢?因为添加51号商品时没有告诉布隆,添加53号告诉布隆了

image-20210925153116727
④ 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 中的数据

image-20210925162256641

启动微服务

image-20210925162427503

此时的 redis 中

image-20210925162457028

前端发请求给布隆过滤器中添加数据

image-20210925162537693

此时再查询

image-20210925162613125

7、布隆过滤器的总结

① 讨论
image-20210925163001638
② 使用逻辑
image-20210925163207035

三、分布式缓存切面+自定义缓存注解测试

通过规律我们可以发现,缓存任何值都是上面的那一套逻辑,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 的使用
image-20210925212044505 image-20210925212131848 image-20210925212312276

@annotation 看标注了哪一个注解

② @Around 的使用

image-20210925213035274

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("分布式缓存切面,后置通知···");
        }
    }
}
image-20211214100942323
// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuId
Object[] args = pjp.getArgs();
image-20211214101202682

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 {
    
}
⑤ 测试

启动微服务

image-20210925215528470

给切面打断点

image-20210925215748415

给目标方法打断点

image-20210925215919690

前端发请求

image-20210925215836535

目标方法还没执行,先来到切面,切面里面有个切入点表达式

targetClass:将要执行的类

image-20210925220238305

target:类的对象 + 类的所有属性

image-20210925220513332

方法

image-20210925220640464

Object[] args = pjp.getArgs();拿到目标方法要用的所有参数

image-20210925221144281

目标方法还没执行,切面就先给它拦住了,先执行切面的前置通知

image-20210925221915952

放行后,来到下一个断点,要查数据库了

image-20210925222123587

继续放行

image-20210925222239128

测试完成,我们可以将响应的代码写到各个通知的位置了

如果用 postman 发请求的话

image-20210925222502627

四、分布式缓存切面+自定义缓存注解

1、第一版

① GmallCache

我们先给自定义缓存注解加一些元注解信息,可以抄已有的注解,例如:Transactional 注解

image-20210925223258118

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 写上

image-20211214104219103

② 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 包下的)

image-20210925225906461 image-20210925225958266 image-20210925230016475 image-20210925230054463 image-20210925230117451
(2)获取当前方法标注的注解

前端发送请求

image-20210926120847806

后端

GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);
image-20210925225423423 image-20210925225341106

继续完善

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 发送请求

image-20210926123139123

后端

计算 bloomPrefix+ args[0] +bloomSuffix

image-20210926123216902 image-20210926123250066 image-20210926123307702

计算 skufilter.contains(bloomPrefix+ args[0] +bloomSuffix);

image-20210926123430752 image-20210926123357375 image-20210926123531844

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;
    }
}

image-20211214153910268

(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,数据都清空了,只有布隆的占位和配置信息

image-20210926135055354

postman 发送请求

image-20210926134416827

后端

拿到方法的详情信息、注解的详细信息,根据 redisCacheKey 判断布隆过滤器中有没有

image-20210926134631852

布隆过滤器中有,先看缓存中有没有,step into 到 getFromCache 查询

image-20210926134843145

缓存中没有,getFromCache 返回 null

image-20210926135223483

缓存中没有,准备加锁执行目标方法查询数据库

image-20210926140036439

目标方法远程查询出数据

image-20210926140242299

目标方法执行完,把数据给缓存中存一份

image-20210926140348577

image-20210926140435384

查看 redis 缓存

image-20210926140504802

然后 controller 返回

image-20210926140608377

⑤ 测试是否锁住了

压力测试 synchronized (redisCacheKey.intern())

image-20210926162702543

多次测试发现 String redisCacheKey = bloomPrefix + args[0] + bloomSuffix; redisCacheKey 的值在不断变化

String intern = redisCacheKey.intern(); intern 的值还是 10009

压力测试

image-20210926163819509

只查了一遍数据库

image-20210926164142509

4、第四版

① 分析

现在有个问题,sku 用的是 sku 的布隆过滤器,spu 应该有 spu 的布隆过滤器,XXX 应该有 XXX 的布隆过滤器,未来我们的业务会很多,都应该有自己的布隆过滤器,这样每个布隆过滤器都很小,效率很高

来到切面我们可以看到,一进来用的是 sku 的布隆,我们要怎么才能自己用自己的呢?

image-20210926170712127
② 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
image-20211214184533675 image-20211214184623343
	/**
     * 提前准备的布隆过滤器
     * @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

我们之前的布隆过滤器用的是这个,但是现在我们要让他动态变化

image-20211214190706516

改进后:

@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)
image-20210926173631919
⑦ SkuInfoServiceImpl

把之前用的 skuiflter 改成 BloomName.SKU

@Qualifier(BloomName.SKU)
image-20210926173841262
⑧ SkuInfoController

把之前用的 skuiflter 改成 BloomName.SKU

    @Qualifier(BloomName.SKU)
    @Autowired
    RBloomFilter<Object> skuFilter;
image-20211214192931397
⑨ 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;
    }
image-20210926174131375
⑩ 测试

postman 发送请求

image-20210926174342950

先看这个 map,key 是布隆过滤器的名字,值是对象,以后想用哪个用哪个

image-20210926174507087 image-20210926174443687
XI 未解决的问题

saveToCache 可能会缓存空值

image-20210926175218926

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类型的数据,直接写死了,如果缓存中有的数据它就是一个对象,这个时候就不合适了,从缓存中返回到的应该是目标方法的返回值类型

image-20211214200620270

	// ···
	// 前置通知
            // 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 中可以指定把红框部分转换成什么样的类型

image-20210928171730957

6、debug 第一次

① postman 第一次请求

这个时候的 redis 中无数据

image-20211214204952233

② 后台

首先来到切面类

image-20211214212535619

image-20211214212704268

image-20211214212741729

image-20211214212842138

缓存中有就 step into 进入到 getFromCache 方法中获取数据

image-20211214213018887

缓存中为 null,就 return null

image-20211214213414659

getFromCache 走完后返回为 null,接着就开始加锁

image-20211214213705880

双检查缓存,这时会除了 redisCacheKey 外,还传递了 method

// 使用目标方法的返回值类型,序列化和反序列化 redis 的数据
// 为了方便将当前方法全部信息往下传递
Method method = signature.getMethod();

image-20211214213909670

双检查发现 cacheTemp 确实为 null,此时执行目标方法

image-20211214214252123

目标方法处理完得到 proceed 值,然后把它保存到缓存中

image-20211214214352308

step into 到 saveToCache 中

image-20211214214539114

保存完后 redis 中就会有数据

image-20211214214634124

7、debug 第二次

① postman 第一次请求

这个时候的 redis 中有 51 号数据

image-20211214204952233

② 后台

前面都是一样的,判断缓存中有没有就不一样了,此时将 method 传到 getFromCache 中

image-20211214215734113

step into 到 getFromCache 中

从缓存中查到了 JSON 字符串数据,判断是否为 miss

image-20211214215847015

点击 objectMapper 的 readValue 方法进来

ObjectMapper 的 readValue 中可以指定把红框部分转换成什么样的类型,我们可以设置成目标方法的返回值作为转换的类型

image-20210928171730957

此时反序列化的值类型为 LinkedHashMap,原因就是 method.getReturnType() 我们设置了类型为目标方法的返回值类型

image-20211214221323117

我们来验证一下看看 method.getReturnType() 的类型到底是什么

image-20211214221621372

计算得到

image-20211214221659345

同理如果目标方法的返回类型是其他的,那么反序列化的类型和其是一致的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值