Day09 首页分类与SpEL动态缓存切面

一、优化缓存逻辑

百万并发进来,判断 bloomFilter 和缓存中拿,先执行哪个最好?1. 先布隆 ,再缓存     面对攻击 1 好
2. 先缓存 ,再布隆     正常业务 2 好  99%数据可能都在缓存有  99w + 99w ~= 200w缓存永远都得看,布隆少判断一次就节省很多时间所以最后我们商品详情的流程是这样的,用户查询查某个skuId,先判断缓存中有没有,这里解释一下为什么要先判断缓存,在正常业务时,先缓存再布隆好,因为99%的数据缓存中都有,百万并发过来,99w都在缓存中有,如果先判断布隆,要连接redis 99w,查redis又是99w,相当于一个正常业务要连接redis 99w,缓存永远都得看,不看不行,先判断redis,有就直接返回了,布隆少判断一次就节省了很多时间,没有才会再次查布隆;而在面对攻击的时候,先布隆再缓存好,但是并非每天都在受到攻击,考虑到实际情况和效率所以在查询布隆过滤器之前要先查一次缓存,双检查

1、GmallCacheAspect

@Slf4j
@Aspect
@Component
public class GmallCacheAspect {@Qualifier(BloomName.SKU)@AutowiredRBloomFilter<Object> skufilter;/*** Spring 会自动从容器中拿到所有 RBloomFilter 类型的组件* 给我们封装好map,map的key用的就是组件的名,值就是组件对象*/@AutowiredMap<String, RBloomFilter<Object>> blooms;@AutowiredStringRedisTemplate stringRedisTemplate;@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuIdObject[] 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);//百万并发进来????//1、判断bloomFilter 和 缓存中拿  先执行哪个最好。 缓存永远都得看,布隆少判断一次就节省很多时间//1.1)、先布隆 ,再缓存     面对攻击 1 好//1.2)、先缓存 ,再布隆     正常业务 2 好  99%数据可能都在缓存有  99w + 99w ~= 200w//优化后逻辑,先看缓存是否存在//1、先看缓存有没有Object cache = getFromCache(redisCachekey, method);if (cache != null) {// 缓存中有直接返回return cache;}//2、缓存中如果没有,准备查库要问布隆try {//使用布隆boolean contains = bloomFilter.contains(redisCachekey);//布隆查redis,比缓存快很多...if (!contains) {return null;}//如果缓存中没有log.info("分布式缓存切面,准备加锁执行目标方法......");
//                synchronized (this){//                      this代表的是这个切面类,切面全系统就一个,当缓存用户和缓存图书时,它们用的都是一个切面类,就会出问题
//                      如果是this,例如查询51,sku:51:info,百万并发进来都查询51号,52号,它都能锁住
//
//                }//TODO synchronized() //八锁 对象锁 sku:50:info能锁住,sku:51:info就锁不住synchronized (intern) { //最终结果就是ok的,同样的sku都是同意把锁//字符串是常量池的,每一个人进来都有它唯一的字符串,例如查询50,sku:50:info,百万并发进来都查询50号,50号就锁住了//字符串在常量池就一个地址,相当于同一个商品用了一把锁//看这个锁能不能锁住 sku:50:info 只有一个放行了,按照并发,短时间,jvm会缓存到元空间//第一个人sku:50:info,常量池就有,100w直接涌进来都用这个字符串,同样的字符串内存地址一样//在这个应用只有一个副本的情况下,查数据库最多一次//在这个应用有N个副本的情况下,查数据库最多N次log.info("强锁成功......正在双检查");Object cacheTemp = getFromCache(redisCachekey, method);if (cacheTemp == null) {//前置通知//目标方法的返回值不确定Object proceed = pjp.proceed(args); //目标方法的执行,目标方法的异常切面不要吃掉saveToCache(redisCachekey, proceed, ttl, missDataTtl);return proceed;}return cacheTemp;}} catch (Exception e) {log.error("缓存切面失败:{}", e);} finally {//后置通知log.info("分布式缓存切面,如果分布式锁要在这里解锁......");}return cache;}/*** 把数据保存到缓存** @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();// 缓存是 JSONString 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;}
}

二、异步编排 CompletableFuture

1、介绍

查询商品详情页的逻辑非常复杂,数据的获取都需要远程调用,必然需要花费更多的时间,假如商品详情页的每个查询,需要如下标注的时间才能完成

1. 获取sku的基本信息        1.5s
2. 获取sku的图片信息       0.5s
3. 获取spu的所有销售属性    1s
4. sku价格                0.5s...

那么,用户需要3.5s后才能看到商品详情页的内容。很显然是不能接受的;但是如果有多个线程同时完成这4步操作,也许只需要1.5s即可完成响应。

异步任务既能异步,还有按序编排1、查Sku图片,基本信息    0.2s  五个人都异步来做
2、查Sku的目录           0.2s
3、查Sku的销售属性       0.2s
4、查Sku的价格           0.2s
5、查Sku属性组合信息     0.2s串行执行:执行完需要  1s; 影响吞吐量1s 能接  共计 500 请求tomcat线程池最大500;同步:  1个请求阻塞1s, 500请求同时进来。 500把tomcat塞满。 并发就是 500异步:  1个请求阻塞0.2s, 500请求同时进来, 500只能把tomcat占用0.2s, 并发就是2500  吞吐量异步==快?   异步提升吞吐量的  总时间是一样的这种情景下就不能同时异步,必须等订单详情干完才能查物流信息...1、查出订单详情。订单详情中有物流单号2、根据物流单号,查询物流信息3、根据物流单号,查询仓库信息4、根据物流单号,查询xxx信息

以前:实现Runnable、继承Thread、还有 Callable 接口

现在:运行异步任务的两种方法;快速的给系统提交一个异步任务

2、runAsync()

快速启动一个异步任务,我们不关心异步任务的返回值

发现 Runnable 是一个函数式接口

复习:什么是函数式接口?
没有参数、没有返回值@FunctionalInterface
public interface Runnable {public abstract void run();
}函数式接口就可以使用 lambada 表达式了

测试

