当前位置: 首页 > news >正文

微服务高级篇学习【4】之多级缓存

文章目录

  • 前言
  • 一 多级缓存
  • 二 JVM进程缓存
    • 2.1 案例导入
      • 2.1.1 使用docker安装mysql
      • 2.1.2 修改配置
      • 2.1.3 导入项目工程
      • 2.1.4 导入商品查询页面
      • 2.1.5 反向代理
    • 2.2 初识Caffeine
    • 2.3 实现JVM进程缓存
  • 三 Lua脚本入门
    • 3.1 安装Lua
    • 3.2 Lua语法学习
  • 四 实现多级缓存
    • 4.1 OpenResty简介
    • 4.2 OpenResty的安装
      • 4.2.1 安装开发库
      • 4.2.2 安装OpenResty仓库
      • 4.2.3 安装OpenResty
      • 4.2.4 安装opm工具
      • 4.2.5 目录结构
      • 4.2.6 配置nginx的环境变量
      • 4.2.7 启动和运行
    • 4.3 OpenResty快速入门
      • 4.3.1 反向代理流程
      • 4.3.2 OpenResty监听请求
      • 4.3.3 编写item.lua
    • 4.4 请求参数处理
      • 4.4.1 获取参数的API
      • 4.4.2 获取参数并返回
    • 4.5 查询Tomcat
      • 4.5.1 补充:开放windows端口
      • 4.5.2 补充:测试Windows端口开放
      • 4.5.3 补充:限制访问IP
      • 4.5.4 发送http请求的API
      • 4.5.5 封装http工具
      • 4.5.6 CJSON工具类
      • 4.5.7 实现Tomcat查询
      • 4.5.8 基于ID负载均衡
    • 4.6 Redis缓存预热
    • 4.7 查询Redis缓存
      • 4.7.1 封装Redis工具
      • 4.7.2 实现Redis查询
    • 4.8 Nginx本地缓存
      • 4.8.1 本地缓存API
      • 4.8.2 实现本地缓存查询
  • 五 缓存同步
    • 5.1 数据同步策略
    • 5.2 Canal简介
    • 5.3 安装Canal
      • 5.3.1 开启MySQL主从
        • 5.3.1.1 开启binlog
        • 5.3.1.2 设置用户权限
      • 5.3.2 安装Canal
        • 5.3.2.1 创建网络
        • 5.3.2.2 创建Canal容器
    • 5.4 监听Canal
      • 5.4.1 引入依赖
      • 5.4.2 编写配置
      • 5.4.3 修改Item实体类
      • 5.4.4 编写监听器
      • 5.5 测试

前言

  • 本文学自黑马,虽然微服务的部署确实比较麻烦,但是相信大家通过动手还是会有很多收获的!!
  • 最总架构图:
    在这里插入图片描述

一 多级缓存

  • 传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库

  • 存在下面的问题:

    • 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
    • Redis缓存失效时,会对数据库产生冲击
  • 多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能:

    • 浏览器访问静态资源时,优先读取浏览器本地缓存
    • 访问非静态资源(ajax查询数据)时,访问服务端
    • 请求到达Nginx后,优先读取Nginx本地缓存
    • 如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat)
    • 如果Redis查询未命中,则查询Tomcat
    • 请求进入Tomcat后,优先查询JVM进程缓存
    • 如果JVM进程缓存未命中,则查询数据库
      在这里插入图片描述
  • 在多级缓存架构中,Nginx内部需要编写本地缓存查询、Redis查询、Tomcat查询的业务逻辑,因此这样的nginx服务不再是一个反向代理服务器,而是一个编写业务的Web服务器。Tomcat服务将来也会部署为集群模式。

  • 多级缓存的关键有两个:

    • 一个是在nginx中编写业务,实现nginx本地缓存、Redis、Tomcat的查询
    • 另一个就是在Tomcat中实现JVM进程缓存

二 JVM进程缓存

2.1 案例导入

  • 案例导入,需要动手实践,黑马官方的文档本就很详细,大家可以按照文档进行操作,这里主要列举重要的内容。

2.1.1 使用docker安装mysql

  • 这里因为小编原来已经有一个mysql容器,所以这里将其命名为mysql2
  • 准备两个目录,用于挂载容器的数据和配置文件目录
    # 进入/tmp目录
    cd /tmp
    # 创建文件夹
    mkdir mysql
    # 进入mysql目录
    cd mysql
    
  • 进入mysql目录后,执行下面的Docker命令:
    docker run \-p 3306:3306 \--name mysql2 \-v $PWD/conf:/etc/mysql/conf.d \-v $PWD/logs:/logs \-v $PWD/data:/var/lib/mysql \-e MYSQL_ROOT_PASSWORD=123 \--privileged \-d \mysql:5.7.25
    

2.1.2 修改配置

  • 在/tmp/mysql/conf目录添加一个my.cnf文件,作为mysql的配置文件:
    # 创建文件
    touch /tmp/mysql/conf/my.cnf
    
  • 文件的内容如下:
    [mysqld]
    skip-name-resolve
    character_set_server=utf8
    datadir=/var/lib/mysql
    server-id=1000
    
  • 配置修改后,必须重启容器:
    docker restart mysql2
    

2.1.3 导入项目工程

  • 项目,注意修改application.yml文件中配置的mysql地址信息
  • 访问:http://localhost:8081/item/10001即可查询数据
    在这里插入图片描述

2.1.4 导入商品查询页面

  • 需要准备一个反向代理的nginx服务器,如上图红框所示,将静态的商品页面放到nginx目录中
    在这里插入图片描述
  • 使用黑马官方提供的nginx资料,运行命令:
    start nginx.exe
    
  • 然后访问 http://localhost/item.html?id=10001
    在这里插入图片描述

2.1.5 反向代理

  • 页面是假数据展示的。我们需要向服务器发送ajax请求,查询商品数据。打开控制台,可以看到页面有发起ajax查询数据:
    在这里插入图片描述
  • 而这个请求地址同样是80端口,所以被当前的nginx反向代理了。查看nginx的conf目录下的nginx.conf文件:
  • 其中的关键配置如下:
    在这里插入图片描述
  • 192.168.188.112是虚拟机IP,也就是Nginx业务集群要部署的地方
  • 完整内容如下:
    #user  nobody;
    worker_processes  1;events {worker_connections  1024;
    }http {include       mime.types;default_type  application/octet-stream;sendfile        on;#tcp_nopush     on;keepalive_timeout  65;upstream nginx-cluster{server 192.168.188.112:8081;}server {listen       80;server_name  localhost;location /api {proxy_pass http://nginx-cluster;}location / {root   html;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}}
    }
    

2.2 初识Caffeine

缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:

  • 分布式缓存,例如Redis:
    • 优点:存储容量更大、可靠性更好、可以在集群间共享
    • 缺点:访问缓存有网络开销
    • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
  • 进程本地缓存,例如HashMap、GuavaCache:
    • 优点:读取本地内存,没有网络开销,速度更快
    • 缺点:存储容量有限、可靠性较低、无法共享
    • 场景:性能要求较高,缓存数据量较小

  • 可以利用Caffeine框架来实现JVM进程缓存
    Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址
  • Caffeine的性能非常好,下图是官方给出的性能对比:
    在这里插入图片描述

  • 缓存使用的基本API:
    @Test
    void testBasicOps() {// 构建cache对象Cache<String, String> cache = Caffeine.newBuilder().build();// 存数据cache.put("gf", "热巴");// 取数据String gf = cache.getIfPresent("gf");System.out.println("gf = " + gf);// 取数据,包含两个参数:// 参数一:缓存的key// 参数二:Lambda表达式,表达式参数就是缓存的key,方法体是查询数据库的逻辑// 优先根据key查询JVM缓存,如果未命中,则执行参数二的Lambda表达式String defaultGF = cache.get("defaultGF", key -> {// 根据key去数据库查询数据return "柳岩";});System.out.println("defaultGF = " + defaultGF);
    }
    

  • Caffeine既然是缓存的一种,肯定需要有缓存的清除策略,防止内存有耗尽。Caffeine提供了三种缓存驱逐策略:

  • 基于容量:设置缓存的数量上限

    // 创建缓存对象
    Cache<String, String> cache = Caffeine.newBuilder().maximumSize(1) // 设置缓存大小上限为 1.build();
    
  • 基于时间:设置缓存的有效时间

    // 创建缓存对象
    Cache<String, String> cache = Caffeine.newBuilder()// 设置缓存有效期为 10 秒,从最后一次写入开始计时 .expireAfterWrite(Duration.ofSeconds(10)) .build();
    
  • 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

注意:在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。

2.3 实现JVM进程缓存

利用Caffeine实现下列需求:

  • 给根据id查询商品的业务添加缓存,缓存未命中时查询数据库
  • 给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库
  • 缓存初始大小为100
  • 缓存上限为10000
    在这里插入图片描述

  • 定义两个Caffeine的缓存对象,分别保存商品、库存的缓存数据。
    package com.heima.item.config;
    @Configuration
    public class CaffeineConfig {@Beanpublic Cache<Long, Item> itemCache(){return Caffeine.newBuilder().initialCapacity(100) //初始化大小为100.maximumSize(10_000) //缓存上限为10_000.build();}@Beanpublic Cache<Long, ItemStock> stockCache(){return Caffeine.newBuilder().initialCapacity(100).maximumSize(10_000).build();}
    }
    
  • 修改ItemController类,添加缓存逻辑:
    @RestController
    @RequestMapping("item")
    public class ItemController {@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService stockService;@Autowiredprivate Cache<Long, Item> itemCache;@Autowiredprivate Cache<Long, ItemStock> stockCache;// ...其它略@GetMapping("/{id}")public Item findById(@PathVariable("id") Long id) {return itemCache.get(id, key -> itemService.query().ne("status", 3).eq("id", key).one());}@GetMapping("/stock/{id}")public ItemStock findStockById(@PathVariable("id") Long id) {return stockCache.get(id, key -> stockService.getById(key));}
    }
    

三 Lua脚本入门

  • Nginx编程需要用到Lua语言。Lua经常嵌入到C语言开发的程序中,例如游戏开发、游戏插件等。Nginx本身也是C语言开发,因此也允许基于Lua做拓展。
  • Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

3.1 安装Lua

  • Lua脚本【环境准备】

3.2 Lua语法学习

  • Lua语法学习

四 实现多级缓存

  • 多级缓存的实现离不开Nginx编程,而Nginx编程又离不开OpenResty。

4.1 OpenResty简介

在这里插入图片描述
OpenResty® 是一个基于 Nginx的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点:

  • 具备Nginx的完整功能
  • 基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块
  • 允许使用Lua自定义业务逻辑自定义库

4.2 OpenResty的安装

  • 前提:Linux虚拟机必须联网

4.2.1 安装开发库

  • 首先要安装OpenResty的依赖开发库,执行命令:
    yum install -y pcre-devel openssl-devel gcc --skip-broken
    

4.2.2 安装OpenResty仓库

  • 在 CentOS 系统中添加 openresty 仓库,便于未来安装或更新软件包(通过 yum check-update 命令)。运行下面的命令就可以添加我们的仓库:
    yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
    
    在这里插入图片描述
    在这里插入图片描述
  • 如果提示说命令不存在,则运行以下命令,然后再重复上面的命令:
    yum install -y yum-utils 
    
    在这里插入图片描述

4.2.3 安装OpenResty

  • 安装软件包
    yum install -y openresty
    

在这里插入图片描述
在这里插入图片描述

4.2.4 安装opm工具

  • opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。
  • 安装命令行工具 opm,可以这样安装 openresty-opm 包:
    yum install -y openresty-opm
    
    在这里插入图片描述
    在这里插入图片描述

4.2.5 目录结构

  • 默认情况下,OpenResty安装的目录是:/usr/local/openresty看到里面的nginx目录,OpenResty就是在Nginx基础上集成了一些Lua模块。
    在这里插入图片描述

4.2.6 配置nginx的环境变量

  • 打开配置文件:

    vi /etc/profile
    
  • 在最下面加入两行:

    export NGINX_HOME=/usr/local/openresty/nginx
    export PATH=${NGINX_HOME}/sbin:$PATH
    
    • NGINX_HOME:后面是OpenResty安装目录下的nginx的目录
      在这里插入图片描述
  • 然后让配置生效:

    source /etc/profile
    

4.2.7 启动和运行

  • OpenResty底层是基于Nginx的,查看OpenResty目录的nginx目录,结构与windows中安装的nginx基本一致,所以运行方式与nginx基本一致:
    # 启动nginx
    nginx
    # 重新加载配置
    nginx -s reload
    # 停止
    nginx -s stop
    

在这里插入图片描述

  • nginx的默认配置文件注释太多,影响后续编辑,这里将nginx.conf中的注释部分删除,保留有效部分。修改/usr/local/openresty/nginx/conf/nginx.conf文件,内容如下:

    #user  nobody;
    worker_processes  1;
    error_log  logs/error.log;events {worker_connections  1024;
    }http {include       mime.types;default_type  application/octet-stream;sendfile        on;keepalive_timeout  65;server {listen       8081;server_name  localhost;location / {root   html;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}}
    }
    
  • 在Linux的控制台输入命令以启动nginx:

    nginx
    
  • 然后访问页面:http://192.168.188.112:8081,注意ip地址替换为你自己的虚拟机IP:

在这里插入图片描述

4.3 OpenResty快速入门

  • 多级缓存架构如图:
    在这里插入图片描述
  • 其中:
    • windows上的nginx用来做反向代理服务,将前端的查询商品的ajax请求代理到OpenResty集群
    • OpenResty集群用来编写多级缓存业务

4.3.1 反向代理流程

  • 商品详情页使用的是假的商品数据。不过在浏览器中,可以看到页面有发起ajax请求查询真实商品数据。
    在这里插入图片描述
  • 请求地址是localhost,端口是80,就被windows上安装的Nginx服务给接收到了。然后代理给了OpenResty集群:
    在这里插入图片描述
  • 需要在OpenResty中编写业务,查询商品数据并返回到浏览器。

4.3.2 OpenResty监听请求

  • OpenResty的很多功能都依赖于其目录下的Lua库,需要在nginx.conf中指定依赖库的目录,并导入依赖:
  1. 添加对OpenResty的Lua模块的加载
    • 修改/usr/local/openresty/nginx/conf/nginx.conf文件,在其中的http下面,添加下面代码:
    #lua 模块
    lua_package_path "/usr/local/openresty/lualib/?.lua;;";
    #c模块     
    lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  
    
  2. 监听/api/item路径
    • 修改/usr/local/openresty/nginx/conf/nginx.conf文件,在nginx.conf的server下面,添加对/api/item这个路径的监听:
    #监听,就类似于SpringMVC中的`@GetMapping("/api/item")`做路径映射
    location  /api/item {# 默认的响应类型default_type application/json;# 响应结果由lua/item.lua文件来决定content_by_lua_file lua/item.lua;
    }
    

4.3.3 编写item.lua

  1. /usr/loca/openresty/nginx目录创建文件夹:lua

    在这里插入图片描述

  2. /usr/loca/openresty/nginx/lua文件夹下,新建文件:item.lua
    在这里插入图片描述

  3. 编写item.lua

    ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 22寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":18800,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')
    
  4. 重新加载配置

    nginx -s reload
    
  5. 刷新商品页面:http://localhost/item.html?id=1001,即可看到效果:
    在这里插入图片描述

4.4 请求参数处理

  • 在OpenResty接收前端请求,要返回真实数据,必须根据前端传递来的商品id,查询商品信息才可以。

4.4.1 获取参数的API

  • OpenResty中提供了一些API用来获取不同类型的前端请求参数:
    在这里插入图片描述

4.4.2 获取参数并返回

  • 在前端发起的ajax请求如图:
    在这里插入图片描述
    • 可以看到商品id是以路径占位符方式传递的,因此可以利用正则表达式匹配的方式来获取ID

  1. 获取商品id
    • 修改/usr/loca/openresty/nginx/nginx.conf文件中监听/api/item的代码,利用正则表达式获取ID:
    location ~ /api/item/(\d+) {# 默认的响应类型default_type application/json;# 响应结果由lua/item.lua文件来决定content_by_lua_file lua/item.lua;
    }
    

  1. 拼接ID并返回
    • 修改/usr/loca/openresty/nginx/lua/item.lua文件,获取id并拼接到结果中返回:
    -- 获取商品id
    local id = ngx.var[1]
    -- 拼接并返回
    ngx.say('{"id":' .. id .. ',"name":"SALSA AIR","title":"RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')
    

  1. 重新加载并测试
    • 运行命令以重新加载OpenResty配置:
    nginx -s reload
    
    • 刷新页面可以看到结果中已经带上了ID:
      在这里插入图片描述

4.5 查询Tomcat

  • 架构图:
    在这里插入图片描述
    • 注意:OpenResty是在虚拟机,Tomcat是在Windows电脑上

4.5.1 补充:开放windows端口

  • 由于虚拟机nginx需要连接windows上的tomcat,所以需要开放windows的端口或者关闭防火墙
  • 这里需要开放8081和8082的端口
  1. 打开控制面->系统和安全
    在这里插入图片描述
  2. 查看防火墙的状态:开启状态
    在这里插入图片描述
  3. 开启特定的端口:高级设置->入站规则->新建规则
    • 规则类型:端口
      在这里插入图片描述
    • 协议和端口:TCP和特定端口:xxxx
      在这里插入图片描述
      • 操作:允许连接
        在这里插入图片描述
    • 配置文件:全选
      在这里插入图片描述
    • 名称:自定义
      在这里插入图片描述

4.5.2 补充:测试Windows端口开放

  • 按照下图,开启远程登录客户端
    在这里插入图片描述
    • 在win终端输入一些命令,测试:
    telnet 192.168.188.10 8081
    

4.5.3 补充:限制访问IP

  1. 右键创建的端口,打开属性,选择作用域,远程IP地址
    在这里插入图片描述

4.5.4 发送http请求的API

  • nginx提供了内部API用以发送http请求:

    local resp = ngx.location.capture("/path",{method = ngx.HTTP_GET,   -- 请求方式args = {a=1,b=2},  -- get方式传参数
    })
    
  • 返回的响应内容包括:

    • resp.status:响应状态码
    • resp.header:响应头,是一个table
    • resp.body:响应体,就是响应数据
  • 注意:这里的path是路径,并不包含IP和端口。这个请求会被nginx内部的server监听并处理。但是这个请求发送到Tomcat服务器,所以还需要编写一个server来对这个路径做反向代理:

     location /path {# 这里是windows电脑的ip和Java服务端口,需要确保windows防火墙处于关闭状态proxy_pass http://192.168.188.10:8081; }
    
  • 原理如图:
    在这里插入图片描述

4.5.5 封装http工具

  • 封装一个发送Http请求的工具,基于ngx.location.capture来实现查询tomcat。
  1. 添加反向代理,到windows的Java服务
    • 因为item-service中的接口都是/item开头,所以我们监听/item路径,代理到windows上的tomcat服务。修改 /usr/local/openresty/nginx/conf/nginx.conf文件,添加一个location:
    location /item {proxy_pass http://192.168.150.1:8081;
    }
    

  1. 封装工具类
    • OpenResty启动时会加载以下两个目录中的工具文件
      在这里插入图片描述
    • 所以,自定义的http工具也需要放到这个目录下
  • /usr/local/openresty/lualib目录下,新建一个common.lua文件:

    vi /usr/local/openresty/lualib/common.lua
    
  • 完成内容:

    -- 封装函数,发送http请求,并解析响应
    local function read_http(path, params)local resp = ngx.location.capture(path,{method = ngx.HTTP_GET,args = params,})if not resp then-- 记录错误信息,返回404ngx.log(ngx.ERR, "http请求查询失败, path: ", path , ", args: ", args)ngx.exit(404)endreturn resp.body
    end
    -- 将方法导出
    local _M = {  read_http = read_http
    }  
    return _M
    

  1. 实现商品查询
    • 修改/usr/local/openresty/nginx/lua/item.lua文件,利用刚刚封装的函数库实现对tomcat的查询:
    -- 引入自定义common工具模块,返回值是common中返回的 _M
    local common = require("common")
    -- 从 common中获取read_http这个函数
    local read_http = common.read_http
    -- 获取路径参数
    local id = ngx.var[1]
    -- 根据id查询商品
    local itemJSON = read_http("/item/".. id, nil)
    -- 根据id查询商品库存
    local itemStockJSON = read_http("/item/stock/".. id, nil)
    

  • 查询到的结果是json字符串,并且包含商品、库存两个json字符串,页面最终需要的是把两个json拼接为一个json:需要把JSON变为lua的table,完成数据整合后,再转为JSON。
    在这里插入图片描述

4.5.6 CJSON工具类

  • OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。
  • 官方地址
  1. 引入cjson模块:
    local cjson = require "cjson"
    

  1. 序列化
    local obj = {name = 'jack',age = 21
    }
    -- 把 table 序列化为 json
    local json = cjson.encode(obj)
    

  1. 反序列化:
    local json = '{"name": "jack", "age": 21}'
    -- 反序列化 json为 table
    local obj = cjson.decode(json);
    print(obj.name)
    

4.5.7 实现Tomcat查询

  • 修改item.lua中的业务,添加json处理功能:
    -- 导入common函数库
    local common = require('common')
    local read_http = common.read_http
    -- 导入cjson库
    local cjson = require('cjson')-- 获取路径参数
    local id = ngx.var[1]
    -- 根据id查询商品
    local itemJSON = read_http("/item/".. id, nil)
    -- 根据id查询商品库存
    local itemStockJSON = read_http("/item/stock/".. id, nil)-- JSON转化为lua的table
    local item = cjson.decode(itemJSON)
    local stock = cjson.decode(stockJSON)-- 组合数据
    item.stock = stock.stock
    item.sold = stock.sold-- 把item序列化为json 返回结果
    ngx.say(cjson.encode(item))
    

4.5.8 基于ID负载均衡

  • 之前的的代码中,tomcat是单机部署。而实际开发中,tomcat一定是集群模式:
    在这里插入图片描述

  • 因此,OpenResty需要对tomcat集群做负载均衡。而默认的负载均衡规则是轮询模式,当我们查询/item/10001时:

    • 第一次会访问8081端口的tomcat服务,在该服务内部就形成了JVM进程缓存
    • 第二次会访问8082端口的tomcat服务,该服务内部没有JVM缓存(因为JVM缓存无法共享),会查询数据库
  • 因为轮询的原因,第一次查询8081形成的JVM缓存并未生效,直到下一次再次访问到8081时才可以生效,缓存命中率太低


  • 如果能让同一个商品,每次查询时都访问同一个tomcat服务,那么JVM缓存就一定能生效。也就是说,我们需要根据商品id做负载均衡,而不是轮询。

  1. 原理
    • nginx提供了基于请求路径做负载均衡的算法:nginx根据请求路径做hash运算,把得到的数值对tomcat服务的数量取余,余数是几,就访问第几个服务,实现负载均衡。
  2. 实现
    • 修改/usr/local/openresty/nginx/conf/nginx.conf文件,实现基于ID做负载均衡。
    • 首先,定义tomcat集群,并设置基于路径做负载均衡:
    upstream tomcat-cluster {hash $request_uri;-- Ip为主机IPserver 192.168.188.10:8081;server 192.168.188.10:8082;
    }
    
    • 然后,修改对tomcat服务的反向代理,目标指向tomcat集群:
    location /item {proxy_pass http://tomcat-cluster;
    }
    
    • 重新加载OpenResty
    nginx -s reload
    
  3. 测试:
    • 启动两台tomcat服务:
      在这里插入图片描述
    • 同时启动,再次访问页面,可以看到不同id的商品,访问到了不同的tomcat服务:
      在这里插入图片描述
      在这里插入图片描述

4.6 Redis缓存预热

  • Redis缓存会面临冷启动问题:
    • 冷启动:服务刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。
    • 缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。
  • 目前,数据量较少,并且没有数据统计相关功能,目前可以在启动时将所有数据都放入缓存中。

  1. 利用Docker安装Redis
    docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes
    
  2. 在item-service服务中引入Redis依赖
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  3. 配置Redis地址
    spring:redis:host: 192.168.188.112
    
  4. 编写初始化类
    • 缓存预热需要在项目启动时完成,并且必须是拿到RedisTemplate之后。这里利用InitializingBean接口来实现,因为InitializingBean可以在对象被Spring创建并且成员变量全部注入后执行。
    package com.heima.item.config;import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.heima.item.pojo.Item;
    import com.heima.item.pojo.ItemStock;
    import com.heima.item.service.IItemService;
    import com.heima.item.service.IItemStockService;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Component;import java.util.List;@Component
    public class RedisHandler implements InitializingBean {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService stockService;private static final ObjectMapper MAPPER = new ObjectMapper();@Overridepublic void afterPropertiesSet() throws Exception {// 初始化缓存// 1.查询商品信息List<Item> itemList = itemService.list();// 2.放入缓存for (Item item : itemList) {// 2.1.item序列化为JSONString json = MAPPER.writeValueAsString(item);// 2.2.存入redisredisTemplate.opsForValue().set("item:id:" + item.getId(), json);}// 3.查询商品库存信息List<ItemStock> stockList = stockService.list();// 4.放入缓存for (ItemStock stock : stockList) {// 2.1.item序列化为JSONString json = MAPPER.writeValueAsString(stock);// 2.2.存入redisredisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);}}
    }
    

4.7 查询Redis缓存

  • Redis缓存已经准备就绪,我们可以再OpenResty中实现查询Redis的逻辑
    在这里插入图片描述
  • 当请求进入OpenResty之后:
    • 优先查询Redis缓存
    • 如果Redis缓存未命中,再查询Tomcat

4.7.1 封装Redis工具


  • OpenResty提供了操作Redis的模块,我们只要引入该模块就能直接使用。但是为了方便,我们将Redis操作封装到之前的common.lua工具库中。修改/usr/local/openresty/lualib/common.lua文件:
  1. 引入Redis模块,并初始化Redis对象
    -- 导入redis
    local redis = require('resty.redis')
    -- 初始化redis
    local red = redis:new()
    red:set_timeouts(1000, 1000, 1000)
    

  1. 封装函数,用来释放Redis连接,其实是放入连接池
-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒local pool_size = 100 --连接池大小local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)if not ok thenngx.log(ngx.ERR, "放入redis连接池失败: ", err)end
end

  1. 封装函数,根据key查询Redis数据
    -- 查询redis的方法 ip和port是redis地址,key是查询的key
    local function read_redis(ip, port, key)-- 获取一个连接local ok, err = red:connect(ip, port)if not ok thenngx.log(ngx.ERR, "连接redis失败 : ", err)return nilend-- 查询redislocal resp, err = red:get(key)-- 查询失败处理if not resp thenngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)end--得到的数据为空处理if resp == ngx.null thenresp = nilngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)endclose_redis(red)return resp
    end
    

  1. 导出
    -- 将方法导出
    local _M = {  read_http = read_http,read_redis = read_redis
    }  
    return _M
    

  • 完整的common.lua:

    -- 导入redis
    local redis = require('resty.redis')
    -- 初始化redis
    local red = redis:new()
    red:set_timeouts(1000, 1000, 1000)-- 关闭redis连接的工具方法,其实是放入连接池
    local function close_redis(red)local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒local pool_size = 100 --连接池大小local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)if not ok thenngx.log(ngx.ERR, "放入redis连接池失败: ", err)end
    end-- 查询redis的方法 ip和port是redis地址,key是查询的key
    local function read_redis(ip, port, key)-- 获取一个连接local ok, err = red:connect(ip, port)if not ok thenngx.log(ngx.ERR, "连接redis失败 : ", err)return nilend-- 查询redislocal resp, err = red:get(key)-- 查询失败处理if not resp thenngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)end--得到的数据为空处理if resp == ngx.null thenresp = nilngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)endclose_redis(red)return resp
    end-- 封装函数,发送http请求,并解析响应
    local function read_http(path, params)local resp = ngx.location.capture(path,{method = ngx.HTTP_GET,args = params,})if not resp then-- 记录错误信息,返回404ngx.log(ngx.ERR, "http查询失败, path: ", path , ", args: ", args)ngx.exit(404)endreturn resp.body
    end
    -- 将方法导出
    local _M = {  read_http = read_http,read_redis = read_redis
    }  
    return _M
    

4.7.2 实现Redis查询

  • 接下来,去修改item.lua文件,实现对Redis的查询
  • 查询逻辑是:
    • 根据id查询Redis
    • 如果查询失败则继续查询Tomcat
    • 将查询结果返回
  1. 修改/usr/local/openresty/lua/item.lua文件,添加一个查询函数:
    -- 导入common函数库
    local common = require('common')
    local read_http = common.read_http
    local read_redis = common.read_redis
    -- 封装查询函数
    function read_data(key, path, params)-- 查询本地缓存local val = read_redis("127.0.0.1", 6379, key)-- 判断查询结果if not val thenngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)-- redis查询失败,去查询httpval = read_http(path, params)end-- 返回数据return val
    end
    
  2. 修改商品查询、库存查询的业务
    -- 查询商品信息
    local itemJSON = read_data("item:id:" .. id,  "/item/" .. id, nil)
    -- 查询库存信息
    local stockJSON = read_data("item:stock:id:" .. id, "/item/stock/" .. id, nil)
    
  3. 完整的item.lua代码:
    -- 导入common函数库
    local common = require('common')
    local read_http = common.read_http
    local read_redis = common.read_redis
    -- 导入cjson库
    local cjson = require('cjson')-- 封装查询函数
    function read_data(key, path, params)-- 查询本地缓存local val = read_redis("127.0.0.1", 6379, key)-- 判断查询结果if not val thenngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)-- redis查询失败,去查询httpval = read_http(path, params)end-- 返回数据return val
    end-- 获取路径参数
    local id = ngx.var[1]-- 查询商品信息
    local itemJSON = read_data("item:id:" .. id,  "/item/" .. id, nil)
    -- 查询库存信息
    local stockJSON = read_data("item:stock:id:" .. id, "/item/stock/" .. id, nil)-- JSON转化为lua的table
    local item = cjson.decode(itemJSON)
    local stock = cjson.decode(stockJSON)
    -- 组合数据
    item.stock = stock.stock
    item.sold = stock.sold-- 把item序列化为json 返回结果
    ngx.say(cjson.encode(item))
    
  4. 测试:
    在这里插入图片描述

