缓存的作用是减轻数据库的压力,缩短服务响应的时间,从而提高整个服务的并发能力。之前学习过redis分布式缓存 的单阶能力已经很高了,但是依然有自己的上限,对于像淘宝、京东这种并发量达到上亿级流量 的场景,仅靠redis缓存不够,需要使用多级缓存 。
传统缓存:

上图中的缓存策略存在一些问题
(1)用户请求需要经过tomcat,由tomcat去查询redis缓存,tomcat本身的并发能力不如redis,即tomcat的性能成为整个系统的瓶颈。
(2)redis缓存失效时,会对数据库造成压力。
一、多级缓存方案
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻tomcat压力,提升服务性能。

上图中,需要在nginx中编写对redis和tomcat访问的业务逻辑编写,因此需要部署为集群(当然redis和tomcat也可部署为集群模式):

tomcat进程缓存通过JVM进程缓存实现。
nginx本地缓存通过lua语言实现。
二、JVM进程缓存
2.1 导入商品案例
2.1.1 使用docker安装Mysql
后期做数据同步需要用到MySQL的主从功能,所以需要在虚拟机中,利用Docker来运行一个MySQL容器。
1.拉取mysql镜像

bash
docker pull mysql:5.7.25
docker images
2.准备目录和运行命令

bash
cd /tmp
mkdir mysql
cd mysql
docker run \
-p 3306:3306 \
--name mysql \
-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


3.修改配置

bash
vi conf/my.cnf
文件内容如下:

bash
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000

4.重启mysql容器
bash
docker restart mysql


2.1.2 导入sql
将课程资料中的item.sql文件导入。



tb_item是商品表,tb_item_stock是商品库存表(包括库存和销量信息)。
2.1.3 导入demo工程
打开课程资料中的item-service工程。


该项目已经实现了商品库存的增删改查业务。
注意:修改mysql配置。

2.1.4 导入商品查询页面
将课程资料中的nginx服务拷贝到一个不含中文的目录中。



现在页面中的信息是写死的。

需要一个反向代理的nginx服务器,将静态的商品页面放到nginx目录中,页面需要的数据通过ajax向服务端(nginx业务集群)查询。

配置文件中的内容:


刚才在windows系统启动nginx服务就是起到反向代理的作用,nginx业务集群部署在虚拟机(服务器)中。
2.2 Caffeine
2.2.1 分布式缓存vs进程缓存
缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:
- 分布式缓存,例如Redis:
- 优点:存储容量更大、可靠性更好、可以在集群间共享
- 缺点:访问缓存有网络开销
- 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
- 进程本地缓存,例如HashMap、GuavaCache:
- 优点:读取本地内存,没有网络开销,速度更快
- 缺点:存储容量有限、可靠性较低、无法共享
- 场景:性能要求较高,缓存数据量较小
2.2.2 caffeine示例
caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库,目前spring内部的缓存就是caffeine。
下面是一个caffeine示例:

java
@Test
void testBasicOps() {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder().build();
// 存数据
cache.put("gf", "迪丽热巴");
// 取数据,不存在则返回null
String gf = cache.getIfPresent("gf");
System.out.println("gf = " + gf);
// 取数据,不存在则去数据库查询
String defaultGF = cache.get("defaultGF", key -> {
// 这里可以去数据库根据 key查询value(这里没有查询数据库,写了一个假的)
return "柳岩";
});
System.out.println("defaultGF = " + defaultGF);
}

2.2.3 caffeine缓存驱逐策略
缓存空间有限,若是满了则根据过期策略把一些缓存清理,caffeine也有驱逐策略。
Caffeine既然是缓存的一种,肯定需要有缓存的清除策略,不然的话内存总会有耗尽的时候。
Caffeine提供了三种缓存驱逐策略:
-
基于容量:设置缓存的数量上限
java// 创建缓存对象 Cache<String, String> cache = Caffeine.newBuilder() .maximumSize(1) // 设置缓存大小上限为 1 .build(); -
基于时间:设置缓存的有效时间
java// 创建缓存对象 Cache<String, String> cache = Caffeine.newBuilder() // 设置缓存有效期为 10 秒,从最后一次写入开始计时 .expireAfterWrite(Duration.ofSeconds(10)) .build(); -
基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。
注意:在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。
2.2.3.1一个基于大小设置驱逐策略的案例

java
@Test
void testEvictByNum() throws InterruptedException {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
// 设置缓存大小上限为 1
.maximumSize(1)
.build();
// 存数据
cache.put("gf1", "柳岩");
cache.put("gf2", "范冰冰");
cache.put("gf3", "迪丽热巴");
// 延迟10ms,给清理线程一点时间
Thread.sleep(10L);
// 获取数据
System.out.println("gf1: " + cache.getIfPresent("gf1"));
System.out.println("gf2: " + cache.getIfPresent("gf2"));
System.out.println("gf3: " + cache.getIfPresent("gf3"));
}


2.2.3.2 一个基于时间设置驱逐策略的案例

java
@Test
void testEvictByTime() throws InterruptedException {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 10 秒
.build();
// 存数据
cache.put("gf", "柳岩");
// 获取数据
System.out.println("gf: " + cache.getIfPresent("gf"));
// 休眠一会儿
Thread.sleep(1200L);
System.out.println("gf: " + cache.getIfPresent("gf"));
}

说明:在实际开发中,可以把Cache对象设置为静态的,在代码其他地方调用。
2.2.4 案例-实现商品查询的本地进程缓存


java
@Configuration
public class CaffeineConfig {
@Bean
public Cache<Long, Item> itemCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000)
.build();
}
@Bean
public Cache<Long, ItemStock> stockCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000)
.build();
}
}
修改接口查询代码:

java
@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));
}

java
@Autowired
private Cache<Long, Item> itemCache;
@Autowired
private Cache<Long, ItemStock> stockCache;

浏览器访问一次后台接口:

日志打印:



三、多级缓存实现
nginx中查询redis和查询tomcat的业务逻辑代码的编写需要用lua,lua语法参考博文《lua语法》
3.1 OpenResty
OpenResty® 是一个基于 Nginx的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点:
- 具备Nginx的完整功能
- 基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块
- 允许使用Lua自定义业务逻辑 、自定义库
官方网站: https://openresty.org/cn/
3.1.1 安装
1)安装开发库
首先要安装OpenResty的依赖开发库,执行命令:
sh
yum install -y pcre-devel openssl-devel gcc --skip-broken

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

3)安装OpenResty
然后就可以像下面这样安装软件包,比如 openresty:
bash
yum install -y openresty

4)安装opm工具
opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。
如果你想安装命令行工具 opm,那么可以像下面这样安装 openresty-opm 包:
bash
yum install -y openresty-opm

5)目录结构
默认情况下,OpenResty安装的目录是:/usr/local/openresty
看到里面的nginx目录了吗,OpenResty就是在Nginx基础上集成了一些Lua模块。

6)配置nginx的环境变量
打开配置文件:
sh
vi /etc/profile
在最下面加入两行:
sh
export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH
NGINX_HOME:后面是OpenResty安装目录下的nginx的目录
然后让配置生效:
source /etc/profile



3.1.2 启动与运行
nginx的默认配置文件注释太多,影响后续我们的编辑,这里将nginx.conf中的注释部分删除,保留有效部分。
修改/usr/local/openresty/nginx/conf/nginx.conf文件,内容如下:
nginx
#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;
}
}
}


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

3.1.3 案例-实现商品详情页数据查询
2.1.4中项目前端商品详情页目前展示的是加数据,在浏览器的开发者工具控制台可以看到查询商品信息的请求:

而这个请求最终被反向代理到刚才在3.1中安装的OpenResty集群:
2.1.4中nginx的配置文件:


3.1.4.1 修改nginx.conf文件

修改3.1.2中OpenResty中nginx的配置文件:


nginx
#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
location /api/item{
# 默认的响应类型
default_type application/json;
# 响应结果由lua/item.lua文件来决定
content_by_lua_file lua/item.lua;
}
配置文件中配置的item.lua文件下面创建。
3.1.4.2 编写item.lua文件
新建3.1.4.1配置文件中配置的item.lua文件(注意文件的路径/usr/local/openresty/nginx/lua/item.lua):



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

bash
nginx -s reload

上图中的页面没有正常显示,按理应该是可以显示的。
3.1.3 请求参数处理


可以看到,现在前端发送的请求参数采用路径占位符的方式。
需求:获取请求路径中的商品id信息,拼接到json结果中返回。
编辑item.lua脚本:

lua
-- 获取路径参数
local id = ngx.var[1]
--返回结果
ngx.say('{"id":'.. id .. ',"name":"SALSA AIR","title":"RIMOWA 22寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":19900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"{\"颜色\": \"红色\", \"尺码\": \"26寸\"}","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":99996,"sold":3219}')
3.2 tomcat缓存
说明:因为不知道如何让服务器访问到电脑的tomcat服务,所以后面的内容没法动手实践了,都是视频里的截图。
先实现由openresty到tomcat的查询请求:



编辑虚拟机或服务器中的nginx配置文件nginx.conf,配置反向代理:



在编写虚拟机或服务器中的item.lua文件:

重新加载配置:


补充:tomcat集群的负载均衡
生产环境的tomcat服务肯定有多台,需要在nginx中配置tomcat集群。

修改虚拟机或服务器中nginx服务的配置文件nginx.conf文件:

将windows本地服务配置为两台tomcat:


测试:

可以看到,8082的tomcat服务器接收到了请求:

清空8081和8082的日志。


可以看到8081和8082都没有日志打印,说明请求依然到了8082的tomcat服务器,且查询了缓存。
3.3 redis缓存
3.3.1 redis缓存预热
注意,之前提到过,openresty需要先查询redis缓存,redis缓存未命中才查询tomcat,这里加上openresty查询redis的步骤。

注意,先查询redis缓存会有冷启动 问题。



bash
docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes
可以用redis客户端连接:




该文件中的方法会在项目启动的时候执行,可以实现redis缓存预热效果。
java
@Component
public class RedisHandler implements InitializingBean {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private IItemService itemService;
@Autowired
private IItemStockService stockService;
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void afterPropertiesSet() throws Exception {
// 初始化缓存
// 1.查询商品信息
List<Item> itemList = itemService.list();
// 2.放入缓存
for (Item item : itemList) {
// 2.1.item序列化为JSON
String json = MAPPER.writeValueAsString(item);
// 2.2.存入redis
redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
}
// 3.查询商品库存信息
List<ItemStock> stockList = stockService.list();
// 4.放入缓存
for (ItemStock stock : stockList) {
// 2.1.item序列化为JSON
String json = MAPPER.writeValueAsString(stock);
// 2.2.存入redis
redisTemplate.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);
}
}
3.3.2 查询redis缓存


修改之前封装的common.lua文件:


接下来:





3.4 添加nginx本地缓存

修改服务器或虚拟机nginx服务的配置文件nginx.conf:

修改item.lua文件:



修改item.lua文件:

重新加载配置:

查看nginx的日志:

刷新浏览器:

看到nginx日志:


可以看到nginx日志没有再打印日志了,查询到了本地缓存:

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

依然有少量的代码侵入(需要修改item-service服务,低耦合)。

4.2 安装canal
Canal [kə'næl],译意为水道/管道/沟渠,canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。GitHub的地址:https://github.com/alibaba/canal
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的客户端,进而完成对其它数据库的同步。

4.2.1 开启MySQL主从
4.2.1.1 开启binlog
修改文件:
sh
vi /tmp/mysql/conf/my.cnf
添加内容:
ini
log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima
配置解读:
log-bin=/var/lib/mysql/mysql-bin:设置binary log文件的存放地址和文件名,叫做mysql-binbinlog-do-db=heima:指定对哪个database记录binary log events,这里记录heima这个库



4.2.1.2 设置用户权限
接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限。
mysql
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;


重启mysql容器即可
docker restart mysql
测试设置是否成功:在mysql控制台,或者Navicat中,输入命令:
show master status;

4.2.2 安装canal
4.2.2.1 创建网络
需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:
sh
docker network create heima
让mysql加入这个网络:
sh
docker network connect heima mysql

4.2.2.2 安装
使用资料中提供了canal的镜像压缩包:

上传到虚拟机,然后通过命令导入:
docker load -i canal.tar
然后运行命令创建Canal容器:
sh
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
4.3 监听canal

可以利用Canal提供的Java客户端,监听Canal通知消息。当收到变化的消息时,完成对缓存的更新。
不过这里使用GitHub上的第三方开源的canal-starter客户端。地址:https://github.com/NormanGyllenhaal/canal-client
与SpringBoot完美整合,自动装配,比官方客户端要简单好用很多。
5.3.1.引入依赖
xml
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-starter</artifactId>
<version>1.2.1-RELEASE</version>
</dependency>

5.3.2.编写配置
yaml
canal:
destination: heima # canal的集群名字,要与安装canal时设置的名称一致
server: 192.168.150.101:11111 # canal服务地址

5.3.3.修改Item实体类


通过@Id、@Column、等注解完成Item与数据库表字段的映射:
java
package com.heima.item.pojo;
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)
@Id
private 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)
@Transient
private Integer stock;
@TableField(exist = false)
@Transient
private Integer sold;
}
5.3.4.编写监听器

通过实现EntryHandler<T>接口编写监听器,监听Canal消息。注意两点:
- 实现类通过
@CanalTable("tb_item")指定监听的表信息 - EntryHandler的泛型是与表对应的实体类
java
package com.heima.item.canal;
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> {
@Autowired
private RedisHandler redisHandler;
@Autowired
private Cache<Long, Item> itemCache;
@Override
public void insert(Item item) {
// 写数据到JVM进程缓存
itemCache.put(item.getId(), item);
// 写数据到redis
redisHandler.saveItem(item);
}
@Override
public void update(Item before, Item after) {
// 写数据到JVM进程缓存
itemCache.put(after.getId(), after);
// 写数据到redis
redisHandler.saveItem(after);
}
@Override
public void delete(Item item) {
// 删除数据到JVM进程缓存
itemCache.invalidate(item.getId());
// 删除数据到redis
redisHandler.deleteItemById(item.getId());
}
}
在这里对Redis的操作都封装到了RedisHandler这个对象中,是我们之前做缓存预热时编写的一个类,内容如下:
java
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 {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private IItemService itemService;
@Autowired
private IItemStockService stockService;
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public void afterPropertiesSet() throws Exception {
// 初始化缓存
// 1.查询商品信息
List<Item> itemList = itemService.list();
// 2.放入缓存
for (Item item : itemList) {
// 2.1.item序列化为JSON
String json = MAPPER.writeValueAsString(item);
// 2.2.存入redis
redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
}
// 3.查询商品库存信息
List<ItemStock> stockList = stockService.list();
// 4.放入缓存
for (ItemStock stock : stockList) {
// 2.1.item序列化为JSON
String json = MAPPER.writeValueAsString(stock);
// 2.2.存入redis
redisTemplate.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.3.5 测试

为了方便观察nginx缓存的修改,使用浏览器查看:

项目已经有了前端页面:




