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

本文探讨了如何在高并发场景下优化商品详情页的缓存策略,通过GmallCache切面和SpEL表达式动态调整缓存,结合异步编排和布隆过滤器,提升数据获取速度。同时,介绍了如何在首页商品分类查询中应用缓存和远程调用的异步处理。
摘要由CSDN通过智能技术生成

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

商品详情流程_00

一、优化缓存逻辑

百万并发进来,判断 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)
    @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);
        
        //百万并发进来????
        //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();
        // 缓存是 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;
    }
}

二、异步编排 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()

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

image-20210928181506780 image-20210928181556534

发现 Runnable 是一个函数式接口

image-20210928181818620
复习:什么是函数式接口?
没有参数、没有返回值
    
@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);
}
image-20210928182638810

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

3、supplyAsync()

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

image-20210928182842728
//异步任务什么时候执行,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");
image-20210928183208040

4、线程池

异步任务提交给哪里了?

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

Executors.newFixedThreadPool(5)

image-20211215002627345

public static void main(String[] args) {
    //1、创建一个线程池,当前线程池同时处理最多5个  core=5  max=5
        ThreadPoolExecutor 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);
    });
}
image-20210928215807042
② 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);
}
image-20210928220021747

6、exceptionally 异常回调

当没有使用 exceptionally 时

image-20210928220352743

使用 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);
}
image-20210928220736413

7、thenRun

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

thenRun

image-20210928225442807 image-20210928225247416

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().then
    CompletableFuture.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);
}
image-20210928225608690

8、thenAccept

thenAccept

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

image-20211215085302253

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

image-20211215085647904 image-20211215085752023 image-20210928230037407 image-20210928230057653

9、thenApply

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

image-20211215085035507 image-20211215085138950
/**
 * 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);

}
image-20210928230626859

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");
}  
image-20210928231620141

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

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");
}  
image-20210928233548068

11、anyOf

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

image-20211215092107945

三、自己创建业务线程池

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

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 {
    //配置自己的业务线程池 核心业务线程池
    @Bean
    public ThreadPoolExecutor executor() {
        return new ThreadPoolExecutor(16,
                32,
                1, //线程池 1min了都没有活要干了
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(10000),
                new MyItemServiceThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
    }
}
image-20211215150453735
② MyItemServiceThreadFactory

com.atguigu.gmall.item.config.MyItemServiceThreadFactory

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

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

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

image-20211215151223301

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

2、第二版

① application.yaml

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

item-service:  # 配置自己的业务线程池
  thread:
    core: 2
    max:  5
    keepalive: 60000
    queue-length: 10
server:
  port: 9000
  tomcat:
    accept-count: 10000  # tomcat线程池的队列长度  ServerProperties.Tomcat
    threads:
      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: web
  redis:
    host: 192.168.200.188
    port: 6379
    password: yangfan
#底层我们配置好了redisson,redisson功能的快速开关
#redisson:
#  enable: false  #关闭我们自己配置的redisson

item-service:  # 配置自己的业务线程池
  thread:
    core: 2
    max:  5
    keepalive: 60000
    queue-length: 10

logging:
  level:
    com:
      atguigu:
        gmall: info
② ItemConfig

这样写会比较麻烦

image-20211215151916499

改进:

把 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")
    @Data
    class ThreadConfigProperties {
        private Integer core;
        private Integer max;
        private Long keepalive;
        private Integer queueLength;
    }

    class MyItemServiceThreadFactory implements ThreadFactory {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("item-service: " + UUID.randomUUID().toString().substring(0, 5));
            return thread;
        }
    }
    
}
③ 启动测试
image-20211215153134014 image-20211215153202764

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

1、ItemServiceImpl

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

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

image-20211215154428532 image-20211215154529229

改造后:

@Service
@Slf4j
public class ItemServiceImpl implements ItemService {
	@Autowired
    SkuInfoFeignClient skuInfoFeignClient;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedissonClient redissonClient;

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

    @Qualifier("corePool") // 指定用 corePool 线程池
    @Autowired
    ThreadPoolExecutor executor;
    
    @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;
    }
    
    /**
     *  业务线程池控制住所有并发,防止无限消耗
     * 我以下的写法,
     *  对比  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 返回的结果,即 skuInfo
        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;
    }
    
}

注意点:

image-20211215162846505

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")
    @Data
    class ThreadConfigProperties {
        private Integer core;
        private Integer max;
        private Long keepalive;
        private Integer queueLength;
    }

    class MyItemServiceThreadFactory implements ThreadFactory {
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("item-service: " + UUID.randomUUID().toString().substring(0, 5));
            return thread;
        }
    }
}

五、首页商品分类实现

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

image-20211215170417226

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

1、IndexController(测试)

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

image-20211215170939441

image-20211215172325475

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

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

2、配置主机的 host 地址

image-20211215173029712

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

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

3、api-gateway 网关配置

application.yaml

之前的:

image-20211215173650718

配置后:

image-20211215174638183

image-20211215174455106

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

image-20211215174708874

通过 feign 远程查询

image-20211215174925814

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 只在一级菜单的时候要

image-20211215175910256 image-20211215180156535 image-20211215181328444

5、IndexCategoryVo

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

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

image-20211215182513921
/**
 * 对应数据库表 专门的 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

image-20211215183740416
    @GetMapping("/getAllCategorys")
    public List<IndexCategoryVo> getAllCategorysForIndexHtml(){

        return categoryService.getAllCategorysForIndexHtml();
    }
/**
 * 对接后台管理系统,/admin/product相关请求
 */
@RequestMapping("/admin/product")
@RestController
public class CategoryAdminController {

    @Autowired
    CategoryService 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_name
        from base_category1 b1
             left join base_category2 b2 on b2.category1_id = b1.id
             left join base_category3 b3 on b3.category2_id = b2.id
        order by b1.id, b2.id, b3.id
    </select>
</mapper>
⑥ MapperTest 测试【方式一】

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

@SpringBootTest
public class MapperTest {

    @Autowired
    BaseCategory3Mapper baseCategory3Mapper;

    @Test
    void test01(){
        List<IndexCategoryVo> allCategorysForIndexHtml =
                baseCategory3Mapper.getAllCategorysForIndexHtml();
        System.out.println("======");
    }
}

debug 运行

image-20211215201302311

17个一级分类

image-20211215201420993

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

image-20211215201457717

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

image-20211215201559598

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

⑦ BaseCategory3Mapper.xml(方式二 ×)

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

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

image-20211215203015378

<?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、对照 resultMap
           for(查出的记录){
               new IndexCategoryVo();  //封装第一个的categoryChild,又会调用 getChildren ; 18
                for(上次的18条){
                   new IndexCategoryVo(); //封装到 categoryChild,又去调用 getChildren; 18; 查出了10条
                   for(上次的10条){
                     new IndexCategoryVo();  // //封装到 categoryChild, 又去调用 getChildren;100,查出了0条。直到封装结束
                   }
                }
           }-->
    <select id="getAllCategorysForIndexHtml" resultMap="categoryForIndex">
        select id, name
        from base_category1
    </select>