4.8 Nginx本地缓存

  • 现在,整个多级缓存中只差最后一环,也就是nginx的本地缓存
    在这里插入图片描述

4.8.1 本地缓存API

  • OpenResty为Nginx提供了shard dict的功能,可以在nginx的多个worker之间共享数据,实现缓存功能。
  1. 开启共享字典,在nginx.conf的http下添加配置:
    # 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
    lua_shared_dict item_cache 150m; 
    
  2. 操作共享字典:
    -- 获取本地缓存对象
    local item_cache = ngx.shared.item_cache
    -- 存储, 指定key、value、过期时间,单位s,默认为0代表永不过期
    item_cache:set('key', 'value', 1000)
    -- 读取
    local val = item_cache:get('key')
    

4.8.2 实现本地缓存查询

  1. 修改/usr/local/openresty/lua/item.lua文件,修改read_data查询函数,添加本地缓存逻辑:
    -- 导入共享词典,本地缓存
    local item_cache = ngx.shared.item_cache-- 封装查询函数
    function read_data(key, expire, path, params)-- 查询本地缓存local val = item_cache:get(key)if not val thenngx.log(ngx.ERR, "本地缓存查询失败,尝试查询Redis, key: ", key)-- 查询redisval = read_redis("127.0.0.1", 6379, key)-- 判断查询结果if not val thenngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)-- redis查询失败,去查询httpval = read_http(path, params)endend-- 查询成功,把数据写入本地缓存item_cache:set(key, val, expire)-- 返回数据return val
    end
    
  2. 修改item.lua中查询商品和库存的业务,实现最新的read_data函数:
    -- 查询商品信息
    local itemJSON = read_data("item:id:" .. id, 1800,  "/item/" .. id, nil)
    -- 查询库存信息
    local stockJSON = read_data("item:stock:id:" .. id, 60, "/item/stock/" .. id, nil)
    
    • 过期后nginx缓存会自动删除,下次访问即可更新缓存。这里给商品基本信息设置超时时间为30分钟,库存为1分钟。因为库存更新频率较高,如果缓存时间过长,可能与数据库差异较大。
  3. 完整的item.lua文件:
    -- 导入common函数库
    local common = require('common')
    local read_http = common.read_http
    local read_redis = common.read_redis
    -- 导入cjson库
    local cjson = require('cjson')
    -- 导入共享词典,本地缓存
    local item_cache = ngx.shared.item_cache-- 封装查询函数
    function read_data(key, expire, path, params)-- 查询本地缓存local val = item_cache:get(key)if not val thenngx.log(ngx.ERR, "本地缓存查询失败,尝试查询Redis, key: ", key)-- 查询redisval = read_redis("127.0.0.1", 6379, key)-- 判断查询结果if not val thenngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)-- redis查询失败,去查询httpval = read_http(path, params)endend-- 查询成功,把数据写入本地缓存item_cache:set(key, val, expire)-- 返回数据return val
    end-- 获取路径参数
    local id = ngx.var[1]-- 查询商品信息
    local itemJSON = read_data("item:id:" .. id, 1800,  "/item/" .. id, nil)
    -- 查询库存信息
    local stockJSON = read_data("item:stock:id:" .. id, 60, "/item/stock/" .. id, nil)-- JSON转化为lua的table
    local item = cjson.decode(itemJSON)
    local stock = cjson.decode(stockJSON)
    -- 组合数据
    item.stock = stock.stock
    item.sold = stock.sold-- 把item序列化为json 返回结果
    ngx.say(cjson.encode(item))
    

五 缓存同步

  • 大多数情况下,浏览器查询到的都是缓存数据,如果缓存数据与数据库数据存在较大差异,可能会产生比较严重的后果。所以我们必须保证数据库数据、缓存数据的一致性,这就是缓存与数据库的同步。