public static void main(String[] args) {CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {//箭头函数System.out.println("张三宝.....");System.out.println("6666");});
}

执行:发现什么都没有打印,为什么会这样呢?

因为 CompletableFuture future 这个异步任务刚提交还没执行,主方法已经结束了,整个程序就停机了,所以异步任务根本就没有执行

解决:

public static void main(String[] args) {CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {//箭头函数System.out.println("张三宝.....");System.out.println("6666");});Thread.sleep(300000000);
}

runAsync() 是没有返回值的,如果需要返回值就得用 supplyAsync()

3、supplyAsync()

快速启动一个异步任务,我们关心异步任务的返回值

//异步任务什么时候执行,CPU心情好的时候
CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {int i = 10 + 11;try {Thread.sleep(30000);} catch (InterruptedException e) {e.printStackTrace();}return i;
});System.out.println("xxxxxx");// integerCompletableFuture.get() 如果异步任务没完,自己等待
System.out.println("直接计算的结果是什么...." + integerCompletableFuture.get());
System.out.println("yyyyyyyyy");

4、线程池

异步任务提交给哪里了?

业务有自己的线程池,异步任务提交给我们自己的线程池

Executors.newFixedThreadPool(5)

public static void main(String[] args) {//1、创建一个线程池,当前线程池同时处理最多5个  core=5  max=5ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);/*** Executors.newFixedThreadPool(5) 对应的线程池7大参数值如下:* * int corePoolSize,    核心线程      5* int maximumPoolSize, 最大线程      5* long keepAliveTime,  存活时间      0L* TimeUnit unit,       时间单位      ms* BlockingQueue<Runnable> workQueue,  线程队列   new LinkedBlockingQueue<Runnable>() 无界的* ThreadFactory threadFactory,    线程的创建工厂    默认* RejectedExecutionHandler handler  拒绝策略     默认** Executors.newFixedThreadPool(5) == ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(以上异一堆);* 提交了7个任务* 线程池5个线程准备接任务,5个被立即执行,剩下两个进入队列*    1、如果我们弹性了 max。max就开始扩线程,max扩出来的线程自己去队列里面拿任务执行*    2、如果max还是core,max扩不了线程。剩下两个等前面5个执行完就执行*/CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {}, executorService);CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {return 1;}, executorService);
}

5、whenCompleteAsync 异步回调

异步回调(异步任务成功或者异常以后的回调)

whenCompleteAsync:就会开一个新的线程

// 链式的方式,任务结束自动处理一件事情CompletableFuture.supplyAsync(() -> {   //给线程池提交任务System.out.println("正在计算 10 / 0:"+Thread.currentThread().getId());int i = 10 / 0;return i;}, executorService).whenComplete((result,exception)->{  //任务执行完成,以后由主线程执行完成后的处理System.out.println("异步任务成功:Thread.currentThread().getId() = " + Thread.currentThread().getId());System.out.println("上次计算结果result = " + result+",保存到了数据库");System.out.println("上次异常exception = " + exception);});

① whenComplete

public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(3);System.out.println("主线程:Thread.currentThread().getId() = " + Thread.currentThread().getId());//链式的方式,任务结束自动处理一件事情CompletableFuture.supplyAsync(() -> {   //给线程池提交任务System.out.println("正在计算 10 / 0:"+Thread.currentThread().getId());int i = 10 / 0;return i;}, executorService).whenComplete((result,exception)->{  //任务执行完成,以后由主线程执行完成后的处理System.out.println("异步任务成功:Thread.currentThread().getId() = " + Thread.currentThread().getId());System.out.println("上次计算结果result = " + result+",保存到了数据库");System.out.println("上次异常exception = " + exception);});
}

② whenCompleteAsync

public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(3);CompletableFuture.supplyAsync(() -> {   //给线程池提交任务System.out.println("正在计算 10 / 0:"+Thread.currentThread().getId());int i = 10 / 0;return i;
}, executorService).whenCompleteAsync((result,exception)->{  //任务执行完成,线程池执行他们用不同线程?System.out.println("Thread.currentThread().getId() = " + Thread.currentThread().getId());System.out.println("上次计算结果result = " + result+",保存到了数据库");System.out.println("上次异常exception = " + exception);},executorService);
}

6、exceptionally 异常回调

当没有使用 exceptionally 时

使用 exceptionally

public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(3);CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {int i = 10 / 0;return i;}).whenComplete((res,exp)->{System.out.println("res = " + res);System.out.println("exp = " + exp);}).exceptionally((throwable)->{//异常回调。System.out.println("throwable = " + throwable);return 8;  //兜底数据});Integer integer = future.get();System.out.println("我用的返回值是:"+integer);
}

7、thenRun

thenXXXX:异步任务运行成功,接下来要做的事情

thenRun

thenRunAsync

/*** thenXXXX:异步任务运行成功,接下来要做的事情* 1、future.thenAccept()* 2、future.thenApply()* 3、future.thenRun()  thenRunAsync*/
public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(5);System.out.println("主线程 = " + Thread.currentThread().getId());//1、计算:循环相加  Vue  Promise  ajax().then().thenCompletableFuture.supplyAsync(()->{int i = 10/2;System.out.println("i = " + i);System.out.println("异步任务线程 = " + Thread.currentThread().getId());return i;},executorService).thenRunAsync(()->{  //Runable 既不能把别人的值拿来,又不能自己返回System.out.println("then1线程 = " + Thread.currentThread().getId());System.out.println("张三6666");try {Thread.sleep(3000);System.out.println("张三6666--打印完成");} catch (InterruptedException e) {e.printStackTrace();}},executorService).thenRunAsync(()->{System.out.println("then2线程 = " + Thread.currentThread().getId());System.out.println("7777");},executorService);
}

8、thenAccept

thenAccept

thenAccept 是一个消费者接口,只有一个泛型,没有返回值

thenAccept 有一个入参,没有返回值

9、thenApply

函数式接口,有两个泛型,T 为入参,R 为出参

/*** future.thenApply*/
public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(5);CompletableFuture<Integer> integerCompletableFuture = CompletableFuture.supplyAsync(() -> {int i = 10 / 3;return i;}, executorService);CompletableFuture<Integer> integerCompletableFuture1 = integerCompletableFuture.thenApply((res) -> {int i = res + 5;System.out.println("i = " + i);return i;  //}).thenApply((res) -> {int x = res + 10;System.out.println("x = " + x);return x;}).whenComplete((res, excp) -> {System.out.println("res = " + res);System.out.println(excp);});Integer integer = integerCompletableFuture.get();System.out.println("integer = " + integer);Integer integer1 = integerCompletableFuture1.get();System.out.println("integer1 = " + integer1);}

10、allOf

全部完成

情况一:异步任务还未走完就打印

public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(5);CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("查sku图片......");}, executorService);CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("查sku属性......");}, executorService);CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("查sku组合......");}, executorService);CompletableFuture<Void> future3 = CompletableFuture.allOf(future, future1, future2);System.out.println("6666");
}

情况二:异步任务全部走完再打印

public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(5);CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("查sku图片......");}, executorService);CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {try {Thread.sleep(10000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("查sku属性......");}, executorService);CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("查sku组合......");}, executorService);CompletableFuture<Void> future3 = CompletableFuture.allOf(future, future1, future2);future3.get();System.out.println("6666");
}

11、anyOf

顾名思义,任意一个做完即可

三、自己创建业务线程池

每一个微服务都应该有它自己的线程池

1、第一版

① ItemConfig

com.atguigu.gmall.item.config.ItemConfig

/*** 工具类提取的所有自动配置类我们使用 @Import 即可* 导入自己的 GmallCacheAspect.class 分布式缓存切面*/
@Import({ItemServiceRedissonConfig.class, GmallCacheAspect.class})
//让 Spring Boot自己的RedisAutoConfiguration 配置完以后再启动我们的
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableAspectJAutoProxy  //开启切面功能
@Configuration
public class ItemConfig {//配置自己的业务线程池 核心业务线程池@Beanpublic ThreadPoolExecutor executor() {return new ThreadPoolExecutor(16,32,1, //线程池 1min了都没有活要干了TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(10000),new MyItemServiceThreadFactory(),new ThreadPoolExecutor.AbortPolicy());}
}

② MyItemServiceThreadFactory

com.atguigu.gmall.item.config.MyItemServiceThreadFactory

我们建一个自己的线程工厂

public class MyItemServiceThreadFactory implements ThreadFactory {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName(UUID.randomUUID().toString().substring(0,5));return thread;}
}

