目录
[1.1 传统缓存策略的局限](#1.1 传统缓存策略的局限)
[1.2 多级缓存架构优势](#1.2 多级缓存架构优势)
[1.3 架构演进](#1.3 架构演进)
[2.1 缓存类型对比](#2.1 缓存类型对比)
[2.2 Caffeine框架](#2.2 Caffeine框架)
[2.3 实战:商品查询JVM缓存](#2.3 实战:商品查询JVM缓存)
[3.1 Lua简介](#3.1 Lua简介)
[3.2 基本语法](#3.2 基本语法)
[4.1 OpenResty简介](#4.1 OpenResty简介)
[4.2 Nginx配置](#4.2 Nginx配置)
[4.3 Lua工具类封装](#4.3 Lua工具类封装)
[5.1 Redis缓存预热](#5.1 Redis缓存预热)
[5.2 缓存同步策略](#5.2 缓存同步策略)
[6.1 核心要点](#6.1 核心要点)
[6.2 技术选型](#6.2 技术选型)
[6.3 最佳实践](#6.3 最佳实践)
[6.4 性能优化建议](#6.4 性能优化建议)
多级缓存架构通过分层缓存设计显著提升系统性能,包含浏览器、Nginx、Redis和JVM进程四级缓存。传统方案存在单点瓶颈,多级缓存利用Caffeine实现本地JVM缓存,OpenResty+Lua实现Nginx业务逻辑,通过Redis分布式缓存和Canal实现数据同步。该架构采用就近访问原则,支持缓存预热和分级降级,有效降低数据库压力,提高响应速度。最佳实践包括合理设置缓存粒度、过期策略和监控机制,适用于高并发场景,在保证系统可用性的同时实现性能优化。
概述
多级缓存是通过在请求处理的不同层级设置缓存,显著提升系统性能、减轻数据库压力的架构方案。传统缓存策略仅依赖Tomcat和Redis,存在单点瓶颈和缓存失效冲击数据库的问题。
一、多级缓存架构原理
1.1 传统缓存策略的局限
-
所有请求需经Tomcat处理,Tomcat成为性能瓶颈
-
Redis缓存失效时,大量请求直接冲击数据库
-
响应延迟较高,系统扩展性有限
1.2 多级缓存架构优势
多级缓存充分利用请求处理各环节,形成分层缓存体系:
-
浏览器本地缓存:静态资源优先读取本地
-
Nginx本地缓存:非静态请求先查询Nginx缓存
-
Redis分布式缓存:Nginx未命中则查询Redis
-
JVM进程缓存:请求进入Tomcat后优先查询本地缓存
-
数据库查询:最后才访问数据库
1.3 架构演进
-
Nginx从反向代理服务器转变为业务Web服务器
-
业务Nginx需要集群部署提高并发能力
-
Tomcat服务也需部署为集群模式
-
需要专门的Nginx服务做反向代理负载均衡
二、JVM进程缓存实现
2.1 缓存类型对比
分布式缓存(如Redis)
-
优点:存储容量大、可靠性高、集群共享
-
缺点:存在网络开销
-
场景:大数据量、高可靠性要求、集群共享
进程本地缓存(如Caffeine)
-
优点:本地内存读取、无网络开销、速度快
-
缺点:容量有限、可靠性低、无法共享
-
场景:高性能要求、缓存数据量小
2.2 Caffeine框架
基于Java8的高性能本地缓存库,Spring内部缓存默认实现。
基本API使用
java
// 创建缓存实例
Cache<String, String> cache = Caffeine.newBuilder().build();
// 存储数据
cache.put("key", "value");
// 获取数据(直接获取)
String value = cache.getIfPresent("key");
// 获取数据(带默认值函数)
String value = cache.get("key", k -> {
// 缓存未命中时查询数据库
return queryFromDatabase(k);
});
缓存驱逐策略
-
基于容量:设置缓存数量上限
javaCache<String, String> cache = Caffeine.newBuilder() .maximumSize(1000) .build(); -
基于时间:设置缓存有效期
javaCache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(Duration.ofSeconds(60)) .build(); -
基于引用:软引用/弱引用(性能较差,不推荐)
注意:Caffeine不会立即清理过期数据,而是在读写操作或空闲时进行驱逐。
2.3 实战:商品查询JVM缓存
配置类
java
@Configuration
public class CacheConfig {
@Bean
public Cache<Long, Item> itemCache() {
return Caffeine.newBuilder()
.initialCapacity(100) // 初始容量
.maximumSize(10000) // 最大容量
.build();
}
@Bean
public Cache<Long, ItemStock> stockCache() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10000)
.build();
}
}
业务实现
java
@RestController
@RequestMapping("/item")
public class ItemController {
@Autowired
private Cache<Long, Item> itemCache;
@Autowired
private Cache<Long, ItemStock> stockCache;
@GetMapping("/{id}")
public Item findById(@PathVariable Long id) {
return itemCache.get(id, key ->
itemService.query()
.ne("status", 3)
.eq("id", key)
.one()
);
}
@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable Long id) {
return stockCache.get(id, key ->
stockService.getById(key)
);
}
}
三、Lua语法基础(Nginx编程必备)
3.1 Lua简介
-
轻量级脚本语言,C语言实现
-
设计目的是嵌入应用程序提供扩展功能
-
Nginx通过Lua扩展实现业务逻辑编写
3.2 基本语法
数据类型
-
nil:空值
-
boolean:布尔
-
number:数字
-
string:字符串
-
table:表(数组/字典)
-
function:函数
-
userdata:用户数据
-
thread:协程
变量声明
Lua
-- 局部变量
local str = "hello"
local num = 100
local flag = true
-- 字符串拼接
local message = str .. " world"
-- 数组(角标从1开始)
local arr = {"java", "python", "lua"}
print(arr[1]) -- 输出"java"
-- 字典
local map = {name = "Tom", age = 20}
print(map.name) -- 输出"Tom"
循环结构
Lua
-- 遍历数组
for index, value in ipairs(arr) do
print(index, value)
end
-- 遍历字典
for key, value in pairs(map) do
print(key, value)
end
函数定义
Lua
function printTable(tbl)
if not tbl then
print("表不能为空")
return
end
for k, v in pairs(tbl) do
print(k, v)
end
end
条件控制
Lua
if score >= 90 then
print("优秀")
elseif score >= 60 then
print("及格")
else
print("不及格")
end
逻辑运算符
-
and:逻辑与
-
or:逻辑或
-
not:逻辑非
四、OpenResty与多级缓存实现
4.1 OpenResty简介
基于Nginx的高性能Web平台,集成Lua扩展能力,支持自定义业务逻辑。
4.2 Nginx配置
加载Lua模块
Lua
# nginx.conf
http {
# Lua模块路径
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
# 共享字典(本地缓存)
lua_shared_dict item_cache 150m;
}
业务路由配置
Lua
# nginx
server {
location ~ /api/item/(\d+) {
default_type application/json;
content_by_lua_file lua/item.lua;
}
# 反向代理到Tomcat集群
location /item {
proxy_pass http://tomcat-cluster;
}
}
负载均衡配置
Lua
# nginx
upstream tomcat-cluster {
hash $request_uri; # 基于URI的哈希负载均衡
server 192.168.1.101:8081;
server 192.168.1.102:8082;
}
4.3 Lua工具类封装
通用工具模块(common.lua)
Lua
local redis = require("resty.redis")
local cjson = require("cjson")
local _M = {}
-- Redis连接配置
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
-- 关闭Redis连接(实际放入连接池)
local function close_redis(redis_conn)
local ok, err = redis_conn:set_keepalive(10000, 100)
if not ok then
ngx.log(ngx.ERR, "Redis连接池回收失败: ", err)
end
end
-- Redis查询
function _M.read_redis(host, port, key)
local ok, err = red:connect(host, port)
if not ok then
ngx.log(ngx.ERR, "Redis连接失败: ", err)
return nil
end
local resp, err = red:get(key)
if not resp then
ngx.log(ngx.ERR, "Redis查询失败: ", err)
end
if resp == ngx.null then
resp = nil
end
close_redis(red)
return resp
end
-- HTTP查询
function _M.read_http(path, params)
local resp = ngx.location.capture(path, {
method = ngx.HTTP_GET,
args = params
})
if not resp then
ngx.log(ngx.ERR, "HTTP查询失败: ", path)
ngx.exit(404)
end
return resp.body
end
return _M
商品查询业务(item.lua)
Lua
-- 导入依赖
local common = require("common")
local cjson = require("cjson")
-- 获取共享字典(Nginx本地缓存)
local item_cache = ngx.shared.item_cache
-- 统一数据查询函数
function read_data(key, expire, path, params)
-- 1. 查询Nginx本地缓存
local val = item_cache:get(key)
if val then
return val
end
-- 2. 查询Redis
ngx.log(ngx.ERR, "本地缓存未命中,查询Redis: ", key)
val = common.read_redis("127.0.0.1", 6379, key)
if val then
item_cache:set(key, val, expire)
return val
end
-- 3. 查询Tomcat(HTTP)
ngx.log(ngx.ERR, "Redis未命中,查询HTTP: ", key)
val = common.read_http(path, params)
if val then
item_cache:set(key, val, expire)
end
return val
end
-- 主处理逻辑
local id = ngx.var[1]
-- 查询商品信息(缓存30分钟)
local itemJSON = read_data(
"item:id:" .. id,
1800, -- 30分钟
"/item/" .. id,
nil
)
-- 查询库存信息(缓存1分钟)
local stockJSON = read_data(
"item:stock:id:" .. id,
60, -- 1分钟
"/item/stock/" .. id,
nil
)
-- 数据合并与返回
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
item.stock = stock.stock
item.sold = stock.sold
ngx.say(cjson.encode(item))
五、缓存预热与同步
5.1 Redis缓存预热
解决冷启动问题,项目启动时加载热点数据到Redis。
实现方案
java
@Component
public class CachePreheat implements InitializingBean {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private ItemService itemService;
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public void afterPropertiesSet() throws Exception {
// 预热商品数据
List<Item> items = itemService.list();
for (Item item : items) {
String json = mapper.writeValueAsString(item);
redisTemplate.opsForValue()
.set("item:id:" + item.getId(), json);
}
// 预热库存数据
List<ItemStock> stocks = stockService.list();
for (ItemStock stock : stocks) {
String json = mapper.writeValueAsString(stock);
redisTemplate.opsForValue()
.set("item:stock:id:" + stock.getId(), json);
}
}
}
5.2 缓存同步策略
策略对比
-
设置有效期
-
优点:简单方便
-
缺点:时效性差
-
场景:更新频率低,时效要求低
-
-
同步双写
-
优点:强一致性
-
缺点:代码侵入,耦合度高
-
场景:一致性、时效性要求高
-
-
异步通知
-
优点:低耦合,可多服务同步
-
缺点:存在中间不一致状态
-
场景:时效性要求一般,多服务需要同步
-
Canal实现数据库变更监听
基于MySQL主从同步原理,伪装为Slave节点监听Binlog变化。
配置使用
- 添加依赖
XML
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-starter</artifactId>
<version>1.2.1-RELEASE</version>
</dependency>
- 配置文件
XML
# yaml(yml)配置文件
canal:
destination: example # Canal集群名称
server: 127.0.0.1:11111 # Canal服务地址
- 实体类映射
java
@Data
@TableName("tb_item")
public class Item {
@TableId(type = IdType.AUTO)
@Id
private Long id;
@Column(name = "name")
private String name;
// ...其他字段
}
- 变更处理器
java
@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler<Item> {
@Autowired
private Cache<Long, Item> itemCache;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void insert(Item item) {
updateCache(item);
}
@Override
public void update(Item before, Item after) {
updateCache(after);
}
@Override
public void delete(Item item) {
itemCache.invalidate(item.getId());
redisTemplate.delete("item:id:" + item.getId());
}
private void updateCache(Item item) {
// 更新JVM缓存
itemCache.put(item.getId(), item);
// 更新Redis缓存
try {
String json = new ObjectMapper().writeValueAsString(item);
redisTemplate.opsForValue()
.set("item:id:" + item.getId(), json);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
六、多级缓存架构总结
6.1 核心要点
-
分层缓存:浏览器 → Nginx → Redis → JVM → 数据库
-
就近原则:优先访问距离用户最近的缓存
-
性能阶梯:访问速度逐级递减,成本逐级降低
-
容错机制:上级缓存失效时自动降级到下级
6.2 技术选型
-
JVM缓存:Caffeine(高性能本地缓存)
-
Nginx扩展:OpenResty + Lua
-
分布式缓存:Redis
-
缓存同步:Canal(数据库变更监听)
6.3 最佳实践
-
缓存粒度:根据业务场景选择合适的缓存粒度
-
过期策略:热点数据长过期,频繁变化数据短过期
-
降级方案:缓存失效时要有降级策略
-
监控告警:监控各层缓存命中率,及时预警
-
容量规划:根据数据量和访问模式合理规划缓存容量
6.4 性能优化建议
-
Nginx本地缓存:适合变化不频繁的静态数据
-
Redis预热:避免冷启动导致的数据库压力
-
缓存key设计:遵循命名规范,便于管理和清理
-
批量操作:减少网络往返,提升效率
-
连接池配置:合理配置连接池参数,避免连接泄露
通过多级缓存架构,系统可以获得显著的性能提升,有效应对高并发场景,同时保证系统的可用性和扩展性。