5.1 数据同步策略

  • 缓存数据同步的常见方式有三种:
  • 设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新
    • 优势:简单、方便
    • 缺点:时效性差,缓存过期之前可能不一致
    • 场景:更新频率较低,时效性要求低的业务
  • 同步双写:在修改数据库的同时,直接修改缓存
    • 优势:时效性强,缓存与数据库强一致
    • 缺点:有代码侵入,耦合度高;
    • 场景:对一致性、时效性要求较高的缓存数据
  • **异步通知:**修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
    • 优势:低耦合,可以同时通知多个缓存服务
    • 缺点:时效性一般,可能存在中间不一致状态
    • 场景:时效性要求一般,有多个服务需要同步

  • 异步实现又可以基于MQ或者Canal来实现:
  1. 基于MQ的异步通知:
    在这里插入图片描述
  • 解读:
    • 商品服务完成对数据的修改后,只需要发送一条消息到MQ中。
    • 缓存服务监听MQ消息,然后完成对缓存的更新
  • 依然有少量的代码侵入。

  1. 基于Canal的通知
    在这里插入图片描述
  • 解读:
    • 商品服务完成商品修改后,业务直接结束,没有任何代码侵入
    • Canal监听MySQL变化,当发现变化后,立即通知缓存服务
    • 缓存服务接收到canal通知,更新缓存
  • 代码零侵入

5.2 Canal简介

  • Canal [kə’næl] ,译意为水道/管道/沟渠,canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。
  • Canal是基于mysql的主从同步来实现的,MySQL主从同步的原理如下:
    在这里插入图片描述
  1. MySQL master 将数据变更写入二进制日志( binary log),其中记录的数据叫做binary log events
  2. MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log)
  3. MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

  • 而Canal就是把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端,进而完成对其它数据库的同步。
    在这里插入图片描述

5.3 安装Canal

  • 开启mysql的主从同步机制,让Canal来模拟salve。

5.3.1 开启MySQL主从

  • Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。以之前用Docker运行的mysql为例

5.3.1.1 开启binlog

  • 打开mysql2容器挂载的日志文件,如:/tmp/mysql/conf目录:

  • 修改文件:

    vi /tmp/mysql/conf/my.cnf
    
  • 添加内容:

    log-bin=/var/lib/mysql/mysql-bin
    binlog-do-db=heima
    
  • 配置解读:

    • log-bin=/var/lib/mysql/mysql-bin:设置binary log文件的存放地址和文件名,叫做mysql-bin
    • binlog-do-db=heima:指定对哪个database记录binary log events,这里记录heima这个库
  • 最终my.cnf内容:

    [mysqld]
    skip-name-resolve
    character_set_server=utf8
    datadir=/var/lib/mysql
    server-id=1000
    log-bin=/var/lib/mysql/mysql-bin
    binlog-do-db=heima
    
  • 测试:连接mysql查看权限是否开启

    show binary logs;
    

5.3.1.2 设置用户权限

  • 添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限
    create user canal@'%' IDENTIFIED by 'canal';
    GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
    FLUSH PRIVILEGES;
    
  • 重启mysql容器即可
    docker restart mysql2
    
  • 测试设置是否成功:在mysql控制台,或者Navicat中,输入命令:
    show master status;
    

在这里插入图片描述

5.3.2 安装Canal

5.3.2.1 创建网络

  • 需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:
    [root@kongyue ~]# docker network create heima
    e479b78ebfc23a5e38e776aae6e5f55f879cdfc0ff2b45709b5dba8669c27c24
    
  • 让mysql2加入这个网络:
    [root@kongyue ~]# docker network connect heima mysql
    

5.3.2.2 创建Canal容器

  1. 下载安装
    • 官方下载地址
    • 或者使用资料中的压缩包[上传到虚拟机,然后通过命令导入]
    docker load -i canal.tar
    
  2. 然后运行命令创建Canal容器:
    docker run -p 11111:11111 --name canal \
    -e canal.destinations=heima \
    -e canal.instance.master.address=mysql:3306  \
    -e canal.instance.dbUsername=canal  \
    -e canal.instance.dbPassword=canal  \
    -e canal.instance.connectionCharset=UTF-8 \
    -e canal.instance.tsdb.enable=true \
    -e canal.instance.gtidon=false  \
    -e canal.instance.filter.regex=heima\\..* \
    --network heima \
    -d canal/canal-server:v1.1.5
    

在这里插入图片描述

  • 说明:

    • -p 11111:11111:这是canal的默认监听端口
    • -e canal.instance.master.address=mysql:3306:数据库地址和端口,如果不知道mysql容器地址,可以通过docker inspect 容器id来查看
    • -e canal.instance.dbUsername=canal:数据库用户名
    • -e canal.instance.dbPassword=canal :数据库密码
    • -e canal.instance.filter.regex=:要监听的表名称
  • 表名称监听支持的语法:

    mysql 数据解析关注的表,Perl正则表达式.
    多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\) 
    常见例子:
    1.  所有表:.*   or  .*\\..*
    2.  canal schema下所有表: canal\\..*
    3.  canal下的以canal打头的表:canal\\.canal.*
    4.  canal schema下的一张表:canal.test1
    5.  多个规则组合使用然后以逗号隔开:canal\\..*,mysql.test1,mysql.test2 
    

5.4 监听Canal

  • Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。
    在这里插入图片描述
  • 利用Canal提供的Java客户端,监听Canal通知消息。当收到变化的消息时,完成对缓存的更新。
  • 这里会使用GitHub上的第三方开源的canal-starter客户端。地址
  • 与SpringBoot完美整合,自动装配,比官方客户端要简单好用很多。

5.4.1 引入依赖

<dependency><groupId>top.javatool</groupId><artifactId>canal-spring-boot-starter</artifactId><version>1.2.1-RELEASE</version>
</dependency>

5.4.2 编写配置

canal:destination: heima # canal的集群名字,要与安装canal时设置的名称一致server: 192.168.188.112:11111 # canal服务地址

5.4.3 修改Item实体类

  • 通过@Id、@Column、等注解完成Item与数据库表字段的映射:

    import com.baomidou.mybatisplus.annotation.IdType;
    import com.baomidou.mybatisplus.annotation.TableField;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;
    import org.springframework.data.annotation.Id;
    import org.springframework.data.annotation.Transient;import javax.persistence.Column;
    import java.util.Date;@Data
    @TableName("tb_item")
    public class Item {@TableId(type = IdType.AUTO)@Idprivate Long id;//商品id@Column(name = "name")private String name;//商品名称private String title;//商品标题private Long price;//价格(分)private String image;//商品图片private String category;//分类名称private String brand;//品牌名称private String spec;//规格private Integer status;//商品状态 1-正常,2-下架private Date createTime;//创建时间private Date updateTime;//更新时间@TableField(exist = false)@Transientprivate Integer stock;@TableField(exist = false)@Transientprivate Integer sold;
    }
    

