HM-SpringCloud微服务系列11.1【多级缓存的意义&JVM进程缓存】
HM-SpringCloud微服务系列11:多级缓存-高级篇
1. 什么是多级缓存
- 多级缓存是亿级流量的缓存方案
- 浏览器访问静态资源时,优先读取浏览器本地缓存
- 访问非静态资源(ajax查询数据)时,访问服务端
- 请求到达Nginx后,优先读取Nginx本地缓存
- 如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat)
- 如果Redis查询未命中,则查询Tomcat
- 请求进入Tomcat后,优先查询JVM进程缓存
- 如果JVM进程缓存未命中,则查询数据库
- 在多级缓存架构中,Nginx内部需要编写本地缓存查询、Redis查询、Tomcat查询的业务逻辑,因此这样的nginx服务不再是一个反向代理服务器,而是一个编写业务的Web服务器了。
- 因此这样的业务Nginx服务也需要搭建集群来提高并发,再有专门的nginx服务来做反向代理,如图:
或
- 另外,我们的Tomcat服务将来也会部署为集群模式,如图:
- 可见,多级缓存的关键有两个:
- 一个是在nginx中编写业务,实现nginx本地缓存、Redis、Tomcat的查询
- 另一个就是在Tomcat中实现JVM进程缓存
- 其中Nginx编程则会用到OpenResty框架结合Lua这样的语言。
2. JVM进程缓存
2.1 导入商品管理案例
详见https://www.cnblogs.com/yppah/p/16212294.html
2.2 初识Caffeine
2.2.1 本地进程缓存
-
缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:
- 分布式缓存,例如Redis:
- 优点:存储容量更大、可靠性更好、可以在集群间共享
- 缺点:访问缓存有网络开销
- 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
- 进程本地缓存,例如HashMap、GuavaCache:
- 优点:读取本地内存,没有网络开销,速度更快
- 缺点:存储容量有限、可靠性较低、无法共享
- 场景:性能要求较高,缓存数据量较小
- 分布式缓存,例如Redis:
-
Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址:https://github.com/ben-manes/caffeine
Caffeine的性能非常好,下图是官方给出的性能对比:
可以看到Caffeine的性能遥遥领先,所以课程学习利用Caffeine框架来实现JVM进程缓存
2.2.2 Caffeine示例
- 可以通过item-service项目中的单元测试来学习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.2.3 Caffeine入门
/*
基本用法测试
*/
@Test
void testBasicOps() {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder().build();
// 存数据
cache.put("gf", "谭越");
// 取数据1
String gf = cache.getIfPresent("gf"); //getIfPresent()不存在则返回null
System.out.println("gf = " + gf);
// 取数据2
// get()用key="defaultGF"去JVM缓存找数据,有就返回;不存在则去数据库查询
String defaultGF = cache.get("defaultGF", key -> {
// 这里可以去数据库根据 key查询value,进而返回
return "ty"; //假数据,此处上面应写查询数据库的逻辑
});
System.out.println("defaultGF = " + defaultGF);
}
/*
基于大小设置驱逐策略:
*/
@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"));
}
可以看到,在给线程清理足够时间后,前两个key的值被清空了,因为最大上限为1
/*
基于时间设置驱逐策略:
*/
@Test
void testEvictByTime() throws InterruptedException {
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 1秒
.build();
// 存数据
cache.put("gf", "谭越");
// 获取数据
System.out.println("gf: " + cache.getIfPresent("gf"));
// 休眠一会儿
Thread.sleep(1200L); //1.2秒-->缓存会被清空
System.out.println("gf: " + cache.getIfPresent("gf"));
}
2.3 实现JVM进程缓存
2.3.1 需求
利用Caffeine实现下列需求:
- 给根据id查询商品的业务添加缓存,缓存未命中时查询数据库
- 给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库
- 缓存初始大小为100
- 缓存上限为10000
2.3.2 实现
【第一步】
首先,我们需要定义两个Caffeine的缓存对象,分别保存商品、库存的缓存数据。
在item-service的com.heima.item.config
包下定义CaffeineConfig
类:
package com.heima.item.config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CaffeineConfig {
@Bean
public Cache<Long, Item> itemCache() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000) //实际上就是10000,_作用是方便读
.build();
}
@Bean
public Cache<Long, ItemStock> stockCache() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000) //实际上就是10000,_作用是方便读
.build();
}
}
【第二步】
然后,修改item-service中的com.heima.item.web
包下的ItemController类,添加缓存逻辑:
@RestController
@RequestMapping("item")
public class ItemController {
@Autowired
private IItemService itemService;
@Autowired
private IItemStockService stockService;
@Autowired
private Cache<Long, Item> itemCache;
@Autowired
private 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));
}
}
【第三步】
重启服务,访问测试
清空启动日志
访问http://localhost:8081/item/10001
可以看到控制台输出了查询SQL,现在再清空一下日志
然后,刷新http://localhost:8081/item/10001页面
发现这次访问,并没有打印SQL,即走了JVM缓存而没有去查数据库
PS:第一次访问时会访问数据库,所以会打印SQL,并且第一次访问后会将数据放入JVM缓存中
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!