思考:我们自己的线程池中参数不应该写死,而是可调控的

因此我们可以在配置文件中对线程池进行配置

2、第二版

① application.yaml

service/service-item/src/main/resources/application.yaml

item-service:  # 配置自己的业务线程池thread:core: 2max:  5keepalive: 60000queue-length: 10
server:port: 9000tomcat:accept-count: 10000  # tomcat线程池的队列长度  ServerProperties.Tomcatthreads:max: 5000
# IO密集型【一般都是这种】:  disk、network  淘宝  调 内存,线程池的大小
# CPU密集型【人工智能】: 计算;   内存占用不多, 线程池大小, 关注cpu#怎么抽取全微服务都能用
spring:main:allow-bean-definition-overriding: true  #允许bean定义信息的重写zipkin:base-url: http://192.168.200.188:9411/sender:type: webredis:host: 192.168.200.188port: 6379password: yangfan
#底层我们配置好了redisson,redisson功能的快速开关
#redisson:
#  enable: false  #关闭我们自己配置的redissonitem-service:  # 配置自己的业务线程池thread:core: 2max:  5keepalive: 60000queue-length: 10logging:level:com:atguigu:gmall: info

② ItemConfig

这样写会比较麻烦

改进:

把 MyItemServiceThreadFactory 删掉换成 ItemConfig 的内部类