5.4.4 编写监听器

  • 通过实现EntryHandler<T>接口编写监听器,监听Canal消息。注意两点:
    • 实现类通过@CanalTable("tb_item")指定监听的表信息
    • EntryHandler的泛型是与表对应的实体类
    import com.github.benmanes.caffeine.cache.Cache;
    import com.heima.item.config.RedisHandler;
    import com.heima.item.pojo.Item;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import top.javatool.canal.client.annotation.CanalTable;
    import top.javatool.canal.client.handler.EntryHandler;@CanalTable("tb_item")
    @Component
    public class ItemHandler implements EntryHandler<Item> {@Autowiredprivate RedisHandler redisHandler;@Autowiredprivate Cache<Long, Item> itemCache;@Overridepublic void insert(Item item) {// 写数据到JVM进程缓存itemCache.put(item.getId(), item);// 写数据到redisredisHandler.saveItem(item);}@Overridepublic void update(Item before, Item after) {// 写数据到JVM进程缓存itemCache.put(after.getId(), after);// 写数据到redisredisHandler.saveItem(after);}@Overridepublic void delete(Item item) {// 删除数据到JVM进程缓存itemCache.invalidate(item.getId());// 删除数据到redisredisHandler.deleteItemById(item.getId());}
    }
    
  • 在这里对Redis的操作都封装到了RedisHandler这个对象中,是我们之前做缓存预热时编写的一个类,内容如下:
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.heima.item.pojo.Item;
    import com.heima.item.pojo.ItemStock;
    import com.heima.item.service.IItemService;
    import com.heima.item.service.IItemStockService;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Component;import java.util.List;@Component
    public class RedisHandler implements InitializingBean {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService stockService;private static final ObjectMapper MAPPER = new ObjectMapper();@Overridepublic void afterPropertiesSet() throws Exception {// 初始化缓存// 1.查询商品信息List<Item> itemList = itemService.list();// 2.放入缓存for (Item item : itemList) {// 2.1.item序列化为JSONString json = MAPPER.writeValueAsString(item);// 2.2.存入redisredisTemplate.opsForValue().set("item:id:" + item.getId(), json);}// 3.查询商品库存信息List<ItemStock> stockList = stockService.list();// 4.放入缓存for (ItemStock stock : stockList) {// 2.1.item序列化为JSONString json = MAPPER.writeValueAsString(stock);// 2.2.存入redisredisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);}}public void saveItem(Item item) {try {String json = MAPPER.writeValueAsString(item);redisTemplate.opsForValue().set("item:id:" + item.getId(), json);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}public void deleteItemById(Long id) {redisTemplate.delete("item:id:" + id);}
    }
    

5.5 测试

  1. 在商品管理页面进行修改
    在这里插入图片描述
  2. 在商品查询页面进行查询
    在这里插入图片描述

相关文章:

微服务高级篇学习【4】之多级缓存

文章目录 前言一 多级缓存二 JVM进程缓存2.1 案例导入2.1.1 使用docker安装mysql2.1.2 修改配置2.1.3 导入项目工程2.1.4 导入商品查询页面2.1.5 反向代理 2.2 初识Caffeine2.3 实现JVM进程缓存 三 Lua脚本入门3.1 安装Lua3.2 Lua语法学习 四 实现多级缓存4.1 OpenResty简介4.2…...

知乎版ChatGPT「知海图AI」加入国产大模型乱斗,称效果与GPT-4持平

“2023知乎发现大会”上&#xff0c;知乎创始人、董事长兼CEO周源和知乎合作人、CTO李大海共同宣布了知乎与面壁智能联合发布“知海图AI”中文大模型。 周源据介绍&#xff0c;知乎与面壁智能达成深度合作&#xff0c;共同开发中文大模型产品并推进应用落地。目前&#xff0c;知…...

邮件发送配置

QQ邮箱发送和接收配置&#xff1a; POP3/SMTP协议 接收邮件服务器&#xff1a;pop.exmail.qq.com &#xff0c;使用SSL&#xff0c;端口号995 发送邮件服务器&#xff1a;smtp.exmail.qq.com &#xff0c;使用SSL&#xff0c;端口号465 海外用户可使用以下服务器 接收邮件服务器…...

【Open CASCADE -生成MFC和QT事例方式】

源代码目录 adm目录&#xff1a;包含编译OCCT的相关工程; adm/cmake目录&#xff1a;包含使用CMake构建OCCT的相关处理脚本; adm/msvc目录&#xff1a;包含window平台 Visual C 2010, 2012, 2013, 2015, 2017 and 2019等版本的32/64平台solutinon文件; data目录&#xff1a; 包…...

python 笔记:PyTrack(将GPS数据和OpenStreetMap数据进行整合)【官网例子解读】

论文笔记&#xff1a;PyTrack: A Map-Matching-Based Python Toolbox for Vehicle Trajectory Reconstruction_UQI-LIUWJ的博客-CSDN博客4 0 包的安装 官网的两种方式我都试过&#xff0c;装是能装成功&#xff0c;但是python import PyTrack包的时候还是显示找不到Pytrack …...

苦中作乐 ---竞赛刷题31-40(15-20)

&#xff08;一&#xff09;目录 L1-032 Left-pad L1-033 出生年 L1-034 点赞 L1-035 情人节 L1-039 古风排版 &#xff08;二&#xff09;题目 L1-032 Left-pad 根据新浪微博上的消息&#xff0c;有一位开发者不满NPM&#xff08;Node Package Manager&#xff09;的做法…...

100种思维模型之人类误判心理思维模型-49

“我们老得太快&#xff0c;聪明得太迟”——查理芒格。 2005年&#xff0c;81岁的查理芒格认为81岁的他能够比10年前做得更好。他决定对1992年2月2日、1994年10月6日和1995年4月24日的三次演讲稿进行修改&#xff0c;于是就有了这个人类误判心理思维模型——25条人类误判心理学…...

【从零开始学Skynet】实战篇《球球大作战》(十三):场景代码设计(下)

1、主循环 《球球大作战》是一款服务端运算的游戏&#xff0c;一般会使用主循环程序结构&#xff0c;让服务端处理战斗逻辑。如下图所示&#xff0c;图中的 balls 和 foods代表服务端的状态&#xff0c;在循环中执行“ 食物生成”“位置更新”和“碰撞检 测” 等功能&#xff0…...

2023年虚拟数字人行业研究报告

第一章 行业概况 虚拟数字人指存在于非物理世界中&#xff0c;由计算机图形学、图形渲染、动作捕捉、深度学习、语音合成等计算机手段创造及使用&#xff0c;并具有多种人类特征&#xff08;外貌特征、人类表演能力、人类交互能力等&#xff09;的综合产物。虚拟人可分为服务型…...

Oracle 之表的连接类型——舞蹈跳出

嵌套循环&#xff08;Nested Loops Join&#xff09; Oracle 中最基本的连接方法&#xff0c;用于处理数据表之间的连接操作。 嵌套循环是通过对其中一个表&#xff08;外部表&#xff09;进行全循环操作&#xff0c;然后针对每条记录在另一张表&#xff08;内部表&#xff09;…...

深入浅出JS定时器:从setTimeout到setInterval

前言 当谈到 JavaScript 编程语言最基本的概念时&#xff0c;定时器就是一个必须掌握的知识点。在编写网站时&#xff0c;你经常会遇到需要在一定时间间隔内执行一些代码的情况。这时候&#xff0c;JavaScript 定时器就可以派上用场了。 什么是定时器&#xff1f; JS 定时器是…...

CountDownLatch、CyclicBarrier、Semaphore 的原理以及实例总结

文章目录 CountDownLatch、CyclicBarrier、Semaphore 的原理以及实例总结一、CountDownLatch二、CyclicBarrier三、Semaphore总结 CountDownLatch、CyclicBarrier、Semaphore 的原理以及实例总结 在Java多线程编程中&#xff0c;有三种常见的同步工具类&#xff1a;CountDownL…...

企业电子招投标系统源码之了解电子招标投标全流程

随着各级政府部门的大力推进&#xff0c;以及国内互联网的建设&#xff0c;电子招投标已经逐渐成为国内主流的招标投标方式&#xff0c;但是依然有很多人对电子招投标的流程不够了解&#xff0c;在具体操作上存在困难。虽然各个交易平台的招标投标在线操作会略有不同&#xff0…...

SpringCloud之Gateway组件简介

网关的理解 网关类似于海关或者大门&#xff0c;出入都需要经过这个网关。别人不经过这个网关&#xff0c;永远也看不到里面的东西。可以在网关进行条件过滤&#xff0c;比如大门只有对应的钥匙才能入内。网关和大门一样&#xff0c;永远暴露在最外面 不使用网关 前端需要记住每…...

GoNote第三章 主流框架加对比

GoNote第三章 主流框架加对比 Golang主流框架介绍 自从面市以来&#xff0c;Golang成为了程序员在编写API和开发Web服务时的首选之一。近90%的受访者表示会在自己下一组项目中持续使用Golang。与我们熟悉的C和C类似&#xff0c;Go语言也是现有Golang的“灵魂”。而Golang则是…...

Quartz框架详解分析

文章目录 1 Quartz框架1.1 入门demo1.2 Job 讲解1.2.1 Job简介1.2.2 Job 并发1.2.3 Job 异常1.2.4 Job 中断 1.3 Trigger 触发器1.3.1 SimpleTrigger1.3.2 CornTrigger 1.4 Listener监听器1.5 Jdbc store1.5.1 简介1.5.2 添加pom依赖1.5.3 建表SQL1.5.4 配置文件quartz.propert…...

Nginx专题-基于多网卡的主机配置

文章目录 Nginx 基于多网卡的主机实现一、虚拟机前置环境准备ifcfg-ens32配置文件的内容参考ifcfg-ens33配置文件的内容 二、案例演示修改nginx.conf配置文件解决中文乱码 Nginx 基于多网卡的主机实现 一、虚拟机前置环境准备 点击虚拟机右下角的 红色标框按钮&#xff0c;然后…...

4.2和4.3、MAC地址、IP地址、端口

计算机网络等相关知识可以去小林coding进行巩固&#xff08;点击前往&#xff09; 4.2和4.3、MAC地址、IP地址、端口 1.MAC地址的简介2.IP地址①IP地址简介②IP地址编址方式③A类IP地址④B类IP地址⑤C类IP地址⑥D类IP地址⑧子网掩码 3.端口①简介②端口类型 1.MAC地址的简介 …...

放弃 console.log 吧!用 Debugger 你能读懂各种源码

很多同学不知道为什么要用 debugger 来调试&#xff0c;console.log 不行么&#xff1f; 还有&#xff0c;会用 debugger 了&#xff0c;还是有很多代码看不懂&#xff0c;如何调试复杂源码呢&#xff1f; 这篇文章就来讲一下为什么要用这些调试工具&#xff1a; console.lo…...

epoll机制解析

一、epoll实现原理 1、实现原理 epoll通过3个方法来实现对句柄的监控操作&#xff0c;要深刻理解epoll&#xff0c;首先得了解epoll的三大关键要素&#xff1a;mmap、红黑树、链表。下面是epoll的框架图&#xff0c;如下&#xff1a; mmap epoll是通过内核与用户空间mmap同一块…...

Cursor实现用excel数据填充word模版的方法

cursor主页&#xff1a;https://www.cursor.com/ 任务目标&#xff1a;把excel格式的数据里的单元格&#xff0c;按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例&#xff0c;…...

Golang dig框架与GraphQL的完美结合

将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用&#xff0c;可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器&#xff0c;能够帮助开发者更好地管理复杂的依赖关系&#xff0c;而 GraphQL 则是一种用于 API 的查询语言&#xff0c;能够提…...

ardupilot 开发环境eclipse 中import 缺少C++

目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...

UR 协作机器人「三剑客」:精密轻量担当(UR7e)、全能协作主力(UR12e)、重型任务专家(UR15)

UR协作机器人正以其卓越性能在现代制造业自动化中扮演重要角色。UR7e、UR12e和UR15通过创新技术和精准设计满足了不同行业的多样化需求。其中&#xff0c;UR15以其速度、精度及人工智能准备能力成为自动化领域的重要突破。UR7e和UR12e则在负载规格和市场定位上不断优化&#xf…...

ip子接口配置及删除

配置永久生效的子接口&#xff0c;2个IP 都可以登录你这一台服务器。重启不失效。 永久的 [应用] vi /etc/sysconfig/network-scripts/ifcfg-eth0修改文件内内容 TYPE"Ethernet" BOOTPROTO"none" NAME"eth0" DEVICE"eth0" ONBOOT&q…...

Java 二维码

Java 二维码 **技术&#xff1a;**谷歌 ZXing 实现 首先添加依赖 <!-- 二维码依赖 --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.5.1</version></dependency><de…...

Chrome 浏览器前端与客户端双向通信实战

Chrome 前端&#xff08;即页面 JS / Web UI&#xff09;与客户端&#xff08;C 后端&#xff09;的交互机制&#xff0c;是 Chromium 架构中非常核心的一环。下面我将按常见场景&#xff0c;从通道、流程、技术栈几个角度做一套完整的分析&#xff0c;特别适合你这种在分析和改…...

xmind转换为markdown

文章目录 解锁思维导图新姿势&#xff1a;将XMind转为结构化Markdown 一、认识Xmind结构二、核心转换流程详解1.解压XMind文件&#xff08;ZIP处理&#xff09;2.解析JSON数据结构3&#xff1a;递归转换树形结构4&#xff1a;Markdown层级生成逻辑 三、完整代码 解锁思维导图新…...

【Linux】Linux安装并配置RabbitMQ

目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的&#xff0c;需要先安…...

Java数组Arrays操作全攻略

Arrays类的概述 Java中的Arrays类位于java.util包中&#xff0c;提供了一系列静态方法用于操作数组&#xff08;如排序、搜索、填充、比较等&#xff09;。这些方法适用于基本类型数组和对象数组。 常用成员方法及代码示例 排序&#xff08;sort&#xff09; 对数组进行升序…...