springcloud篇10-多级缓存

缓存的作用是减轻数据库的压力,缩短服务响应的时间,从而提高整个服务的并发能力。之前学习过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-bin
  • binlog-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缓存的修改,使用浏览器查看:

项目已经有了前端页面:




相关推荐
爱吃山竹的大肚肚3 小时前
MySQL 支持的各类索引
java·数据库·sql·mysql·spring·spring cloud
kobe_OKOK_3 小时前
Django缓存接口数据
python·缓存·django
爱学习的小道长3 小时前
Ubuntu Python 使用 Redis 缓存
python·ubuntu·缓存
梵得儿SHI3 小时前
SpringCloud 核心组件精讲:Spring Cloud Gateway 网关实战-路由配置 + 过滤器开发 + 限流鉴权(附场景配置模板)
java·spring·spring cloud·gateway·搭建基础网关·现静态/动态路由配置·全局/局部过滤器
橘子真甜~5 小时前
Reids命令原理与应用2 - Redis网络层与优化,pipeline,发布订阅与事务
数据库·redis·缓存·事务·发布订阅·lua脚本·acid特性
努力也学不会java5 小时前
【Spring Cloud】初识Spring Cloud
运维·人工智能·后端·spring·机器学习·spring cloud
鼠爷ねずみ13 小时前
SpringCloud前后端整体开发流程-以及技术总结文章实时更新中
java·数据库·后端·spring·spring cloud
逆袭的菜鸟X16 小时前
极简HTTP缓存类封装
缓存
@淡 定18 小时前
Redis持久化机制
数据库·redis·缓存