/*** 工具类提取的所有自动配置类我们使用 @Import 即可* 导入自己的 GmallCacheAspect.class 分布式缓存切面*/
@Import({ItemServiceRedissonConfig.class, GmallCacheAspect.class})
//让 Spring Boot自己的RedisAutoConfiguration 配置完以后再启动我们的
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableAspectJAutoProxy  //开启切面功能
@Configuration
public class ItemConfig {/*** int corePoolSize,      16* int maximumPoolSize,   32* long keepAliveTime,* TimeUnit unit,* BlockingQueue<Runnable> workQueue,    50* ThreadFactory threadFactory,* RejectedExecutionHandler handler* <p>* 150个线程进来* 1、先立即运行 16个* 2、接下来剩下任务进入队列  50 个 (被人拿了16以后再进16)* 3、拿出16个,达到运行峰值 32 个* 4、状态。32个再运行,50个队列等待。最终只有 82个线程被安排了。* 5、150-82= 68个 要被RejectedExecutionHandler抛弃** @return*///配置自己的业务线程池 核心业务线程池@Bean("corePool")public ThreadPoolExecutor executor(ThreadConfigProperties properties) {return new ThreadPoolExecutor(properties.core,properties.max,properties.keepalive, //线程池 1min了都没有活要干了TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(properties.queueLength),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());}@Component@ConfigurationProperties(prefix = "item-service.thread")@Dataclass ThreadConfigProperties {private Integer core;private Integer max;private Long keepalive;private Integer queueLength;}class MyItemServiceThreadFactory implements ThreadFactory {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("item-service: " + UUID.randomUUID().toString().substring(0, 5));return thread;}}}

③ 启动测试

四、使用异步编排改写代码

1、ItemServiceImpl

com.atguigu.gmall.item.service.impl.ItemServiceImpl

以前的 getFromServiceItemFeign,其实就是 getFromServiceItemFeign01版,因为是同步,所以速度比较慢

改造后:

@Service
@Slf4j
public class ItemServiceImpl implements ItemService {@AutowiredSkuInfoFeignClient skuInfoFeignClient;@AutowiredStringRedisTemplate stringRedisTemplate;@AutowiredRedissonClient redissonClient;@Qualifier(BloomName.SKU)@AutowiredRBloomFilter<Object> skuFilter;@Qualifier("corePool") // 指定用 corePool 线程池@AutowiredThreadPoolExecutor executor;@GmallCache(bloomPrefix = RedisConst.SKUKEY_PREFIX,bloomSuffix = RedisConst.SKUKEY_SUFFIX,ttl = 1000 * 60 * 30,missDataTtl = 1000 * 60 * 10) //需要一个切面类,动态切入所有标了这个注解的方法@Overridepublic Map<String, Object> getSkuInfo(Long skuId) {//查数据log.info("要查数据库了....");HashMap<String, Object> map = getFromServiceItemFeign(skuId);return map;}/***  业务线程池控制住所有并发,防止无限消耗* 我以下的写法,*  对比  new Thread(()->{}).start(); 优点:* 1、线程重复使用。 16~32直接重复使用,少了开线程时间  new Thread  0.01* 2、线程吞吐量的限制。*  以前 1个请求 5个线程,   100请求,new 500个线程   1w个请求  5w线程等待CPU切换(内存占用更大)*  现在 1个请求 5个线程,交给线程池,线程池只有32个一直执行。控制资源*      100 请求 500个线程,交给线程池, 之前32个线程等待CPU切换,  468 就在队列等待执行*      1w 请求  1w个线程,交给线程池,之前32个线程等待CPU切换,  9968 个在队列(占内存)* @param skuId* @return*/private HashMap<String, Object> getFromServiceItemFeign(Long skuId) {HashMap<String, Object> result = new HashMap<>();//1、查询sku详情 2,Sku图片信息CompletableFuture<SkuInfo> future = CompletableFuture.supplyAsync(() -> {SkuInfo skuInfo = skuInfoFeignClient.getSkuInfo(skuId);result.put("skuInfo", skuInfo);return skuInfo;}, executor);//3、Sku分类信息// res 是 future 返回的结果,即 skuInfoCompletableFuture<Void> future1 = future.thenAcceptAsync((res) -> { if (res != null) {BaseCategoryView skuCategorys = skuInfoFeignClient.getCategoryView(res.getCategory3Id());result.put("categoryView", skuCategorys);}});//4,销售属性相关信息CompletableFuture<Void> future2 = future.thenAcceptAsync((res) -> {if (res != null) {List<SpuSaleAttr> spuSaleAttrListCheckBySku = skuInfoFeignClient.getSpuSaleAttrListCheckBySku(skuId, res.getSpuId());result.put("spuSaleAttrList", spuSaleAttrListCheckBySku);}}, executor);//5,Sku价格信息CompletableFuture<Void> future3 = future.thenAcceptAsync((res) -> {if (res != null) {BigDecimal skuPrice = skuInfoFeignClient.getSkuPrice(skuId);result.put("price", skuPrice);}}, executor);//6,Spu下面的所有存在的sku组合信息CompletableFuture<Void> future4 = future.thenAcceptAsync((res) -> {if (res != null) {Map map = skuInfoFeignClient.getSkuValueIdsMap(res.getSpuId());ObjectMapper mapper = new ObjectMapper();try {String jsonStr = mapper.writeValueAsString(map);log.info("valuesSkuJson 内容:{}", jsonStr);result.put("valuesSkuJson", jsonStr);} catch (JsonProcessingException e) {log.error("商品sku组合数据转换异常:{}", e);}}}, executor);CompletableFuture<Void> allOf = CompletableFuture.allOf(future, future1, future2, future3, future4);try {allOf.get();} catch (Exception e) {log.error("线程池异常:{}", e);}return result;}}

注意点:

2、ItemConfig

com.atguigu.gmall.item.config.ItemConfig

@Bean("corePool")@Qualifier("corePool") // 指定用哪一个线程池
@Autowired
ThreadPoolExecutor executor;
/*** 工具类提取的所有自动配置类我们使用 @Import 即可* 导入自己的 GmallCacheAspect.class 分布式缓存切面*/
@Import({ItemServiceRedissonConfig.class, GmallCacheAspect.class})
//让 Spring Boot自己的RedisAutoConfiguration 配置完以后再启动我们的
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableAspectJAutoProxy  //开启切面功能
@Configuration
public class ItemConfig {/*** int corePoolSize,      16* int maximumPoolSize,   32* long keepAliveTime,* TimeUnit unit,* BlockingQueue<Runnable> workQueue,    50* ThreadFactory threadFactory,* RejectedExecutionHandler handler* <p>* 150个线程进来* 1、先立即运行 16个* 2、接下来剩下任务进入队列  50 个 (被人拿了16以后再进16)* 3、拿出16个,达到运行峰值 32 个* 4、状态。32个再运行,50个队列等待。最终只有 82个线程被安排了。* 5、150-82= 68个 要被RejectedExecutionHandler抛弃** @return*///配置自己的业务线程池 核心业务线程池@Bean("corePool")public ThreadPoolExecutor executor(ThreadConfigProperties properties) {return new ThreadPoolExecutor(properties.core,properties.max,properties.keepalive, //线程池 1min了都没有活要干了TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(properties.queueLength),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());}@Component@ConfigurationProperties(prefix = "item-service.thread")@Dataclass ThreadConfigProperties {private Integer core;private Integer max;private Long keepalive;private Integer queueLength;}class MyItemServiceThreadFactory implements ThreadFactory {@Overridepublic Thread newThread(Runnable r) {Thread thread = new Thread(r);thread.setName("item-service: " + UUID.randomUUID().toString().substring(0, 5));return thread;}}
}

五、首页商品分类实现

我们之前是查询某一个 sku 的详情

现在我们要查询首页商品的详情,老规矩先搭环境

1、IndexController(测试)

新建 com.atguigu.gmall.web.all.controller.IndexController

我们现在希望能通过域名直接访问首页

所以接下来需要我们配置一下主机的 host 地址

2、配置主机的 host 地址

直接访问域名就是本机ip:80,而本机ip:80就是访问的本机网关,我们现在要的效果是访问域名直接来到尚品汇的首页

接下来就要在网关里面配置,所有的域名都要往对应的地址转

3、api-gateway 网关配置

application.yaml

之前的:

配置后:

效果出来了,但是页面左侧的三级菜单还需要远程请求才能出来

通过 feign 远程查询

4、文档&分析

数据结构如下:json 数据结构

注意:index 1 代表第一个,index 2 代表第二个,不是 id

一级菜单 “categoryId”: 1

[{"index": 1,"categoryChild": [{"categoryChild": [{"categoryName": "电子书", # 三级分类的name"categoryId": 1},{"categoryName": "网络原创", # 三级分类的name"categoryId": 2},...],"categoryName": "电子书刊", #二级分类的name"categoryId": 1},...],"categoryName": "图书、音像、电子书刊", # 一级分类的name"categoryId": 1},...
"index": 2,"categoryChild": [{"categoryChild": [{"categoryName": "超薄电视", # 三级分类的name"categoryId": 1},{"categoryName": "全面屏电视", # 三级分类的name"categoryId": 2},...],"categoryName": "电视", #二级分类的name"categoryId": 1},...],"categoryName": "家用电器", # 一级分类的name"categoryId": 2}
]

分析:

index 只在一级菜单的时候要

5、IndexCategoryVo

新建 com.atguigu.gmall.model.vo.IndexCategoryVo

推荐如果数据有自己特定的返回特性,我们就在 model 中放一份对应的 VO

/*** 对应数据库表 专门的 POJO;JavaBean,Entity;* <p>* 页面 :数据库的个别字段的组合;* 写接口的时候,自己建立vo,把数据库查出来的封装成前端喜欢的vo* <p>* <p>* 级联封装的vo*/
@Data
public class IndexCategoryVo {private Integer index;private String categoryName;private Integer categoryId;private List<IndexCategoryVo> categoryChild;
}

6、service-product

① CategoryAdminController

com.atguigu.gmall.product.controller.CategoryAdminController

    @GetMapping("/getAllCategorys")public List<IndexCategoryVo> getAllCategorysForIndexHtml(){return categoryService.getAllCategorysForIndexHtml();}
/*** 对接后台管理系统,/admin/product相关请求*/
@RequestMapping("/admin/product")
@RestController
public class CategoryAdminController {@AutowiredCategoryService categoryService;@GetMapping("/getAllCategorys")public List<IndexCategoryVo> getAllCategorysForIndexHtml(){return categoryService.getAllCategorysForIndexHtml();}/*** 获取一级分类信息* @return*/@GetMapping("/getCategory1")public Result<List<BaseCategory1>> getCategory1(){List<BaseCategory1> category1s = categoryService.getCategory1();return Result.ok(category1s);}/*** 获取二级分类信息* @param category1Id* @return*/@GetMapping("/getCategory2/{category1Id}")public Result<List<BaseCategory2>> getCategory2(@PathVariable("category1Id") Long category1Id){if(category1Id > 0){List<BaseCategory2> category2s = categoryService.getCategory2(category1Id);return Result.ok(category2s);}else {return Result.build(null, ResultCodeEnum.SECKILL_ILLEGAL);}}/*** 获取三级分类信息* @param category2Id* @return*/@GetMapping("/getCategory3/{category2Id}")public Result<List<BaseCategory3>> getCategory3(@PathVariable("category2Id") Long category2Id){if(category2Id > 0){List<BaseCategory3> category3s = categoryService.getCategory3(category2Id);return Result.ok(category3s);}else {return Result.build(null, ResultCodeEnum.SECKILL_ILLEGAL);}}
}

② CategoryService

List<IndexCategoryVo> getAllCategorysForIndexHtml();

③ CategoryServiceImpl

com.atguigu.gmall.product.service.impl.CategoryServiceImpl

@Override
public List<IndexCategoryVo> getAllCategorysForIndexHtml() {return baseCategory3Mapper.getAllCategorysForIndexHtml();
}

④ BaseCategory3Mapper

/*** 操作三级菜单的Mapper*/
public interface BaseCategory3Mapper extends BaseMapper<BaseCategory3> {List<IndexCategoryVo> getAllCategorysForIndexHtml();
}

⑤ BaseCategory3Mapper.xml(方式一)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.gmall.product.mapper.BaseCategory3Mapper"><resultMap id="categoryForIndex" type="com.atguigu.gmall.model.vo.IndexCategoryVo"><result property="categoryId" column="b1_id"></result><result property="categoryName" column="b1_name"></result><collection property="categoryChild" ofType="com.atguigu.gmall.model.vo.IndexCategoryVo"><result property="categoryId" column="b2_id"></result><result property="categoryName" column="b2_name"></result><collection property="categoryChild" ofType="com.atguigu.gmall.model.vo.IndexCategoryVo"><result property="categoryId" column="b3_id"></result><result property="categoryName" column="b3_name"></result></collection></collection></resultMap><select id="getAllCategorysForIndexHtml" resultMap="categoryForIndex">select b1.id   b1_id,b1.name b1_name,b2.id   b2_id,b2.name b2_name,b3.id   b3_id,b3.name b3_namefrom base_category1 b1left join base_category2 b2 on b2.category1_id = b1.idleft join base_category3 b3 on b3.category2_id = b2.idorder by b1.id, b2.id, b3.id</select>
</mapper>

⑥ MapperTest 测试【方式一】

新建测试类 com.atguigu.gmall.product.MapperTest

@SpringBootTest
public class MapperTest {@AutowiredBaseCategory3Mapper baseCategory3Mapper;@Testvoid test01(){List<IndexCategoryVo> allCategorysForIndexHtml =baseCategory3Mapper.getAllCategorysForIndexHtml();System.out.println("======");}
}

debug 运行

17个一级分类

一级分类展开后还有12个二级分类

二级分类展开后还有3个三级分类

找到了我们想要的数据,但是如果有四层、五层、六层······那就很麻烦了,所以我们可以使用方式二

⑦ BaseCategory3Mapper.xml(方式二 ×)

数据库表不支持递归封装,方式二失败

先参考mybatis官方文档 https://mybatis.net.cn/sqlmap-xml.html#select

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.gmall.product.mapper.BaseCategory3Mapper"><!--    递归版Sql的写法id  name parent_id     --><resultMap id="categoryForIndex" type="com.atguigu.gmall.model.vo.IndexCategoryVo"><result property="categoryId" column="id"></result><result property="categoryName" column="name"></result><collection property="categoryChild" select="getChildren" column="{id=id}"></collection></resultMap><!--    1、先调用 getAllCategorysForIndexHtml 查出  base_category1 里面的所有2、对照 resultMapfor(查出的记录){new IndexCategoryVo();  //封装第一个的categoryChild,又会调用 getChildren ; 18for(上次的18条){new IndexCategoryVo(); //封装到 categoryChild,又去调用 getChildren; 18; 查出了10条for(上次的10条){new IndexCategoryVo();  // //封装到 categoryChild, 又去调用 getChildren;100,查出了0条。直到封装结束}}}--><select id="getAllCategorysForIndexHtml" resultMap="categoryForIndex">select id, namefrom base_category1</select><!--        ${}拼串,用在各个位置   #{}是占位符,只能用在参数位置占位符只能用在 key=value 这种情况,像order by就不能使用#{}只能用${} --><select id="getChildren" resultMap="categoryForIndex">select id, namefrom base_category2where category1_id = #{id}</select>
</mapper>

发现三级分类是错误的

⑧ MySQL 递归 SQL 案例

视频:Day09:17、MyBatis递归SQL的案例

(1)数据表

(2)TblPersonMapper
package com.atguigu.gmall.product.mapper;import com.atguigu.gmall.model.product.TblPerson;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;import java.util.List;public interface TblPersonMapper extends BaseMapper<TblPerson> {List<TblPerson>  getAllPerson();
}

(3)TblPerson
package com.atguigu.gmall.model.product;import com.atguigu.gmall.model.base.BaseEntity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;import java.util.List;@Data
@TableName("tbl_person")
public class TblPerson extends BaseEntity {@TableField("name")private String name;@TableField("gender")private String gender;@TableField(exist = false)private List<TblPerson> childrens;
}

(4)TblPersonMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.atguigu.gmall.product.mapper.TblPersonMapper"><resultMap id="persons" type="com.atguigu.gmall.model.product.TblPerson"><id property="id" column="id"></id><result property="name" column="name"></result><result property="gender" column="gender"></result><collection property="childrens" select="getChildrens" column="{f_id=id}"></collection></resultMap><!--
1、调用getAllPerson 查询数据库,2个人1.1、指定的resultMap开始封装for(2){//1、按照规则封装完。封装到了 childrens, 调用  getChildrens(f_id=1) 查询,2条(4,5)for(2) {//1、封装查到的结果. 封装到了 childrens,调用  getChildrens(f_id=4)查询,3条 (11,12,13)for(3){// 1、封装查到的结果. 封装到了 childrens,调用  getChildrens(f_id=11) == null 。递归结果// 2、封装查到的结果. 封装到了 childrens,调用  getChildrens(f_id=12) == 10for(10){}}}}
--><select id="getAllPerson" resultMap="persons">select * from tbl_person  where father_id = 0</select><select id="getChildrens" resultMap="persons">select * from tbl_person where father_id = #{f_id}</select>
</mapper>
(5)application.yaml
#mybatis-plus:
logging:level:com:atguigu:gmall: debug
server:port: 8000spring:main:allow-bean-definition-overriding: true  #允许bean定义信息的重写datasource:url: jdbc:mysql://192.168.200.188:3306/gmall_product?characterEncoding=utf-8&useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.jdbc.Driverzipkin:base-url: http://192.168.200.188:9411/sender:type: webredis:host: 192.168.200.188port: 6379password: yangfanminio:url: http://192.168.200.188:9000accessKey: gmall-osssecretKey: gmall123defaultBucket: gmallsearch-service:  # 配置自己的业务线程池thread:core: 2max:  5keepalive: 60000queue-length: 10#mybatis-plus:
logging:level:com:atguigu:gmall: debug
(6)MapperTest 测试【递归SQL】
@SpringBootTest
public class MapperTest {@AutowiredBaseCategory3Mapper baseCategory3Mapper;@@AutowiredTblPersonMapper tblPersonMapper@Testvoid test01(){List<IndexCategoryVo> allCategorysForIndexHtml =baseCategory3Mapper.getAllCategorysForIndexHtml();System.out.println("======");}@Testvoid test02(){List<TblPerson> allPerson =tblPersonMapper.getAllPerson();System.out.println("======");}
}
(7)debug 测试

控制台:

5、service-feign-client

新建 com.atguigu.gmall.feign.product.CategoryFeignClient

@FeignClient("service-product")
public interface CategoryFeignClient {@GetMapping("/admin/product/getAllCategorys")List<IndexCategoryVo> getAllCategoryForIndexHtml();
}

6、web-all

① IndexController

com.atguigu.gmall.web.all.controller.IndexController

@Controller
public class IndexController {@AutowiredIndexService indexService;@GetMapping({"/","/index.html"})public String index(Model model) {//调用商品远程服务查询int i = 1;List<IndexCategoryVo> indexCategory = indexService.getIndexCategory();for (IndexCategoryVo indexCategoryVo : indexCategory) {indexCategoryVo.setIndex(i++);}model.addAttribute("list",indexCategory);return "index/index";}
}

② IndexService

com.atguigu.gmall.web.all.service.IndexService

public interface IndexService{List<IndexCategoryVo> getIndexCategory();
}

③ IndexServiceImpl

com.atguigu.gmall.web.all.service.impl.IndexServiceImpl

@Service
public class IndexServiceImpl implements IndexService {@AutowiredCategoryFeignClient categoryFeignClient;@Overridepublic List<IndexCategoryVo> getIndexCategory(){List<IndexCategoryVo> allCategoryForIndexHtml = categoryFeignClient.getAllCategoryForIndexHtml();return allCategoryForIndexHtml;}
}

启动发现报错了

④ ServiceWebApplication

@EnableFeignClients({"com.atguigu.gmall.feign.item","com.atguigu.gmall.feign.product"})
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ServiceWebApplication {public static void main(String[] args) {SpringApplication.run(ServiceWebApplication.class, args);}
}

测试,菜单出来了

六、改造缓存切面,使用SpEL表达式指定缓存

思考:我们有必要每次查首页数据都去数据库中查询吗?

完全可以把首页数据放到缓存嘛

1、思考

下面的这种写法有没有什么问题?

① IndexServiceImpl

首页最终以 index:category 为键作为缓存,这里被写死了

② ServiceWebConfig

@EnableAspectJAutoProxy
@Configuration
@Import({GmallCacheAspect.class})
public class ServiceWebConfig {}

③ GmallCacheAspect

由于 IndexServiceImpl 中的 index:category 写死了,我们要拿到 args 的参数,取 args[0],这效果就爆炸

我们应该动态传递 index:category,让用户指定表达式,而不应该写死

④ GmallCache

2、SpElTest 测试

现在给我们自定义的注解加表达式功能

SpElTest

新建 com.atguigu.gmall.web.test.SpElTest

public class SpElTest {@Testvoid test01() {String expStr = "index:#{#abc[0]}:#{#abc[1]}:categoty";//index:11:18:categoty  //1、准备一个SpEL的解析器SpelExpressionParser expressionParser = new SpelExpressionParser();//2、让解析器解析我们指定的字符串Expression expression = expressionParser.parseExpression(expStr, new TemplateParserContext());//3、上下文指定好所有能用的东西//上下文。相当于一个map,提前装好值。EvaluationContext context = new StandardEvaluationContext();List<Integer> integers = Arrays.asList(11, 18, 22, 56);String value = expression.getValue(context, String.class);System.out.println("表达是的值是:" + value);}
}

启动测试1:

setValue 传一个上下文对象

EvaluationContext:计算的上下文,表达式中解析的这些值从哪里来,这个需要提前告知,而上下文就是用来保存解析的这些值是从哪里来的

测试2:

public class SpElTest {@Testvoid test01() {//        String expStr = "index:#{#abc[0]}:#{#abc[1]}:categoty";String expStr = "index-#{#abc[#abc.size()-1]}";//  #属性名  能获取到 EvaluationContext 里面放好的对象//index:11:18:categoty  index-//1、准备一个SpEL的解析器SpelExpressionParser expressionParser = new SpelExpressionParser();//2、让解析器解析我们指定的字符串Expression expression = expressionParser.parseExpression(expStr, new TemplateParserContext());//3、上下文指定好所有能用的东西//上下文。相当于一个map,提前装好值。EvaluationContext context = new StandardEvaluationContext();List<Integer> integers = Arrays.asList(11, 18, 22, 56);context.setVariable("abc", integers);String value = expression.getValue(context, String.class);System.out.println("表达是的值是:" + value);}
}

启动测试2:

3、开始改造【item】

① GmallCache

/*** 语法规范:* #{ #对象名 }* 可以写的对象名:* args:代表当前目标方法的所有参数* method: 代表当前目标方法* xxxxx** @return*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface GmallCache {String cacheKeyExpr() default "";//缓存key用的表达式String bloomName() default "";boolean enableBloom() default true; //设置是否需要布隆过滤long ttl() default -1L; //以毫秒为单位long missDataTtl() default 1000 * 60 * 3L; //缓存空值,不宜太长
}

GmallCache 修改之后就有跟他相关联的地方出问题了,接着我们继续改

② ItemServiceImpl

@Service
@Slf4j
public class ItemServiceImpl implements ItemService {@AutowiredSkuInfoFeignClient skuInfoFeignClient;@AutowiredStringRedisTemplate stringRedisTemplate;@AutowiredRedissonClient redissonClient;@Qualifier(BloomName.SKU)@AutowiredRBloomFilter<Object> skuFilter;@Qualifier("corePool")@AutowiredThreadPoolExecutor executor;@GmallCache(cacheKeyExpr = RedisConst.SKUKEY_PREFIX + "#{#args[0]}" + RedisConst.SKUKEY_SUFFIX,ttl = 1000 * 60 * 30,bloomName = BloomName.SKU, //用哪个布隆过滤器可以自己指定missDataTtl = 1000 * 60 * 10) //需要一个切面类,动态切入所有标了这个注解的方法@Overridepublic Map<String, Object> getSkuInfo(Long skuId) {//查数据log.info("要查数据库了....");HashMap<String, Object> map = getFromServiceItemFeign(skuId);return map;}private HashMap<String, Object> getFromServiceItemFeign(Long skuId) {HashMap<String, Object> result = new HashMap<>();//1、查询sku详情 2,Sku图片信息CompletableFuture<SkuInfo> future = CompletableFuture.supplyAsync(() -> {SkuInfo skuInfo = skuInfoFeignClient.getSkuInfo(skuId);result.put("skuInfo", skuInfo);return skuInfo;}, executor);//3、Sku分类信息CompletableFuture<Void> future1 = future.thenAcceptAsync((res) -> {if (res != null) {BaseCategoryView skuCategorys = skuInfoFeignClient.getCategoryView(res.getCategory3Id());result.put("categoryView", skuCategorys);}});//4,销售属性相关信息CompletableFuture<Void> future2 = future.thenAcceptAsync((res) -> {if (res != null) {List<SpuSaleAttr> spuSaleAttrListCheckBySku = skuInfoFeignClient.getSpuSaleAttrListCheckBySku(skuId, res.getSpuId());result.put("spuSaleAttrList", spuSaleAttrListCheckBySku);}}, executor);//5,Sku价格信息CompletableFuture<Void> future3 = future.thenAcceptAsync((res) -> {if (res != null) {BigDecimal skuPrice = skuInfoFeignClient.getSkuPrice(skuId);result.put("price", skuPrice);}}, executor);//6,Spu下面的所有存在的sku组合信息CompletableFuture<Void> future4 = future.thenAcceptAsync((res) -> {if (res != null) {Map map = skuInfoFeignClient.getSkuValueIdsMap(res.getSpuId());ObjectMapper mapper = new ObjectMapper();try {String jsonStr = mapper.writeValueAsString(map);log.info("valuesSkuJson 内容:{}", jsonStr);result.put("valuesSkuJson", jsonStr);} catch (JsonProcessingException e) {log.error("商品sku组合数据转换异常:{}", e);}}}, executor);CompletableFuture<Void> allOf = CompletableFuture.allOf(future, future1, future2, future3, future4);try {allOf.get();} catch (Exception e) {log.error("线程池异常:{}", e);}return result;}
}

③ GmallCacheAspect

@Slf4j
@Aspect
@Component
public class GmallCacheAspect {@Qualifier(BloomName.SKU)@AutowiredRBloomFilter<Object> skufilter;/*** Spring 会自动从容器中拿到所有 RBloomFilter 类型的组件* 给我们封装好map,map的key用的就是组件的名,值就是组件对象*/@AutowiredMap<String, RBloomFilter<Object>> blooms;@AutowiredStringRedisTemplate stringRedisTemplate;@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuIdObject[] args = pjp.getArgs();log.info("分布式缓存切面,前置通知···");// 拿到当前方法 @GmallCache 标记注解的值// 拿到当前方法的详细信息MethodSignature signature = (MethodSignature) pjp.getSignature();// 拿到标记的注解的值GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);String cacheKeyExpr = gmallCache.cacheKeyExpr();boolean enableBloom = gmallCache.enableBloom();String bloomName = gmallCache.bloomName();long ttl = gmallCache.ttl();long missDataTtl = gmallCache.missDataTtl();String redisCachekey = parseExpression(cacheKeyExpr, pjp);//prefix + args[0] + bloomSuffix;String intern = redisCacheKey.intern();log.info("redisCacheKey:对象地址:", intern);//优化后逻辑,先看缓存是否存在//1、先看缓存有没有Object cache = getFromCache(redisCachekey, method);if (cache != null) {// 缓存中有直接返回return cache;}//2、缓存中如果没有,准备查库要问布隆try {//1、先看缓存有没有if (enableBloom) {//使用布隆RBloomFilter<Object> bloomFilter = blooms.get(bloomName);boolean contains = bloomFilter.contains(redisCachekey);//布隆查redis,比缓存快很多...if (!contains) {return null;}}//如果缓存中没有log.info("分布式缓存切面,准备加锁执行目标方法......");synchronized (intern) { log.info("强锁成功......正在双检查");Object cacheTemp = getFromCache(redisCachekey, method);if (cacheTemp == null) {//前置通知//目标方法的返回值不确定Object proceed = pjp.proceed(args); //目标方法的执行,目标方法的异常切面不要吃掉saveToCache(redisCachekey, proceed, ttl, missDataTtl);return proceed;}return cacheTemp;}} catch (Exception e) {log.error("缓存切面失败:{}", e);} finally {//后置通知log.info("分布式缓存切面,如果分布式锁要在这里解锁......");}return cache;}/*** 把数据保存到缓存** @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();// 缓存是 JSONString 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;}private String parseExpression(String cacheKeyExpr, ProceedingJoinPoint pjp) {//1、准备解析器SpelExpressionParser parser = new SpelExpressionParser();//2、得到表达式对象Expression expression = parser.parseExpression(cacheKeyExpr, new TemplateParserContext());//3、获取表达式真正的值StandardEvaluationContext context = new StandardEvaluationContext();//代表的就是目标方法的参数context.setVariable("args", pjp.getArgs());String value = expression.getValue(context, String.class);return value;}
}

④ IndexServiceImpl

@Service
public class IndexServiceImpl implements IndexService {final String cachePrefix = "index:category";@AutowiredCategoryFeignClient categoryFeignClient;@GmallCache(cacheKeyExpr = "index:category",ttl = 1000*60*60*24*5,enableBloom = false)@Overridepublic List<IndexCategoryVo> getIndexCategory(){List<IndexCategoryVo> allCategoryForIndexHtml = categoryFeignClient.getAllCategoryForIndexHtml();return allCategoryForIndexHtml;}
}

4、测试 item

首先会来到切面,拿到目标方法的参数

往下走,来到 cacheKeyExpr

看 spring 怎么解析【图中应该是 sku:#{#args[0]}】

step into 进到 parseExpression 中【图中应该是 sku:#{#args[0]}】

【图中应该是 sku:#{#args[0]}】

value 是 sku:51:info 跟之前一样,该咋整咋整

继续往下

5、继续改造【web-all】

① IndexServiceImpl

com.atguigu.gmall.web.all.service.impl.IndexServiceImpl

思考:这个缓存要咋搞

方法名是什么,缓存的名就是什么。那我直接把 getIndexCategory 一复制粘到 CacheKeyExpr 行不行?

这样肯定不行,如果还有参数值怎么办?参数值从哪里复制粘贴呢?参数值可是动态的呀

所以我们干脆都动态获取

@Service
public class IndexServiceImpl implements IndexService {final String cachePrefix = "index:category";@AutowiredCategoryFeignClient categoryFeignClient;@GmallCache(cacheKeyExpr = "index:category:#{#method.getName()}",ttl = 1000*60*60*24*5,enableBloom = false)@Overridepublic List<IndexCategoryVo> getIndexCategory(){List<IndexCategoryVo> allCategoryForIndexHtml = categoryFeignClient.getAllCategoryForIndexHtml();return allCategoryForIndexHtml;}
}

再看,为什么我们可以这么写?cacheKeyExpr = "index:category:#{#method.getName()}"一定是有人要解析这个!

这个解析的人就是 parseExpression

③ GmallCacheAspect

@Slf4j
@Aspect
@Component
@Import(ItemServiceRedissonConfig.class)
public class GmallCacheAspect {@Qualifier(BloomName.SKU)@AutowiredRBloomFilter<Object> skufilter;/*** Spring 会自动从容器中拿到所有 RBloomFilter 类型的组件* 给我们封装好map,map的key用的就是组件的名,值就是组件对象*/@AutowiredMap<String, RBloomFilter<Object>> blooms;@AutowiredStringRedisTemplate stringRedisTemplate;@Around("@annotation(com.atguigu.gmall.common.cache.GmallCache)")public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {// 指的是目标方法的参数,也就是 ItemServiceImpl 的 getSkuInfo(Long skuId) 方法中的 skuIdObject[] args = pjp.getArgs();log.info("分布式缓存切面,前置通知···");// 拿到当前方法 @GmallCache 标记注解的值// 拿到当前方法的详细信息MethodSignature signature = (MethodSignature) pjp.getSignature();// 拿到标记的注解的值GmallCache gmallCache = signature.getMethod().getAnnotation(GmallCache.class);String cacheKeyExpr = gmallCache.cacheKeyExpr();boolean enableBloom = gmallCache.enableBloom();String bloomName = gmallCache.bloomName();long ttl = gmallCache.ttl();long missDataTtl = gmallCache.missDataTtl();String redisCachekey = parseExpression(cacheKeyExpr, pjp);//prefix + args[0] + bloomSuffix;String intern = redisCacheKey.intern();log.info("redisCacheKey:对象地址:", intern);//优化后逻辑,先看缓存是否存在//1、先看缓存有没有Object cache = getFromCache(redisCachekey, method);if (cache != null) {// 缓存中有直接返回return cache;}//2、缓存中如果没有,准备查库要问布隆try {//1、先看缓存有没有if (enableBloom) {//使用布隆RBloomFilter<Object> bloomFilter = blooms.get(bloomName);boolean contains = bloomFilter.contains(redisCachekey);//布隆查redis,比缓存快很多...if (!contains) {return null;}}//如果缓存中没有log.info("分布式缓存切面,准备加锁执行目标方法......");synchronized (intern) { log.info("强锁成功......正在双检查");Object cacheTemp = getFromCache(redisCachekey, method);if (cacheTemp == null) {//前置通知//目标方法的返回值不确定Object proceed = pjp.proceed(args); //目标方法的执行,目标方法的异常切面不要吃掉saveToCache(redisCachekey, proceed, ttl, missDataTtl);return proceed;}return cacheTemp;}} catch (Exception e) {log.error("缓存切面失败:{}", e);} finally {//后置通知log.info("分布式缓存切面,如果分布式锁要在这里解锁......");}return cache;}/*** 把数据保存到缓存** @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();// 缓存是 JSONString 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;}private String parseExpression(String cacheKeyExpr, ProceedingJoinPoint pjp) {//1、准备解析器SpelExpressionParser parser = new SpelExpressionParser();//2、得到表达式对象Expression expression = parser.parseExpression(cacheKeyExpr, new TemplateParserContext());//3、获取表达式真正的值StandardEvaluationContext context = new StandardEvaluationContext();//代表的就是目标方法的参数context.setVariable("args", pjp.getArgs());MethodSignature signature = (MethodSignature) pjp.getSignature();Method method = signature.getMethod();context.setVariable("method", method);String value = expression.getValue(context, String.class);return value;}
}

④ 启动报错

redis 连接不上

我们可以在 service-util 中配置一份,让大家都来用,如果大家有自己的 redis 配置那就用自己的,如果自己没有配置就用公共的

⑤ service-util 的 application.yaml

spring:redis:host: 192.168.200.188port: 6379password: yangfan

启动还是报错,这就很尴尬了,算了直接配置 web-all 自己的 redis

⑥ web-all 的 application.yaml

spring:zipkin:base-url: http://192.168.200.188:9411/sender:type: webthymeleaf:cache: falseservlet:content-type: text/htmlencoding: UTF-8mode: HTML5prefix: classpath:/templates/suffix: .htmlredis:host: 192.168.200.188port: 6379password: yangfanmain:allow-bean-definition-overriding: true

6、测试 web-all 的 index 首页

后台,先来到切面,pjp 获取目标方法的参数,没有参数

往下继续

step into 到 parseExpression 中

此时的 value 就是动态获取的方法名

继续往后走的时候发现

① 转换有 bug

② debug GmallCacheAspect

从缓存中拿的 JSON 数据没问题

ArrayList 中的数据是一个 Map

所以从缓存中读取数据的时候要使用带泛型的 getReturnType

/*** 把数据从缓存加载* <p>* 别把这个写死 Map<String, Object>** @param cacheKey* @return*/
public Object getFromCache(String cacheKey, Method method) throws JsonProcessingException {//缓存是jsonObjectMapper objectMapper = new ObjectMapper();String json = stringRedisTemplate.opsForValue().get(cacheKey);if ("miss".equals(json)) {return null;}// 使用目标方法的返回值类型,序列化和反序列化 redis 的数据Class<?> returnType = method.getReturnType();Type genericReturnType = method.getGenericReturnType();Class<? extends Type> aClass = genericReturnType.getClass();if (!StringUtils.isEmpty(json)) {//json和map本来就是对应的TypeReference typeReference = new TypeReference<Class<? extends Type>>() {@Overridepublic Type getType() {return genericReturnType;}};Object readValue = objectMapper.readValue(json, typeReference);return readValue;}return null;
}

测试,重新来到 getFromCache

genericReturnType 的类型

谷粒商城 Day09 首页分类与SpEL动态缓存切面相关推荐

  1. 谷粒商城--API三级分类--网关统一配置跨域

    什么是跨域,为什么会有跨域问题的出现 一.什么是同源策略? 1.同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本 ...

  2. 谷粒商城-分布式高级篇[商城业务-检索服务]

    谷粒商城-分布式基础篇[环境准备] 谷粒商城-分布式基础[业务编写] 谷粒商城-分布式高级篇[业务编写]持续更新 谷粒商城-分布式高级篇-ElasticSearch 谷粒商城-分布式高级篇-分布式锁与 ...

  3. 谷粒商城-分布式高级篇【业务编写】

    谷粒商城-分布式基础篇[环境准备] 谷粒商城-分布式基础[业务编写] 谷粒商城-分布式高级篇[业务编写]持续更新 谷粒商城-分布式高级篇-ElasticSearch 谷粒商城-分布式高级篇-分布式锁与 ...

  4. 谷粒商城-分布式高级篇[商城业务-秒杀服务]

    谷粒商城-分布式基础篇[环境准备] 谷粒商城-分布式基础[业务编写] 谷粒商城-分布式高级篇[业务编写]持续更新 谷粒商城-分布式高级篇-ElasticSearch 谷粒商城-分布式高级篇-分布式锁与 ...

  5. 谷粒商城六商品服务三级分类

    递归-树形结构数据获取 sql文件 sql文件太大了,这个博主写的非常厉害,看他的就ok了 CategoryController package com.atguigu.gulistore.produ ...

  6. 分布式项目-谷粒商城。

    分布式项目 一,分布图 二,环境搭建 1.安装linux 2.安装docker 1 卸载系统之前的docker sudo yum remove docker \docker-client \docke ...

  7. 谷粒商城高级篇上(未完待续)

    谷粒商城高级篇(上)保姆级整理 之前整理了基础篇,Typora提示将近20000词,谷粒商城基础篇保姆级整理 在学高级篇的时候,不知不觉又整理了两万多词,做了一阶段,先发出来,剩余部分整理好了再发.自 ...

  8. 谷粒商城高级篇笔记1

    这里写自定义目录标题 0.ElasticSearch 1.Nginx配置域名问题 01.Nginx(反向代理) 配置 02.Nginx(负载均衡)+ 网关 配置 03.Nginx动静分离 2.JMet ...

  9. 谷粒商城-高级篇-aiueo

    105 初步检索 105.1 _cat GET /_cat/nodes : 查看所有节点 GET /_cat/health : 查看es健康状况 GET /_cat/master : 查看主节点 GE ...

最新文章

  1. head tail mkdir cp
  2. 第一个shell脚本
  3. java中treemap_Java中TreeMap集合讲解
  4. 【转】PF_RING学习笔记
  5. cjson 对象是json数组型结构体_C语言 - cJSON解析特定格式 含有数组array类型的数据...
  6. goldengate for mysql_GoldenGate for mysql to mysql:单向同步
  7. 浏览器接收到html文档后,认识HTMl,了解HTML文档在服务器和浏览器间是如何传递的...
  8. cocos2dx-lua 骨骼动画spine使用心得(cocos2dx版本 3.17 spine版本3.6.53)
  9. 苏州真不能成为一线城市?
  10. MySQL临时表的作用
  11. 开机预读快还是不预读快_启用预读为网页浏览提速
  12. mongodb分片集群数据库安全认证
  13. 抖音账号和视频都没有问题,为什么我的流量还是不好?丨国仁网络资讯
  14. HTML5期末大作业:个人网页设计——薛之谦6页(代码质量好) 学生DW网页设计作业源码 web课程设计网页规划与设计
  15. 超详细且简单的Qt Designer设置界面背景图
  16. NS3中路由协议分析【AODV代码分析】
  17. 如画的水乡,如画的同里1012
  18. 6年全栈工程师回答:web前端的主要学习什么,现在还有前途吗?一般工资是多少?
  19. matlab如何做粒子模拟,求助,如何用matlab做蒙特卡罗模拟!!??
  20. Ubuntu17.10如何安装网易云音乐并解决无法打开

热门文章

  1. 【2020年高被引学者】 陶大程 悉尼大学
  2. (二) DIM-SUM系统环境搭建之编译与调试环境
  3. python3 笔记6 字符串
  4. iphone11屏比例_iPhone 11屏幕和iPhone X哪个大 iPhone 11和iPhone X屏幕大小对比
  5. 微信视频号的抓取记录
  6. udp通信2--多发多收
  7. flash 嵌入html代码,flash嵌入html在html网页代码中嵌入Flash文件的解决方案(下).doc...
  8. 中医学专业学c语言吗,考研专业课中医学题型分析
  9. LP wizard无法生成PCB封装
  10. 软件测试-5-移动应用测试