Redis从入门到精通(十六)多级缓存(一)Caffeine、JVM进程缓存

ops/2024/10/21 5:42:39/

文章目录

  • 第6章 多级缓存
    • 6.1 什么是多级缓存
    • 6.2 搭建测试项目
      • 6.2.1 项目介绍
      • 6.2.2 新增商品表
      • 6.2.3 编写商品相关代码
      • 6.2.4 启动服务并测试
      • 6.2.5 导入商品查询页面,配置反向代理
    • 6.3 JVM进程缓存
      • 6.3.1 Caffeine
      • 6.3.2 实现JVM进程缓存
        • 6.3.2.1 需求分析
        • 6.3.2.2 代码实现

第6章 多级缓存

6.1 什么是多级缓存

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

但这样的策略存在以下两个问题:

  • 请求要经过Tomcat处理,那Tomcat的性能成为整个系统的瓶颈;
  • Redis缓存失效时,会对数据库产生冲击。

而多级缓存就是要充分利用请求处理的各个环节,分别添加缓存,减轻Tomcat压力,如图:

  • 1)浏览器访问静态资源时,优先读取浏览器本地缓存;访问非静态资源时,才访问服务端。
  • 2)请求到达Nginx后,优先读取Nginx本地缓存
  • 3)如果Nginx本地缓存未命中,则去查询Redis
  • 4)如果Redis查询未命中,则将请求发送到Tomcat,并优先查询JVM进程缓存
  • 5)如果JVM进程缓存未命中,则查询数据库

在以上多级缓存架构中,Nginx服务已经不再是一个反向代理服务器,而是一个需要编写本地缓存查询、Redis查询、Tomcat查询业务逻辑的业务服务器。

进一步优化,业务Nginx服务也会搭建集群来提高并发,还有专门的Nginx服务来做反向代理,同时Tomcat服务也会部署成集群模式,如图:

综上,多级缓存的关键有两个:

  • 1)在业务Nginx中编写业务逻辑,实现Nginx本地缓存、Redis、Tomcat的查询,这里会用到OpenResty框架结合Lua语言;
  • 2)在Tomcat中实现JVM进程缓存

6.2 搭建测试项目

6.2.1 项目介绍

这里继续沿用【第4章 Redis实战】系列文章中编写的测试项目,代码下载地址见文末。↓↓↓

该项目的结构如下:

其中的业务包括:

  • 获取验证码;用户登录;用户签到;用户签到统计。
  • 查询商户列表;根据ID查询商户详情;新增商户;修改商户;图片上传。
  • 新增探店笔记;根据ID查询笔记详情;点赞/取消点赞功能;查询点赞排行榜;查询关注用户的探店笔记列表。
  • 关注或者取消关注功能;查询共同关注好友。
  • 添加普通优惠券;添加秒杀优惠券;秒杀下单。

6.2.2 新增商品表

数据库创建一个商品表tb_item并初始化几条数据:

DROP TABLE IF EXISTS `tb_item`;
CREATE TABLE `tb_item` (`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '商品id',`title` VARCHAR(264) NOT NULL COMMENT '商品标题',`name` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '商品名称',`price` BIGINT(20) NOT NULL COMMENT '价格(分)',`image` VARCHAR(200) NULL DEFAULT NULL COMMENT '商品图片',`category` VARCHAR(200) NULL DEFAULT NULL COMMENT '类目名称',`brand` VARCHAR(100) NULL DEFAULT NULL COMMENT '品牌名称',`spec` VARCHAR(200) NULL DEFAULT NULL COMMENT '规格',`status` INT(1) NOT NULL DEFAULT 1 COMMENT '商品状态 1-正常,2-下架,3-删除',`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE,INDEX `status`(`status`) USING BTREE,INDEX `updated`(`update_time`) USING BTREE
) CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '商品表';INSERT INTO `tb_item` VALUES (1, 'RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4', 'SALSA AIR', 16900, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp', '拉杆箱', 'RIMOWA', '{\"颜色\": \"红色\", \"尺码\": \"26寸\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (2, '安佳脱脂牛奶 新西兰进口轻欣脱脂250ml*24整箱装*2', '脱脂牛奶', 68600, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t25552/261/1180671662/383855/33da8faa/5b8cf792Neda8550c.jpg!q70.jpg.webp', '牛奶', '安佳', '{\"数量\": 24}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (3, '唐狮新品牛仔裤女学生韩版宽松裤子 A款/中牛仔蓝(无绒款) 26', '韩版牛仔裤', 84600, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t26989/116/124520860/644643/173643ea/5b860864N6bfd95db.jpg!q70.jpg.webp', '牛仔裤', '唐狮', '{\"颜色\": \"蓝色\", \"尺码\": \"26\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (4, '森马(senma)休闲鞋女2019春季新款韩版系带板鞋学生百搭平底女鞋 黄色 36', '休闲板鞋', 10400, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t1/29976/8/2947/65074/5c22dad6Ef54f0505/0b5fe8c5d9bf6c47.jpg!q70.jpg.webp', '休闲鞋', '森马', '{\"颜色\": \"白色\", \"尺码\": \"36\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');
INSERT INTO `tb_item` VALUES (5, '花王(Merries)拉拉裤 M58片 中号尿不湿(6-11kg)(日本原装进口)', '拉拉裤', 38900, 'https://m.360buyimg.com/mobilecms/s720x720_jfs/t24370/119/1282321183/267273/b4be9a80/5b595759N7d92f931.jpg!q70.jpg.webp', '拉拉裤', '花王', '{\"型号\": \"XL\"}', 1, '2019-05-01 00:00:00', '2019-05-01 00:00:00');

再创建一个商品库存表tb_item_stock并初始化几条数据:

DROP TABLE IF EXISTS `tb_item_stock`;
CREATE TABLE `tb_item_stock`  (`item_id` BIGINT(20) NOT NULL COMMENT '商品id,关联tb_item表',`stock` INT(10) NOT NULL DEFAULT 9999 COMMENT '商品库存',`sold` INT(10) NOT NULL DEFAULT 0 COMMENT '商品销量',PRIMARY KEY (`item_id`) USING BTREE
) CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '商品库存表';INSERT INTO `tb_item_stock` VALUES (1, 99996, 3219);
INSERT INTO `tb_item_stock` VALUES (2, 99999, 54981);
INSERT INTO `tb_item_stock` VALUES (3, 99999, 189);
INSERT INTO `tb_item_stock` VALUES (4, 99999, 974);
INSERT INTO `tb_item_stock` VALUES (5, 99999, 18649);

6.2.3 编写商品相关代码

首先创建tb_itemtb_item_stock表对应的实体类:

java">// com.star.redis.dzdp.pojo.Item/**** 商品* @author hsgx* @since 2024/4/12 10:01*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tb_item")
public class Item implements Serializable {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.AUTO)private Long id;private String title;private String name;private Double price;private String image;private String category;private String brand;private String spec;private Integer status;private Date crateTime;private Date updateTime;}
java">// com.star.redis.dzdp.pojo.ItemStock/**** 商品库存表* @author hsgx* @since 2024/4/12 10:05*/
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("tb_item_stock")
public class ItemStock {private static final long serialVersionUID = 1L;@TableId(value = "item_id", type = IdType.AUTO)private Long itemId;private Integer stock;private Integer sold;}

然后创建商品Item实体对应的ItemController类-IItemService接口-ItemServiceImpl实现类-ItemMapper接口,创建商品库存ItemStock实体对应的IItemStockService接口-ItemStockServiceImpl实现类-ItemStockMapper接口,均使用MyBatis-Plus来实现。

接下来在ItemController类中实现两个接口,其接口文档和代码如下:

项目说明
功能根据ID查询商品信息
请求方法GET
请求路径/item/{id}
请求方法id:Long,商品ID
请求方法Item:商品信息
项目说明
功能根据ID查询商品库存信息
请求方法GET
请求路径/item/stock/{id}
请求方法id:Long,商品ID
请求方法Item:商品库存信息
java">// com.star.redis.dzdp.controller.ItemController@Slf4j
@RestController
@RequestMapping("/item")
public class ItemController {@Resourceprivate IItemService itemService;/*** 根据ID查询商品信息* @author hsgx* @since 2024/4/12 10:17* @param id* @return com.star.redis.dzdp.pojo.Item*/@GetMapping("/{id}")public Item queryById(@PathVariable("id") Long id) {return itemService.getById(id);}/*** 根据ID查询商品库存信息* @author xiaowd* @since 2024/4/12 14:22* @param id* @return com.star.redis.dzdp.pojo.ItemStock*/@GetMapping("/stock/{id}")public ItemStock queryStockById(@PathVariable("id") Long id) {return itemStockService.getById(id);}}

6.2.4 启动服务并测试

6.2.5 导入商品查询页面,配置反向代理

这里已经准备好了一个Nginx反向代理服务器和静态资源,下载地址见文末。↓↓↓

将该文件夹拷贝到一个非中文目录下,然后修改conf/nginx.conf文件以配置反向代理:

# nginx-1.18.0/conf/nginx.conf#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;server {listen       8082;server_name  localhost;location /api {proxy_pass http://127.0.0.1:8081/dzdp;}location / {root   html;index  index.html index.htm;}error_page   500 502 503 504  /50x.html;location = /50x.html {root   html;}}
}

打开cmd窗口,执行start nginx.exe命令运行服务。然后在浏览器访问http://localhost:8082/item.html?id=1,显示如下页面:

在上述页面中打开控制台,可以看到ajax向后台发起请求,并成功拿到数据:

至此,测试项目搭建完毕。

6.3 JVM进程缓存

6.3.1 Caffeine

缓存一般可以分为两类:

  • 分布式缓存,例如Redis:

    • 优点:存储容量更大、可靠性更好、可以在集群间共享
    • 缺点:访问缓存有网络开销
    • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
  • 进程本地缓存,例如HashMap、GuavaCache:

    • 优点:读取本地内存,没有网络开销,速度更快
    • 缺点:存储容量有限、可靠性较低、无法共享
    • 场景:性能要求较高,缓存数据量较小

Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部使用的缓存就是Caffeine。

Caffeine的GitHub地址:https://github.com/ben-manes/caffeine

要使用Caffeine,首先要引入Caffeine的依赖:

<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>2.7.0</version>
</dependency>

下面是Caffeine的基本API的使用案例:

java">@Test
public void testCaffeine() {// 构建cache对象Cache<String, String> cache = Caffeine.newBuilder().build();// 存数据cache.put("name", "Jack");// 取数据String name = cache.getIfPresent("name");System.out.println("name = " + name);// 取数据:先查缓存,如果未命中则执行Lambda表达式// 参数1:缓存的key// 参数2:Lambda表达式的参数即缓存的keyString age = cache.get("age", key -> {return "25";});System.out.println("age = " + age);
}

运行以上代码,结果如下:

name = Jack
age = 25

Caffeine提供了三种缓存清除策略:

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

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

6.3.2 实现JVM进程缓存

6.3.2.1 需求分析

利用Caffeine实现以下需求:

6.3.2.2 代码实现

首先定义两个Caffeine缓存对象,分别保存商品信息、商品库存的缓存数据:

java">// com.star.redis.dzdp.config.CaffeineConfig/**** Caffeine缓存配置* @author hsgx* @since 2024/4/12 14:29*/
@Configuration
public class CaffeineConfig {@Beanpublic Cache<Long, Item> itemCache() {return Caffeine.newBuilder().initialCapacity(1000).maximumSize(10000).build();}@Beanpublic Cache<Long, ItemStock> itemStockCache() {return Caffeine.newBuilder().initialCapacity(1000).maximumSize(10000).build();}
}

接着修改ItemController类的queryById()方法和queryStockById()方法:

java">@Resource
private Cache<Long, Item> itemCache;
@Resource
private Cache<Long, ItemStock> itemStockCache;@GetMapping("/{id}")
public Item queryById(@PathVariable("id") Long id) {//return itemService.getById(id);// 添加缓存return itemCache.get(id, key -> {return itemService.getById(id);});
}@GetMapping("/stock/{id}")
public ItemStock queryStockById(@PathVariable("id") Long id) {//return itemStockService.getById(id);// 添加缓存return itemStockCache.get(id, key -> {return itemStockService.getById(id);});
}

本节完,下一节将正式进入多级缓存的实现。

本节所涉及的代码和资源可从git仓库下载:https://gitee.com/weidag/redis_learning.git

更多内容请查阅分类专栏:Redis从入门到精通

感兴趣的读者还可以查阅我的另外几个专栏:

  • SpringBoot源码解读与原理分析(已完结)
  • MyBatis3源码深度解析(已完结)
  • 再探Java为面试赋能(持续更新中…)

http://www.ppmy.cn/ops/4055.html

相关文章

第十章 单调栈part03

今天是代码随想录一刷结束&#xff01;&#xff01; 不敢想象坚持了60天。 84. 柱状图中最大的矩形 class Solution:def largestRectangleArea(self, heights: List[int]) -> int:heights.insert(0, 0)heights.append(0)stack [0]result 0for i in range(1, len(height…

iOS依赖库版本一致性检测:确保应用兼容性

一、背景 在 iOS 应用开发的世界里&#xff0c;每次 Xcode 更新都带来了新的特性和挑战。最近的 Xcode 15 更新不例外&#xff0c;这次升级引入了对 SwiftUI 的自动强依赖。SwiftUI最低是从 iOS 13 开始支持。 这一变化也带来了潜在的兼容性问题。如果您的项目在升级到 Xcode…

最小生成树算法的实现c++

最小生成树算法的实现c 题目链接&#xff1a;1584. 连接所有点的最小费用 - 力扣&#xff08;LeetCode&#xff09; 主要思路&#xff1a;使用krusal算法&#xff0c;将边的权值进行排序&#xff08;从小到大排序&#xff09;&#xff0c;每次将权值最小且未加入到连通分量中…

docker 环境变量设置实现方式

1、前言 docker在当前运用的越来广泛&#xff0c;很多应用或者很多中间软件都有很多docker镜像资源&#xff0c;运行docker run 启动镜像资源即可应用。但是很多应用或者中间件有很多配置参数。这些参数在运用过程怎么设置给docker 容器呢&#xff1f;下面介绍几种方式 2 、do…

域名被污染了只能换域名吗?

域名污染是指域名的解析结果受到恶意干扰或篡改&#xff0c;使得用户在访问相关网站时出现异常。很多域名遭遇过污染的情况&#xff0c;但是并不知道是域名污染&#xff0c;具体来说&#xff0c;域名污染可能表现为以下情况&#xff1a;用户无法通过输入正确的域名访问到目标网…

逐步学习Go-Slice(切片还可以多挖一下)

特性 扩缩容&#xff1a;只有自动扩容&#xff0c;没有自动缩容原地与非原地&#xff1a;切片有原地有非原地操作 自动扩容 Slice只有自动扩容&#xff0c;不会自动缩容&#xff0c;而且自动扩容会根据当前slice的容量来计算不同的扩容系数&#xff0c;具体容量和扩展因子关…

多线程8

void 取款(){int oldBalance balance if(!CAS(balance,oldBalance,oldBalance-500)){}} 在这个线程中如果变成了这样 void 取款(){int oldBalance balancevoid 取款(){int oldBalance balance if(!CAS(balance,oldBalance,oldBalance-500)){}有人转账发生了500->1000。} i…

量子密钥分发系统设计与实现(一):系统基本架构讨论

经过一段时间讨论&#xff0c;我们了解到量子密钥分发设备是当前量子保密通信系统的基础。从本文开始&#xff0c;我将开启量子密钥分发系统设计与实现系列&#xff0c;详细讨论量子密钥分发设备如何从0到1的搭建。 1.QKD系统总体讨论 QKD系统的核心功能就是为通信双方提供理论…