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)
@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](https://i-blog.csdnimg.cn/blog_migrate/439be05e295bab5f819a940a94818883.png)
![image-20210928181556534](https://i-blog.csdnimg.cn/blog_migrate/49b8f3b11c024b62168059de4b327cac.png)
发现 Runnable 是一个函数式接口
![image-20210928181818620](https://i-blog.csdnimg.cn/blog_migrate/d3bb14b8a52b91a3cb07e6b5aec9b4d2.png)
复习:什么是函数式接口?
没有参数、没有返回值
@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](https://i-blog.csdnimg.cn/blog_migrate/6272757da86ad7ec2bdb03750ac23253.png)
runAsync() 是没有返回值的,如果需要返回值就得用 supplyAsync()
3、supplyAsync()
快速启动一个异步任务,我们关心异步任务的返回值
![image-20210928182842728](https://i-blog.csdnimg.cn/blog_migrate/4d73c3d8e1cd931ad6bea1bf968ba3b0.png)
//异步任务什么时候执行,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](https://i-blog.csdnimg.cn/blog_migrate/850442ea18d42b9b43c9ddd09bbc3c2a.png)
4、线程池
异步任务提交给哪里了?
业务有自己的线程池,异步任务提交给我们自己的线程池
Executors.newFixedThreadPool(5)
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](https://i-blog.csdnimg.cn/blog_migrate/40dfd9efbd52f1fe5c0f7e1fb0d1652d.png)
② 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](https://i-blog.csdnimg.cn/blog_migrate/3472586a07785d7471ea20a544ad0f41.png)
6、exceptionally 异常回调
当没有使用 exceptionally 时
![image-20210928220352743](https://i-blog.csdnimg.cn/blog_migrate/f0c7528ffcba9c97486b9c9c073b8882.png)
使用 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](https://i-blog.csdnimg.cn/blog_migrate/37c46d7f4603590eeed080a4ca0f81f8.png)
7、thenRun
thenXXXX:异步任务运行成功,接下来要做的事情
thenRun
![image-20210928225442807](https://i-blog.csdnimg.cn/blog_migrate/14a9f1084379731393f7fbe0ee43d2de.png)
![image-20210928225247416](https://i-blog.csdnimg.cn/blog_migrate/25fd726bb60092ccf9cb83dd986d8cd0.png)
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](https://i-blog.csdnimg.cn/blog_migrate/5b932ee405fd891eba9d57c727e41128.png)
8、thenAccept
thenAccept
thenAccept 是一个消费者接口,只有一个泛型,没有返回值
![image-20211215085302253](https://i-blog.csdnimg.cn/blog_migrate/1112e1b243c9ad226c67ae828c2f3d24.png)
thenAccept 有一个入参,没有返回值
![image-20211215085647904](https://i-blog.csdnimg.cn/blog_migrate/f99bcec39fffc3d0515f7ef5d2dc286f.png)
![image-20211215085752023](https://i-blog.csdnimg.cn/blog_migrate/0133f8ccff23a3ce344688de28aff8bd.png)
![image-20210928230037407](https://i-blog.csdnimg.cn/blog_migrate/64f075ad8fec4df753cfdc809d9405ca.png)
![image-20210928230057653](https://i-blog.csdnimg.cn/blog_migrate/9478f4922f8e1fc035823271d43a35f7.png)
9、thenApply
函数式接口,有两个泛型,T 为入参,R 为出参
![image-20211215085035507](https://i-blog.csdnimg.cn/blog_migrate/40fffe37f78ac29e625031a85ea7829d.png)
![image-20211215085138950](https://i-blog.csdnimg.cn/blog_migrate/333c4bd70423fe77d16f3ba651d8841b.png)
/**
* 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](https://i-blog.csdnimg.cn/blog_migrate/443bd88e9ab808cfe73ab1d5948033ca.png)
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](https://i-blog.csdnimg.cn/blog_migrate/2f0d7ec02ec6c19527af5060f605af99.png)
情况二:异步任务全部走完再打印
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](https://i-blog.csdnimg.cn/blog_migrate/8638d3cfc455fd995a1ae212b51e5511.png)
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 {
//配置自己的业务线程池 核心业务线程池
@Bean
public ThreadPoolExecutor executor() {
return new ThreadPoolExecutor(16,
32,
1, //线程池 1min了都没有活要干了
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(10000),
new MyItemServiceThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
}
}
![image-20211215150453735](https://i-blog.csdnimg.cn/blog_migrate/33769a7c3a9932459398c394b7e6e75a.png)
② 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](https://i-blog.csdnimg.cn/blog_migrate/f5135ea94a525c95161b9db03f12b34a.png)
因此我们可以在配置文件中对线程池进行配置
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](https://i-blog.csdnimg.cn/blog_migrate/1092789ea068e428cd33ce0fb364a15a.png)
改进:
把 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](https://i-blog.csdnimg.cn/blog_migrate/bdb0689e239715449e0ddf7d7fd73e19.png)
![image-20211215153202764](https://i-blog.csdnimg.cn/blog_migrate/7e48176dcae19771a1dba2e9cfbf0b12.png)
四、使用异步编排改写代码
1、ItemServiceImpl
com.atguigu.gmall.item.service.impl.ItemServiceImpl
以前的 getFromServiceItemFeign,其实就是 getFromServiceItemFeign01版,因为是同步,所以速度比较慢
![image-20211215154428532](https://i-blog.csdnimg.cn/blog_migrate/d6501678b6167aac264d80babb0c1aa8.png)
![image-20211215154529229](https://i-blog.csdnimg.cn/blog_migrate/ddef5d6aab5bad0f1c46909d0a4f594f.png)
改造后:
@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](https://i-blog.csdnimg.cn/blog_migrate/d6af0c08bf3e9434b226943f967cd188.png)
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 的详情
现在我们要查询首页商品的详情,老规矩先搭环境
1、IndexController(测试)
新建 com.atguigu.gmall.web.all.controller.IndexController
![image-20211215172325475](https://i-blog.csdnimg.cn/blog_migrate/118de98d7b9dda4634de2738e21b72ff.png)
我们现在希望能通过域名直接访问首页
所以接下来需要我们配置一下主机的 host 地址
2、配置主机的 host 地址
![image-20211215173029712](https://i-blog.csdnimg.cn/blog_migrate/f4ecf69c9f545ef425d1675ef758f089.png)
直接访问域名就是本机ip:80,而本机ip:80就是访问的本机网关,我们现在要的效果是访问域名直接来到尚品汇的首页
接下来就要在网关里面配置,所有的域名都要往对应的地址转
3、api-gateway 网关配置
application.yaml
之前的:
![image-20211215173650718](https://i-blog.csdnimg.cn/blog_migrate/be121f184e86e8180bdc443e814793b0.png)
配置后:
效果出来了,但是页面左侧的三级菜单还需要远程请求才能出来
通过 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 只在一级菜单的时候要
![image-20211215175910256](https://i-blog.csdnimg.cn/blog_migrate/9cd3ba98e1c17cc88998bbfc8e9b6df6.png)
![image-20211215180156535](https://i-blog.csdnimg.cn/blog_migrate/e3d924603f482637c78da5ff0ce9d547.png)
![image-20211215181328444](https://i-blog.csdnimg.cn/blog_migrate/19c5f6a10dcc1aafc34e43cae5958d05.png)
5、IndexCategoryVo
新建 com.atguigu.gmall.model.vo.IndexCategoryVo
推荐如果数据有自己特定的返回特性,我们就在 model 中放一份对应的 VO
![image-20211215182513921](https://i-blog.csdnimg.cn/blog_migrate/03d4ab17839650ce3bd1195d29996000.png)
/**
* 对应数据库表 专门的 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](https://i-blog.csdnimg.cn/blog_migrate/0f4f14729b2f75dcd29d318dec942f83.png)
@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](https://i-blog.csdnimg.cn/blog_migrate/054f51ab55b192eb2257ce292d7dcc6e.png)
17个一级分类
![image-20211215201420993](https://i-blog.csdnimg.cn/blog_migrate/1ea993c2554a143c61b8425a3934f2c8.png)
一级分类展开后还有12个二级分类
![image-20211215201457717](https://i-blog.csdnimg.cn/blog_migrate/95659820c31a57b446c556ac382c0789.png)
二级分类展开后还有3个三级分类
![image-20211215201559598](https://i-blog.csdnimg.cn/blog_migrate/d007f702a0611b529461e77f48734179.png)
找到了我们想要的数据,但是如果有四层、五层、六层······那就很麻烦了,所以我们可以使用方式二
⑦ 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、对照 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](https://i-blog.csdnimg.cn/blog_migrate/784ff085e593ca47ec14dedc13aab214.png)
⑧ 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) == 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](https://i-blog.csdnimg.cn/blog_migrate/2779eacb6f447a08efba774d250326ed.png)
控制台:
5、service-feign-client
新建 com.atguigu.gmall.feign.product.CategoryFeignClient
![image-20211215175110232](https://i-blog.csdnimg.cn/blog_migrate/3b5fa4593b92008a41249ab5e59d3448.png)
@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;
}
}
启动发现报错了
④ 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 {
@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](https://i-blog.csdnimg.cn/blog_migrate/b4ffe23b7395668235e248ce5ae03938.png)
setValue 传一个上下文对象
EvaluationContext:计算的上下文,表达式中解析的这些值从哪里来,这个需要提前告知,而上下文就是用来保存解析的这些值是从哪里来的
测试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](https://i-blog.csdnimg.cn/blog_migrate/aedf5998b7f3ea4edb5b578356caa197.png)
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-20211216135408914](https://i-blog.csdnimg.cn/blog_migrate/1e7e74c23fe4ceee1d510d6eb209d77f.png)
往下走,来到 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";
@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
③ 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 连接不上
我们可以在 service-util 中配置一份,让大家都来用,如果大家有自己的 redis 配置那就用自己的,如果自己没有配置就用公共的
⑤ service-util 的 application.yaml
spring:
redis:
host: 192.168.200.188
port: 6379
password: yangfan
![image-20211216150326511](https://i-blog.csdnimg.cn/blog_migrate/4d39d4794c40062c6c821a037236cc63.png)
启动还是报错,这就很尴尬了,算了直接配置 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](https://i-blog.csdnimg.cn/blog_migrate/c1a82529737b1dad9b69cbf9eb260986.png)
6、测试 web-all 的 index 首页
![image-20211216151206394](https://i-blog.csdnimg.cn/blog_migrate/aa7bc9492f0282bae8e3297681b3135c.png)
后台,先来到切面,pjp 获取目标方法的参数,没有参数
往下继续
step into 到 parseExpression 中
此时的 value 就是动态获取的方法名
继续往后走的时候发现
① 转换有 bug
② debug GmallCacheAspect
从缓存中拿的 JSON 数据没问题
![image-20211216152629146](https://i-blog.csdnimg.cn/blog_migrate/3efcddb01754bbd5ce493e8a0af19be6.png)
![image-20211216153230047](https://i-blog.csdnimg.cn/blog_migrate/dcfd932d24800b550c78a80548103cd1.png)
![image-20211216153333959](https://i-blog.csdnimg.cn/blog_migrate/a091f2ba1ef562db608099761851337e.png)
ArrayList 中的数据是一个 Map
![image-20211216153421055](https://i-blog.csdnimg.cn/blog_migrate/1232f489c9427bb82ca6c0673599c3ec.png)
所以从缓存中读取数据的时候要使用带泛型的 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
genericReturnType 的类型