从零开始 Spring Cloud 15:多级缓存
多级缓存架构
传统的缓存使用 Redis,大致架构如下:
这个架构存在一些问题:
-
请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
-
Redis缓存失效时,会对数据库产生冲击
可以使用多级缓存来解决这个问题:
具体过程为:
- 浏览器访问静态资源时,优先读取浏览器本地缓存
- 访问非静态资源(ajax查询数据)时,访问服务端
- 请求到达Nginx后,优先读取 Nginx本地缓存
- 如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat)
- 如果Redis查询未命中,则查询Tomcat
- 请求进入Tomcat后,优先查询JVM进程缓存
- 如果JVM进程缓存未命中,则查询数据库
传统架构中的 Nginx 仅仅充当 Tomcat 的反向代理服务器,所以只要部署一个实例就可以了,在这个新架构中,Nginx 上同样要实现业务逻辑,以便直接从 Redis 上查询业务数据,所以为了提高可靠性, Nginx 同样需要集群部署:
当然,Tomcat 作为主要业务代码的托管方,也要集群部署:
JVM 进程缓存
导入案例
在继续教程前,需要先导入示例工程,具体可以阅读案例导入说明。
Caffine
缓存可以按照存储的位置大致分为两类:
- 分布式缓存,例如Redis:
- 优点:存储容量更大、可靠性更好、可以在集群间共享
- 缺点:访问缓存有网络开销
- 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
- 进程本地缓存,例如HashMap、GuavaCache:
- 优点:读取本地内存,没有网络开销,速度更快
- 缺点:存储容量有限、可靠性较低、无法共享
- 场景:性能要求较高,缓存数据量较小
下面我们会使用 Caffeine 作为本地缓存。
Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址:https://github.com/ben-manes/caffeine
简单示例
示例项目中已经引入了 Caffine 的依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
用 Caffine 写一个使用本地缓存的简单示例:
@Test
void simpleTest(){
Cache<String, String> cache = Caffeine.newBuilder()
.build();
String name = cache.getIfPresent("name");
if (name == null){
System.out.println("缓存未命中,写入缓存");
cache.put("name", "icexmoon");
System.out.println("从缓存读取");
name = cache.getIfPresent("name");
}
System.out.println(name);
}
Cache.getIfPresent
会在未命中缓存时返回 null,这和使用容器作为内存缓存时用法是类似的。不过 Caffine 提供更方便的 API:
@Test
void simpleTest2(){
Cache<String, String> cache = Caffeine.newBuilder()
.build();
String name = cache.get("name", key-> "icexmoon");
System.out.println(name);
}
Cache.get
方法会先检查缓存中是否有,如果有就直接返回,如果没有,就使用参数列表中的 Lamda 表达式生成缓存值,写入缓存,然后再返回。整个过程和上边的示例是相同的,但写法要简洁很多。
缓存驱逐
所有的缓存机制都需要考虑缓存驱逐(过期)的问题,以防止存储空间被耗尽。
Caffine 有三种缓存驱逐策略可选:
-
基于容量:设置缓存的数量上限
// 创建缓存对象 Cache<String, String> cache = Caffeine.newBuilder() .maximumSize(1) // 设置缓存大小上限为 1 .build();
-
基于时间:设置缓存的有效时间
// 创建缓存对象 Cache<String, String> cache = Caffeine.newBuilder() // 设置缓存有效期为 10 秒,从最后一次写入开始计时 .expireAfterWrite(Duration.ofSeconds(10)) .build();
-
基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。
注意:在默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。
实现 JVM 进程缓存
下面使用 Caffine 在示例项目中实现 JVM 进程缓存。
这里使用 Caffine 缓存商品详情和商品库存的查询结果。
首先在配置类中将缓存对象定义为 Spring Bean:
@Configuration
public class WebConfig {
/**
* 商品详情缓存
* @return
*/
@Bean
public Cache<Long, Item> itemCache() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10000)
.build();
}
/**
* 库存缓存
* @return
*/
@Bean
public Cache<Long, ItemStock> itemStockCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10000)
.build();
}
}
在 Controller 中使用缓存对象缓存查询结果:
@RestController
@RequestMapping("item")
public class ItemController {
// ...
@Autowired
private Cache<Long, Item> itemCache;
@Autowired
private Cache<Long, ItemStock> itemStockCache;
// ...
@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 itemStockCache.get(id, key -> stockService.getById(key));
}
}
Lua
在 Nginx 上实现业务代码需要使用 Lua 作为编程语言,所以下面先学习 Lua。
介绍
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。官网:https://www.lua.org/
Lua经常嵌入到C语言开发的程序中,例如游戏开发、游戏插件等。
Nginx本身也是C语言开发,因此也允许基于Lua做拓展。
Hello World
首先查看虚拟机上是否已经安装了 Lua:
[icexmoon@192 tmp]$ lua -v
Lua 5.4.4 Copyright (C) 1994-2022 Lua.org, PUC-Rio
如果没有安装,用包管理器安装: