谷粒商城 Day06 商品详情页web&缓存简介

Day06 商品详情页web&缓存简介

一、查询商品详情页流程图

谷粒商城 Day06 商品详情页web&缓存简介_第1张图片

分布式下使用本地Map作为缓存主要以下问题

1、数据一致性问题

2、缓存失效问题

​ 缓存在某些时刻并没有缓解数据库压力。全发给数据库,导致请求堆积问题,造成服务雪崩

思路:

虽然咱们实现了页面需要的功能,但是考虑到该页面是被用户高频访问的,所以性能需要优化。

一般一个系统最大的性能瓶颈,就是数据库的io操作。从数据库入手也是调优性价比最高的切入点。

一般分为两个层面,一是提高数据库sql本身的性能,二是尽量避免直接查询数据库。

重点要讲的是另外一个层面:尽量避免直接查询数据库。

解决办法就是:缓存

数据尽量不“回源(回到MySQL查)

谷粒商城 Day06 商品详情页web&缓存简介_第2张图片

二、答疑

2、springboot中经常提到的“类路径classpath”具体指的是哪个路径?

开发期间:

谷粒商城 Day06 商品详情页web&缓存简介_第3张图片

打包后:

谷粒商城 Day06 商品详情页web&缓存简介_第4张图片

三、feign

未来我们的调用过程应该是这样的:

web-all(需要 service-item 返回的数据)=> service-item(组装后面的数据)=> service-product(4、5个接口)

1、service-item 获取 sku 详情信息

@RestController
@RequestMapping("/api/item")
public class SkuInfoController {
    @Autowired
    SpuFeignClient spuFeignClient;

    @Autowired
    ItemService itemService;
    
     /**
     * 获取sku详情信息:聚合接口
     *
     * @param skuId
     * @return
     */
    @GetMapping("/{skuId}")
    public Result getItem(@PathVariable Long skuId) {
        // item-service 远程调用 service-product 获取当前商品的所有详情
        Map<String, Object> skuInfo = itemService.getSkuInfo(skuId);
        return Result.ok(skuInfo);
    }
}

2、TODO RPC 查询 skuDetail

//TODO RPC 查询  skuDetail
//TODO 1、Sku基本信息 以及 所有sku图片
//TODO 2,Sku图片信息(sku的默认图片[sku_info],sku_image[一组图片])
//TODO 3,Sku分类信息(sku_info[只有三级分类],根据这个三级分类查出所在的一级,二级分类内容,连上三张分类表继续查)
//TODO 4,Sku销售属性相关信息(查出自己的sku组合,还要查出这个sku所在的spu定义了的所有销售属性和属性值)
//TODO 5,Sku价格信息(平台可以单独修改价格,sku后续会放入缓存,为了回显最新价格,所以单独获取)

3、service-feign-client

新建 service-feign-client 项目

谷粒商城 Day06 商品详情页web&缓存简介_第5张图片

① SkuInfoFeignClient

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

把 service-product 的 api 包下的 ProductApiController 中写好的远程接口暴露到 SkuInfoFeignClient 中(复制 + 粘贴)

@RequestMapping("/api/product")
@FeignClient(value = "service-product",fallback = SkuInfoFeignClientFallback.class)
public interface SkuInfoFeignClient {

    @GetMapping("/inner/getSkuInfo/{skuId}")
    SkuInfo getSkuInfo(@PathVariable("skuId") Long skuId);

    @GetMapping("/inner/getCategoryView/{category3Id}")
    BaseCategoryView getCategoryView(@PathVariable("category3Id") Long category3Id);

    @GetMapping("/inner/getSkuPrice/{skuId}")
    BigDecimal getSkuPrice(@PathVariable Long skuId);

    @GetMapping("/inner/getSpuSaleAttrListCheckBySku/{skuId}/{spuId}")
    List<SpuSaleAttr> getSpuSaleAttrListCheckBySku(@PathVariable("skuId") Long skuId,
                                                          @PathVariable("spuId") Long spuId);
    @GetMapping("/inner/getSkuValueIdsMap/{spuId}")
    Map getSkuValueIdsMap(@PathVariable("spuId") Long spuId);
}

② SkuInfoFeignClientFallback

新建 com.atguigu.gmall.feign.product.impl.SkuInfoFeignClientFallback

兜底的熔断类

@Component
public class SkuInfoFeignClientFallback implements SkuInfoFeignClient {
    @Override
    public SkuInfo getSkuInfo(Long skuId) {
        return null;
    }

    @Override
    public BaseCategoryView getCategoryView(Long category3Id) {
        return null;
    }

    @Override
    public BigDecimal getSkuPrice(Long skuId) {
        return null;
    }

    @Override
    public List<SpuSaleAttr> getSpuSaleAttrListCheckBySku(Long skuId, Long spuId) {
        return null;
    }

    @Override
    public Map getSkuValueIdsMap(Long spuId) {
        return null;
    }
}

③ feign 的调用原理

谷粒商城 Day06 商品详情页web&缓存简介_第6张图片

4、service-item

做好 feign 相关配置

① pom.xml

        <dependency>
            <groupId>com.atguigu.gmallgroupId>
            <artifactId>service-feign-clientartifactId>
            <version>1.0version>
        dependency>

此时 feign 的调用是不过网关的

顺便把 VM options 配置一下

谷粒商城 Day06 商品详情页web&缓存简介_第7张图片

② 启动测试

(1)出现问题 1

Consider defining a bean of type ‘com.atguigu.gmall.feign.product.SpuFeignClient’ in your configuration.

谷粒商城 Day06 商品详情页web&缓存简介_第8张图片

谷粒商城 Day06 商品详情页web&缓存简介_第9张图片

springboot 启动只能扫到它所在的包,以及子包下的东西,以前还没有把 feign 抽取出来而是放在 service-item 中的时候,它是可以被 springboot 扫描到的,而现在根本就不在同一个包下,当然就扫描不到了

(2)解决问题 1

抽取feign以后,每个微服务要引用,要自己声明feign所在的包

ItemApplication

所有和商品有关的数据 由service-product来做的,我们不操作,远程调用

//抽取feign以后,每个微服务要引用,要自己声明feign所在的包
@EnableFeignClients(basePackages = "com.atguigu.gmall.feign.product")  //开启feign远程调用
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ItemApplication {
    public static void main(String[] args) {
        SpringApplication.run(ItemApplication.class,args);
    }
}
(3)出现问题 2

谷粒商城 Day06 商品详情页web&缓存简介_第10张图片

谷粒商城 Day06 商品详情页web&缓存简介_第11张图片

(4)解决问题 2

application.yaml

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

spring:
  main:
    allow-bean-definition-overriding: true  #允许bean定义信息的重写
  zipkin:
    base-url: http://192.168.200.188:9411/
    sender:
      type: web
(5)问题 2 的原理
谷粒商城 Day06 商品详情页web&缓存简介_第12张图片

包名重复了,为了解决这个问题,要么就在 application.yaml 中允许 bean 的重新定义,要么就把包的路径名设置得不同

成功

谷粒商城 Day06 商品详情页web&缓存简介_第13张图片

③ 小总结

/**
 * 所有和商品有关的数据 由service-product来做的,我们不操作,远程调用
 * 1、feign-client被抽取以后一定用  @EnableFeignClients(basePackages = "com.atguigu.gmall.product")
 * 2、还要加
 * spring:
 *   main:
 *     allow-bean-definition-overriding: true  #允许bean定义信息的重写
 *
 * 以后任意项目起报名
 *      com.atguigu.gmall.模块.mvc三层包
 */

5、完成 TODO RPC 查询 skuDetail

① ItemServiceImpl

@Service
@Slf4j
public class ItemServiceImpl implements ItemService {
    
    @Autowired
    SkuInfoFeignClient skuInfoFeignClient;
    
    // RPC 查询  skuDetail
    @Override
    public HashMap<String, Object> getFromServiceItemFeign01(Long skuId) {
        
        // 准备一个 map 存储要返回的值
        HashMap<String, Object> result = new HashMap<>();
        
        // 1、Sku基本信息 以及 所有sku图片
        // 2,Sku图片信息(sku的默认图片[sku_info],sku_image[一组图片])
        SkuInfo skuInfo = skuInfoFeignClient.getSkuInfo(skuId);
        
        if (skuInfo != null) {
            result.put("skuInfo", skuInfo);

            // 3,Sku分类信息(sku_info[只有三级分类],根据这个三级分类查出所在的一级,二级分类内容,连上三张分类表继续查)
            BaseCategoryView skuCategorys = skuInfoFeignClient.getCategoryView(skuInfo.getCategory3Id());
            result.put("categoryView", skuCategorys);

            // 4,Sku销售属性相关信息(查出自己的sku组合,还要查出这个sku所在的spu定义了的所有销售属性和属性值)
            List<SpuSaleAttr> spuSaleAttrListCheckBySku = skuInfoFeignClient.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId());
            result.put("spuSaleAttrList", spuSaleAttrListCheckBySku);

            // 5,Sku价格信息(平台可以单独修改价格,sku后续会放入缓存,为了回显最新价格,所以单独获取)
            BigDecimal skuPrice = skuInfoFeignClient.getSkuPrice(skuId);
            result.put("price", skuPrice);

            // 6,Spu下面的所有存在的sku组合信息{"121|123|156":65,"122|123|111":67}
            // 前端这里还需要把map转成json字符串
            Map map = skuInfoFeignClient.getSkuValueIdsMap(skuInfo.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);
            }
        }
        return result;
    }
}

② 测试

谷粒商城 Day06 商品详情页web&缓存简介_第14张图片

谷粒商城 Day06 商品详情页web&缓存简介_第15张图片

谷粒商城 Day06 商品详情页web&缓存简介_第16张图片

四、web-all

1、创建 web-all 模块

① bootstrap.properties

server.port=10000
spring.application.name=web-all
spring.cloud.nacos.server-addr=192.168.200.188:8848

② application.yaml

spring:
  zipkin:
    base-url: http://192.168.200.188:9411/
    sender:
      type: web
  main:
    allow-bean-definition-overriding: true
  thymeleaf:
    cache: false
    servlet:
      content-type: text/html
    encoding: UTF-8
    mode: HTML5
    prefix: classpath:/templates/
    suffix: .html

③ ServiceWebApplication

新建 com.atguigu.gmall.web.all.ServiceWebApplication

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ServiceWebApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceWebApplication.class, args);
    }
}

④ 导入 web-all 的前端所有数据

谷粒商城 Day06 商品详情页web&缓存简介_第17张图片 谷粒商城 Day06 商品详情页web&缓存简介_第18张图片

⑤ pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-thymeleafartifactId>
        dependency>
    dependencies>

2、获取商品详情数据完成

① SkuItemFeignClient

新建 com.atguigu.gmall.feign.item.SkuItemFeignClient

@RequestMapping("/api/item")
@FeignClient("service-item")
public interface SkuItemFeignClient {

    @GetMapping("/{skuId}")
    Result getItem(@PathVariable Long skuId);
}

② pom.xml

service/web-all/pom.xml

    <dependency>
        <groupId>com.atguigu.gmallgroupId>
        <artifactId>service-feign-clientartifactId>
        <version>1.0version>
    dependency>

③ ServiceWebApplication

给启动类加上 @EnableFeignClients(“com.atguigu.gmall.feign.item”),把 item 扫描进来

@EnableFeignClients("com.atguigu.gmall.feign.item")
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ServiceWebApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceWebApplication.class, args);
    }
}

④ SkuItemController

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

因为要跳转页面,所以不用 @RestController

@Controller
public class SkuItemController {

    @Autowired
    SkuItemFeignClient skuItemFeignClient;

    @GetMapping("/{skuId}.html")
    public String getSkuInfo(@PathVariable("skuId") Long skuId,
                             Model model){

        //把当前sku的所有数据交给页面
        Result<Map<String,Object>> item = skuItemFeignClient.getItem(skuId);

        Map<String,Object> data = item.getData();

        //把远程调用得到的所有内容全部交给 model,放在页面的请求域中
        model.addAllAttributes(data);
        return "item/index";
    }
}

为什么要用 item.getData()?为什么要把 Result item = skuItemFeignClient.getItem(skuId);转换成Result> item = skuItemFeignClient.getItem(skuId);

Result item = skuItemFeignClient.getItem(skuId);

因为原来的返回值类型是 Result,里面是 code、message、data,其中 code、message 没啥用,我们这里只需要 data,而 Map skuInfo = itemService.getSkuInfo(skuId);所以自然而然就转换成Map

⑤ 测试效果

谷粒商城 Day06 商品详情页web&缓存简介_第19张图片

谷粒商城 Day06 商品详情页web&缓存简介_第20张图片

⑥ 跳转 bug(前面实际已解决)

当我们点击切换其他颜色或套餐的时候并没有成功跳转

谷粒商城 Day06 商品详情页web&缓存简介_第21张图片

前端解析出来的:

谷粒商城 Day06 商品详情页web&缓存简介_第22张图片

我们实际查到的

谷粒商城 Day06 商品详情页web&缓存简介_第23张图片

解决方法:

封装 SkuAllSaleValue 时就用 String 定义,不要用 Long

谷粒商城 Day06 商品详情页web&缓存简介_第24张图片 谷粒商城 Day06 商品详情页web&缓存简介_第25张图片

五、网关-使用域名的方式转发

谷粒商城 Day06 商品详情页web&缓存简介_第26张图片

1、更改网关配置

① bootstrap.properties

spring.application.name=api-gateway
server.port=80
spring.cloud.nacos.server-addr=192.168.200.188:8848

② 给主机配地址映射

谷粒商城 Day06 商品详情页web&缓存简介_第27张图片

访问这些域名都会来到 192.168.200.1(主机目录),意味着所有的请求都会发到主机目录的 80 端口

那么既然主机的 80 端口接到了,为什么会是 404

我们先想清楚,这个页面是谁给我们返回的?

这是网关给我们返回的,因为 item.gmall.com 来到本机,本机只有网关监听了 80 端口,但是你现在又要访问别人,所以就要在网关的 application.yaml 配置好你要访问的映射路径

③ application.yaml

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: "*"
            allowedMethods: "*"
            allowedHeaders: "*"  #允许复杂请求跨域
            allowCredentials: true  #允许跨域带cookie

      routes:
        - id: admin-product
          uri: lb://service-product
          predicates:
            - Path=/admin/product/**
        - id: web-all
          uri: lb://web-all
          predicates:
            - Host=item.gmall.com

item.gmall.com 这个域名下的所有请求都路由到 web-all

为啥呢?

例子:当我们访问 item.gmall.com/50.html 时,在请求头里面有 Host 主机:item.gmall.com,所以我们网关收到这个请求的时候,会拿到请求头中的字段,看如果是 item.gmall.com 时就给负载均衡到了 web-all

谷粒商城 Day06 商品详情页web&缓存简介_第28张图片

④ 测试

谷粒商城 Day06 商品详情页web&缓存简介_第29张图片

链路通了

六、缓存

1、高并发小口诀

高并发(吞吐量)?有三宝

缓存(分担数据库压力)、”缓存是否高并发的银弹???”

异步(结合线程池)、

队排好(消息队列)

2、思路

虽然咱们实现了页面需要的功能,但是考虑到该页面是被用户高频访问的,所以性能需要优化。

一般一个系统最大的性能瓶颈,就是数据库的io操作。从数据库入手也是调优性价比最高的切入点。

一般分为两个层面,一是提高数据库sql本身的性能,二是尽量避免直接查询数据库。

重点要讲的是另外一个层面:尽量避免直接查询数据库。

解决办法就是:缓存

数据尽量不“回源(回到MySQL查)”

3、压力测试

① 未加缓存的初始情况

使用ab测试工具:httpd-tools(安装:yum install -y httpd-tools)

ab  -n(一次发送的请求数)  -c(请求的并发数) 访问路径

测试如下:5000请求,100并发

ab  -n 5000 -c 100 http://192.168.200.1:9000/api/item/50
谷粒商城 Day06 商品详情页web&缓存简介_第30张图片 谷粒商城 Day06 商品详情页web&缓存简介_第31张图片
#压力测试?
ab -n 5000 -c 100 http://192.168.200.1:9000/api/item/50

Server Software:        
Server Hostname:        192.168.200.1
Server Port:            9000

Document Path:          /api/item/50
Document Length:        2491 bytes

Concurrency Level:      100
Time taken for tests:   10.667 seconds   #总耗费时间
Complete requests:      5000
Failed requests:        0
Write errors:           0
Total transferred:      12980000 bytes
HTML transferred:       12455000 bytes
Requests per second:    468.72 [#/sec] (mean)    #每秒并发               V
Time per request:       213.347 [ms] (mean)      #平均请求耗费的时间       V
Time per request:       2.133 [ms] (mean, across all concurrent requests)
Transfer rate:          1188.28 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   1.5      1      13    #方差越大,代表网络波动越大
Processing:    39  210  69.0    197     724
Waiting:       32  210  68.8    196     708
Total:         39  212  69.6    198     728

Percentage of the requests served within a certain time (ms)
  50%    198
  66%    217
  75%    233
  80%    244
  90%    282      #90%的请求都在282ms内处理完成     V
  95%    331
  98%    420
  99%    528      #99%的请求在528ms内处理完成       V
 100%    728 (longest request)

② 压力测试记录

项目 并发指标 响应速度指标
service-item 468.72/s 90% 282
99% 528
service-product
域名访问商品详情 253.99/s 90% 542
99% 722
加本地缓存:service-item 3258/s
map要比redis
90% 23
99% 49
加分布式缓存:service-item,所有数据存到redis 4096.75/s 90% 32
99% 208

1、压测域名多过了一层网关

网关 — 微服务

2、直接压微服务

微服务

中间件过的越多,速度越慢,并发越小

③ ItemServiceImpl 加本地缓存

ItemServiceImpl

@Service
@Slf4j
public class ItemServiceImpl implements ItemService {
    
    // 临时保存数据,本地缓存
	private Map<String,Object> cache = new HashMap<>();
    
    @Autowired
    SkuInfoFeignClient skuInfoFeignClient;
    
    // RPC 查询  skuDetail
    @Override
    public HashMap<String, Object> getFromServiceItemFeign(Long skuId) {
        
        /**
 		* 1、所有查询之前,先看缓存
 		*   1.1)、如果缓存中有就使用缓存的
 		*   1.2)、如果缓存没有,就查数据库
 		*/
        
        if (cache.get("data") != null) {
            // TODO 缓存中有
            return (HashMap<String, Object>) cache.get("data"); // 强转 (HashMap)
        } else {
            // 缓存中没有
            HashMap<String, Object> result = new HashMap<>();
            //skuInfo信息

            //RPC 查询  skuDetail
            // 1、Sku基本信息(名字,id,xxx,价格,sku_描述) sku_info
            // 2,Sku图片信息(sku的默认图片[sku_info],sku_image[一组图片
            SkuInfo skuInfo = skuInfoFeignClient.getSkuInfo(skuId);
            if (skuInfo != null) {
                result.put("skuInfo", skuInfo);

                // 3,Sku分类信息(sku_info[只有三级分类],根据这个三级分类查出所在的一级,二级分类内容,连上三张分类表继续查)
                BaseCategoryView skuCategorys = skuInfoFeignClient.getCategoryView(skuInfo.getCategory3Id());
                result.put("categoryView", skuCategorys);

                // 4,Sku销售属性相关信息(查出自己的sku组合,还要查出这个sku所在的spu定义了的所有销售属性和属性值)
                List<SpuSaleAttr> spuSaleAttrListCheckBySku = skuInfoFeignClient.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId());
                result.put("spuSaleAttrList", spuSaleAttrListCheckBySku);

                // 5,Sku价格信息(平台可以单独修改价格,sku后续会放入缓存,为了回显最新价格,所以单独获取)
                BigDecimal skuPrice = skuInfoFeignClient.getSkuPrice(skuId);
                result.put("price", skuPrice);

                // 6,Spu下面的所有存在的sku组合信息{"121|123|156":65,"122|123|111":67}
                //前端这里还需要把map转成json字符串
                Map map = skuInfoFeignClient.getSkuValueIdsMap(skuInfo.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);
                }
            }
            // TODO 缓存没有,查到数据再放入缓存
            cache.put("data",result);
            return result;
        }
    }
}

加本地缓存后压 service-item 的效果

Server Software:        
Server Hostname:        192.168.200.1
Server Port:            9000

Document Path:          /api/item/50
Document Length:        2491 bytes

Concurrency Level:      100
Time taken for tests:   1.535 seconds
Complete requests:      5000
Failed requests:        0
Write errors:           0
Total transferred:      12980000 bytes
HTML transferred:       12455000 bytes
Requests per second:    3258.29 [#/sec] (mean)
Time per request:       30.691 [ms] (mean)
Time per request:       0.307 [ms] (mean, across all concurrent requests)
Transfer rate:          8260.26 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5   2.7      5      17
Processing:     2   10  13.4      7     802
Waiting:        2    9  13.0      6     790
Total:          6   15  13.5     12     803

Percentage of the requests served within a certain time (ms)
  50%     12
  66%     15
  75%     17
  80%     19
  90%     23
  95%     28
  98%     41
  99%     49

4、本地缓存

谷粒商城 Day06 商品详情页web&缓存简介_第32张图片

本地缓存模式在分布式下的问题

谷粒商城 Day06 商品详情页web&缓存简介_第33张图片

分布式下使用本地Map作为缓存主要以下问题

1、数据一致性问题

2、缓存失效问题
缓存在某些时刻并没有缓解数据库压力。全发给数据库,导致请求堆积问题,造成服务雪崩

5、分布式缓存

① 图解

谷粒商城 Day06 商品详情页web&缓存简介_第34张图片

② 缓存的使用模式

(1)读模式(伪代码)
// 伪代码
if(redisCache.get(“hello”)) {
	Return redisCache.get(“hello”)
} else {
	// 缓存没数据
	Synchronized(this){
		// 双检查
		if(!redisCache.get(“hello”) {
			Object data = queryFromDb();
			redisCache.put(“hello”,data );
		} else {
 			Return redisCache.get(“hello”);
		}
	}
	// 把结果保存到缓存
  	redisCache.put(“hello”,data );
}
(2)写模式(伪代码)
// 改一条数据?
   UpdateXXX(Long dataId){
      //1、修改数据库
      updateDb(dataId);
      //2、更新缓存
      // 2.1)、双写模式:把刚才的最新数据查出来,再放到缓存,覆盖之前的结果
      // 2.2)、失效模式:redisCache.delete(dataId),删除redis中的数据,让下一次的查询自己再查一遍
}

③ 高并发下缓存失效问题

(1)缓存穿透
谷粒商城 Day06 商品详情页web&缓存简介_第35张图片
(2)缓存击穿
谷粒商城 Day06 商品详情页web&缓存简介_第36张图片
(3)缓存雪崩
谷粒商城 Day06 商品详情页web&缓存简介_第37张图片

④ 缓存使用的小总结

(1)解决缓存失效的方法

1、null结果也应该缓存:缓存穿透

2、缓存的数据都加上过期时间,防止缓存没有被代码更新,数据一直是错误的

3、查询数据库要加锁

4、把所有数据的失效时间设置成随机的【不用做】 30min; 只要加了失效时间就行不用随机

着重注意两点:

1、加锁(解决击穿、雪崩(录入数据的时间本就不一样,时间维度扩散的,只需要解决击穿)?)

2、数据有过期时间(数据过期一定要加)

(2)哪些数据放到缓存

1、查多改少【菜单、sku、spu、优惠券】

2、热点(查多)

3、一致性要求不高【指不用强一致,缓存和数据库的数据是弱一致的】

​ 强一致: 分布式系统 Raft, 数据改完以后,都是同步的。MySQL - Redis 同步的

​ 弱一致: 最终一致性。redis的数据最终和mysql的是一致的就行

(3)哪些数据不适合放缓存

1、改多

2、和钱相关

3、库存(实时校验库存)

sku的销量saleCount? 销量即使数据库变了,缓存没有更新过来无所谓

sku数据在缓存中有。看销量(对用户不重要,每个月进行账单统计的时候才需要精确查)?

6、整合 redis

在 service 中引入缓存中间件

① service-pom.xml


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

② application.yaml

server:
  port: 9000

#怎么抽取全微服务都能用
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

③ SpringBoot 对 Redis 做了哪些配置?

RedisAutoConfiguration

​ RedisTemplate object = 给 redis 存的是 json

​ StringRedisTemplate: extends RedisTemplate 用这个

④ 测试

RedisTempalteTest

新建 com.atguigu.gmall.item.test.RedisTempalteTest

@SpringBootTest
public class RedisTempalteTest {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Test
    void test01(){
        ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
        stringStringValueOperations.set("hello","world");
        String hello = stringStringValueOperations.get("hello");
        System.out.println("查到的值" + hello);
    }
}
谷粒商城 Day06 商品详情页web&缓存简介_第38张图片 谷粒商城 Day06 商品详情页web&缓存简介_第39张图片

7、整合缓存

先考虑简化业务逻辑,把整个业务逻辑查询好的一堆东西都缓存,而不是挨个缓存

① ItemServiceImpl 加 redis

(1)抽取 getFromServiceItemFeign
谷粒商城 Day06 商品详情页web&缓存简介_第40张图片 谷粒商城 Day06 商品详情页web&缓存简介_第41张图片
(2)ItemServiceImpl
@Service
@Slf4j
public class ItemServiceImpl implements ItemService {
    
    @Autowired
    SkuInfoFeignClient skuInfoFeignClient;
    
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    
    /**
    * 远程调用 service-product 查询出和当前 skuId 对应的所有信息
    * 缓存的使用逻辑是固定,我们完全可以抽取
    * 
    * 就可以使用AOP直接抽取出来,数据改了,缓存也要改?  如何使用SpringCache简化缓存开发
    *
    * @param skuId
    * @return
    */
	@Override
    public Map<String, Object> getSkuInfo(Long skuId) {
        ObjectMapper mapper = new ObjectMapper();
        
        /**
         * 1、所有查询之前,先看缓存
         *   1.1)、如果缓存中有就使用缓存的
         *   1.2)、如果缓存没有,就查数据库
         */
        ValueOperations<String, String> operations = stringRedisTemplate.opsForValue();
        
        //1、先去缓存确定是否存在
        String redisContent = operations.get("sku:info:" + skuId);
        if (StringUtils.isEmpty(redisContent)){
            //2、缓存中没有,远程查询
            HashMap<String, Object> fromServiceItemFeign = getFromServiceItemFeign(skuId);

            String jsonStr = null;
            try {
                //对象——>String:序列化
                jsonStr = mapper.writeValueAsString(fromServiceItemFeign);
            } catch (JsonProcessingException e) {
                log.error("skuId 序列化异常:{}",e);
            }
            //2.1、给redis中存一份
            operations.set("sku:info:"+skuId,jsonStr);

            //2.2、返回数据
            return fromServiceItemFeign;
        }else {
            Map<String, Object> stringObjectMap = null;
            try {
                // redis拿到的是string,还要转对象
                // 反序列化
                // 是 fasterxml.jackson 包下的 TypeReference
                stringObjectMap = mapper.readValue(redisContent, new TypeReference<Map<String, Object>>() {
                });
            } catch (JsonProcessingException e) {
                log.error("map 反序列化异常:{}",e);
            }
            return stringObjectMap;
        }
    }
    
	/**
     * 远程查询sku详细信息(01版)
     *
     * @param skuId
     * @return
     */
    private HashMap<String, Object> getFromServiceItemFeign(Long skuId) {
        log.info("开始远程查询,远程会操作数据库-------");

        HashMap<String, Object> result = new HashMap<>();
        //skuInfo信息

        //RPC 查询  skuDetail
        // 1、Sku基本信息(名字,id,xxx,价格,sku_描述) sku_info
        // 2,Sku图片信息(sku的默认图片[sku_info],sku_image[一组图片
        SkuInfo skuInfo = skuInfoFeignClient.getSkuInfo(skuId);
        if (skuInfo != null) {
            result.put("skuInfo", skuInfo);

            // 3,Sku分类信息(sku_info[只有三级分类],根据这个三级分类查出所在的一级,二级分类内容,连上三张分类表继续查)
            BaseCategoryView skuCategorys = skuInfoFeignClient.getCategoryView(skuInfo.getCategory3Id());
            result.put("categoryView", skuCategorys);

            // 4,Sku销售属性相关信息(查出自己的sku组合,还要查出这个sku所在的spu定义了的所有销售属性和属性值)
            List<SpuSaleAttr> spuSaleAttrListCheckBySku = skuInfoFeignClient.getSpuSaleAttrListCheckBySku(skuId, skuInfo.getSpuId());
            result.put("spuSaleAttrList", spuSaleAttrListCheckBySku);

            // 5,Sku价格信息(平台可以单独修改价格,sku后续会放入缓存,为了回显最新价格,所以单独获取)
            BigDecimal skuPrice = skuInfoFeignClient.getSkuPrice(skuId);
            result.put("price", skuPrice);

            // 6,Spu下面的所有存在的sku组合信息{"121|123|156":65,"122|123|111":67}
            //前端这里还需要把map转成json字符串
            Map map = skuInfoFeignClient.getSkuValueIdsMap(skuInfo.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);
            }
        }
        return result;
    }
}

谷粒商城 Day06 商品详情页web&缓存简介_第42张图片

(3)测试
谷粒商城 Day06 商品详情页web&缓存简介_第43张图片
项目 并发指标 响应速度指标
service-item 468.72/s 90% 282
99% 528
service-product
域名访问商品详情 253.99/s 90% 542
99% 722
加本地缓存:service-item 3258/s
map要比redis
90% 23
99% 49
加分布式缓存:service-item,所有数据存到redis 4096.75/s 90% 32
99% 208
谷粒商城 Day06 商品详情页web&缓存简介_第44张图片

redis 官方给的数据时每秒可以有10w的并发

② 思考

缓存的使用逻辑是固定,我们完全可以抽取,使用AOP直接抽取出来

数据改了,缓存也要改?

如何使用SpringCache简化缓存开发?

③ Tomcat 调节

service-item - application.yaml
server:
  port: 9000
  tomcat:
    accept-count: 10000  # tomcat线程池的队列长度  ServerProperties.Tomcat
    threads:
      max: 5000
# IO密集型【一般都是这种】:  disk、network  淘宝  调 内存,线程池的大小
# CPU密集型【人工智能】: 计算;   内存占用不多, 线程池大小, 关注cpu

你可能感兴趣的:(谷粒商城,缓存,前端,java)