    <!--        ${}拼串,用在各个位置   #{}是占位符,只能用在参数位置
                占位符只能用在 key=value 这种情况,像order by就不能使用#{}只能用${} -->
    <select id="getChildren" resultMap="categoryForIndex">
        select id, name
        from base_category2
        where category1_id = #{id}
    </select>
</mapper>

发现三级分类是错误的

image-20211215205007467
⑧ MySQL 递归 SQL 案例

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

(1)数据表

image-20211215205750672

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

image-20211215205926774

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

image-20211215210030950

(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) == 10
             for(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: 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

minio:
  url: http://192.168.200.188:9000
  accessKey: gmall-oss
  secretKey: gmall123
  defaultBucket: gmall

search-service:  # 配置自己的业务线程池
  thread:
    core: 2
    max:  5
    keepalive: 60000
    queue-length: 10

#mybatis-plus:
logging:
  level:
    com:
      atguigu:
        gmall: debug
(6)MapperTest 测试【递归SQL】
@SpringBootTest
public class MapperTest {

    @Autowired
    BaseCategory3Mapper baseCategory3Mapper;
    
    @@Autowired
    TblPersonMapper tblPersonMapper

    @Test
    void test01(){
        List<IndexCategoryVo> allCategorysForIndexHtml =
                baseCategory3Mapper.getAllCategorysForIndexHtml();
        System.out.println("======");
    }
    
    @Test
    void test02(){
        List<TblPerson> allPerson =
                tblPersonMapper.getAllPerson();
        System.out.println("======");
    }
}
(7)debug 测试
image-20211215214034195

控制台:
image-20211215214123362

5、service-feign-client

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

image-20211215175110232
@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 {

    @Autowired
    IndexService 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 {
    @Autowired
    CategoryFeignClient categoryFeignClient;
    
    @Override
    public List<IndexCategoryVo> getIndexCategory(){
        List<IndexCategoryVo> allCategoryForIndexHtml = categoryFeignClient.getAllCategoryForIndexHtml();
        return allCategoryForIndexHtml;
    }
}

启动发现报错了

image-20211215225154855

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

测试,菜单出来了

image-20211215225413509

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

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

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

1、思考

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

① IndexServiceImpl

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

image-20211216095903055

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

image-20211216095251659

③ GmallCacheAspect

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

image-20211216095624649

image-20211216095503053

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

image-20211216095906784

④ GmallCache

image-20211216100004379

2、SpElTest 测试

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

image-20211216101940600

SpElTest

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

public class SpElTest {

    @Test
    void 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:

image-20211216114138571

setValue 传一个上下文对象

image-20211216104503901

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

image-20211216104620060

测试2:

public class SpElTest {

    @Test
    void 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:

image-20211216114952767

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 {
    
    @Autowired
    SkuInfoFeignClient skuInfoFeignClient;

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedissonClient redissonClient;

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

    @Qualifier("corePool")
    @Autowired
    ThreadPoolExecutor executor;
    
    @GmallCache(
            cacheKeyExpr = RedisConst.SKUKEY_PREFIX + "#{#args[0]}" + RedisConst.SKUKEY_SUFFIX,
            ttl = 1000 * 60 * 30,
            bloomName = BloomName.SKU, //用哪个布隆过滤器可以自己指定
            missDataTtl = 1000 * 60 * 10
    ) //需要一个切面类,动态切入所有标了这个注解的方法
    @Override
    public 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)
    @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 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();
        // 缓存是 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;
    }
    
    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";

    @Autowired
    CategoryFeignClient categoryFeignClient;

    @GmallCache(
            cacheKeyExpr = "index:category",
            ttl = 1000*60*60*24*5,
            enableBloom = false
    )
    @Override
    public List<IndexCategoryVo> getIndexCategory(){
        List<IndexCategoryVo> allCategoryForIndexHtml = categoryFeignClient.getAllCategoryForIndexHtml();
        return allCategoryForIndexHtml;
    }
}

4、测试 item

image-20211216135327311

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

image-20211216135408914

往下走,来到 cacheKeyExpr

image-20211216140350241

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

image-20211216135700680

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

image-20211216135810429

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

image-20211216135935338

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

继续往下

image-20211216142505563

5、继续改造【web-all】

① IndexServiceImpl

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

思考:这个缓存要咋搞

image-20211216143613868

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

image-20211216144022509

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

image-20211216145036992

所以我们干脆都动态获取

@Service
public class IndexServiceImpl implements IndexService {

    final String cachePrefix = "index:category";

    @Autowired
    CategoryFeignClient categoryFeignClient;

    @GmallCache(
            cacheKeyExpr = "index:category:#{#method.getName()}",
            ttl = 1000*60*60*24*5,
            enableBloom = false
    )
    @Override
    public List<IndexCategoryVo> getIndexCategory(){
        List<IndexCategoryVo> allCategoryForIndexHtml = categoryFeignClient.getAllCategoryForIndexHtml();
        return allCategoryForIndexHtml;
    }
}

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

这个解析的人就是 parseExpression

image-20211216145455587

③ GmallCacheAspect
@Slf4j
@Aspect
@Component
@Import(ItemServiceRedissonConfig.class)
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 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();
        // 缓存是 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;
    }
    
    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 连接不上

image-20211216150210278

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

⑤ service-util 的 application.yaml
spring:
  redis:
    host: 192.168.200.188
    port: 6379
    password: yangfan
image-20211216150326511

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

⑥ web-all 的 application.yaml
spring:
  zipkin:
    base-url: http://192.168.200.188:9411/
    sender:
      type: web
  thymeleaf:
    cache: false
    servlet:
      content-type: text/html
    encoding: UTF-8
    mode: HTML5
    prefix: classpath:/templates/
    suffix: .html
  redis:
    host: 192.168.200.188
    port: 6379
    password: yangfan

  main:
    allow-bean-definition-overriding: true
image-20211216150838178

6、测试 web-all 的 index 首页

image-20211216151206394

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

image-20211216151230910

往下继续

image-20211216151351786

step into 到 parseExpression 中

image-20211216151602033

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

image-20211216151725698

继续往后走的时候发现

① 转换有 bug

image-20211216151941865

② debug GmallCacheAspect

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

image-20211216152629146 image-20211216153230047 image-20211216153333959

ArrayList 中的数据是一个 Map

image-20211216153421055

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

/**
 * 把数据从缓存加载
 * <p>
 * 别把这个写死 Map<String, Object>
 *
 * @param cacheKey
 * @return
 */
public Object getFromCache(String cacheKey, Method method) throws JsonProcessingException {
    //缓存是json
    ObjectMapper 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>>() {
            @Override
            public Type getType() {
                return genericReturnType;
            }
        };

        Object readValue = objectMapper.readValue(json, typeReference);
        return readValue;
    }
    return null;
}

测试,重新来到 getFromCache

image-20211216154652109

genericReturnType 的类型

image-20211216155557505

image-20211216161116587

image-20211216161151834

image-20211216161318493

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值