目录
本文将通过理论与实践相结合的方式,全面讲解多级缓存架构的设计与实现。学习完本文后,您将能够:
深入理解多级缓存的概念及其优势
掌握JVM进程缓存的实现方法(基于Caffeine)
学习Lua语言基础及其在Nginx中的应用
完整实现包含Nginx本地缓存、Redis缓存和Tomcat缓存的四级缓存架构
掌握使用Canal实现数据库与缓存同步的方案
1. 什么是多级缓存
1.1 传统缓存架构的局限性
在传统的Web应用架构中,通常会采用如图所示的缓存策略:
请求到达Tomcat后,先查询Redis缓存,如果未命中则查询数据库。这种架构存在两个明显的问题:
性能瓶颈:所有请求都必须经过Tomcat处理,当并发量高时,Tomcat的处理能力成为整个系统的性能瓶颈
缓存雪崩风险:当Redis缓存失效时,大量请求会直接冲击数据库,可能导致数据库崩溃
1.2 多级缓存架构的优势
多级缓存的核心思想是充分利用请求处理的每个环节,分别添加缓存层,减轻Tomcat压力,提升整体服务性能。一个完整的多级缓存架构工作流程如下:
浏览器缓存:静态资源优先读取浏览器本地缓存
服务端请求:动态数据(AJAX请求)访问服务端
Nginx缓存:请求到达Nginx后,优先读取Nginx本地缓存
Redis查询:Nginx本地缓存未命中时,直接查询Redis(不经过Tomcat)
Tomcat查询:Redis查询未命中时,才查询Tomcat
JVM进程缓存:请求进入Tomcat后,优先查询JVM进程缓存
数据库查询:JVM进程缓存未命中时,最后才查询数据库
1.3 多级缓存架构的部署方案
在这种架构中,Nginx不再仅仅是反向代理服务器,而是需要编写本地缓存查询、Redis查询、Tomcat查询等业务逻辑的业务服务器。因此需要考虑以下部署方案:
业务Nginx集群:处理业务逻辑的Nginx服务需要集群化部署以提高并发能力
反向代理Nginx:前端使用专门的Nginx服务做负载均衡和反向代理
Tomcat集群:后端Tomcat服务同样需要集群化部署
实现多级缓存的两个关键技术点:
Nginx业务逻辑开发:使用OpenResty框架结合Lua语言实现Nginx本地缓存、Redis查询和Tomcat查询的业务逻辑
JVM进程缓存实现:在Tomcat中使用Caffeine等框架实现本地缓存
2. JVM进程缓存实现
2.1 案例准备
为了演示多级缓存的实现,我们准备一个商品查询的案例。具体导入步骤请参考相关文档。
2.2 Caffeine缓存框架
2.2.1 缓存分类
在分布式系统中,缓存通常分为两类:
缓存类型 | 代表技术 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
分布式缓存 | Redis | 存储容量大、可靠性高、集群共享 | 有网络开销 | 大数据量、高可靠性要求、集群共享 |
进程本地缓存 | Caffeine, GuavaCache | 无网络开销、速度极快 | 容量有限、可靠性低、无法共享 | 高性能要求、小数据量 |
2.2.2 Caffeine简介
Caffeine是基于Java 8开发的高性能本地缓存库,具有接近最优的命中率。Spring框架内部就使用了Caffeine作为缓存实现。
GitHub地址:https://github.com/ben-manes/caffeine
性能对比显示,Caffeine在各项指标上都遥遥领先:
2.2.3 基本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); // 智能获取:缓存未命中时自动查询数据库 String defaultGF = cache.get("defaultGF", key -> { // 模拟数据库查询 return "柳岩"; }); System.out.println("defaultGF = " + defaultGF); }
2.2.4 缓存清除策略
Caffeine提供三种缓存驱逐策略:
基于容量:
Cache<String, String> cache = Caffeine.newBuilder() .maximumSize(1) // 设置缓存大小上限 .build();
基于时间:
Cache<String, String> cache = Caffeine.newBuilder() .expireAfterWrite(Duration.ofSeconds(10)) // 写入后10秒过期 .build();
基于引用(不推荐):利用GC回收缓存数据,性能较差
注意:Caffeine不会立即清理过期数据,而是在读写操作或空闲时进行清理。
2.3 JVM进程缓存实现
2.3.1 需求分析
实现以下功能:
商品查询缓存:未命中时查询数据库
库存查询缓存:未命中时查询数据库
初始缓存大小:100
最大缓存大小:10000
2.3.2 实现代码
配置Caffeine缓存Bean:
@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(); } }
控制器实现:
@RestController @RequestMapping("item") public class ItemController { @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)); } }
3. Lua语法入门
3.1 Lua语言简介
Lua是一种轻量级脚本语言,用标准C编写并以源代码形式开放。设计目的是嵌入应用程序中,提供灵活的扩展和定制功能。
官网:The Programming Language Lua
Lua常用于游戏开发和插件系统。Nginx通过OpenResty支持Lua扩展,使其能够处理复杂业务逻辑。
3.2 基础语法
3.2.1 Hello World
创建hello.lua
文件:
print("Hello World!")
执行:lua hello.lua
3.2.2 变量与数据类型
Lua基本数据类型:
nil:空值
boolean:布尔
number:数字
string:字符串
table:表(数组和字典)
function:函数
userdata:用户数据
thread:线程
变量声明:
local str = 'hello' -- 字符串 local num = 21 -- 数字 local flag = true -- 布尔 local arr = {'java', 'python', 'lua'} -- 数组 local map = {name='Jack', age=21} -- 字典
3.2.3 循环结构
数组遍历:
for index,value in ipairs(arr) do print(index, value) end
字典遍历:
for key,value in pairs(map) do print(key, value) end
3.3 函数与条件控制
3.3.1 函数定义
function printArr(arr) for index, value in ipairs(arr) do print(value) end end
3.3.2 条件控制
if not arr then print('数组不能为空!') elseif #arr > 10 then print('数组过长') else print('数组正常') end
3.3.3 案例:安全打印数组
function safePrintArr(arr) if not arr then print('数组不能为空!') return end for index, value in ipairs(arr) do print(value) end end
4. 多级缓存完整实现
4.1 OpenResty安装与配置
OpenResty是基于Nginx的高性能Web平台,集成了Lua支持。安装步骤参考相关文档。
4.2 OpenResty快速入门
4.2.1 请求处理流程
浏览器发起AJAX请求
Windows Nginx反向代理到OpenResty集群
OpenResty处理请求并返回响应
4.2.2 配置Nginx监听
location /api/item { default_type application/json; content_by_lua_file lua/item.lua; }
4.2.3 编写Lua脚本
/usr/local/openresty/nginx/lua/item.lua
:
ngx.say('{"id":10001,"name":"SALSA AIR","price":17900}')
4.3 请求参数处理
4.3.1 获取路径参数
修改Nginx配置:
location ~ /api/item/(\d+) { default_type application/json; content_by_lua_file lua/item.lua; }
Lua脚本获取ID:
local id = ngx.var[1] ngx.say('{"id":'..id..'}')
4.4 查询Tomcat
4.4.1 Nginx内部请求API
local resp = ngx.location.capture("/path",{ method = ngx.HTTP_GET, args = {a=1,b=2}, })
4.4.2 封装HTTP工具
common.lua
:
local function 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请求失败") ngx.exit(404) end return resp.body end
4.4.3 JSON处理
使用cjson模块:
local cjson = require "cjson" local obj = cjson.decode(jsonStr) local json = cjson.encode(obj)
4.4.4 完整商品查询
local common = require("common") local read_http = common.read_http local cjson = require("cjson") local id = ngx.var[1] local itemJSON = read_http("/item/"..id, nil) local stockJSON = read_http("/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))
4.4.5 负载均衡优化
基于ID的hash负载均衡:
upstream tomcat-cluster { hash $request_uri; server 192.168.150.1:8081; server 192.168.150.1:8082; }
4.5 Redis缓存预热
@Component public class RedisHandler implements InitializingBean { @Autowired private StringRedisTemplate redisTemplate; public void afterPropertiesSet() { // 预热商品数据 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); } } }
4.6 查询Redis缓存
4.6.1 封装Redis工具
common.lua
扩展:
local redis = require("resty.redis") local red = redis:new() red:set_timeouts(1000, 1000, 1000) local function read_redis(ip, port, key) local ok, err = red:connect(ip, port) if not ok then return nil end local resp, err = red:get(key) if not resp then return nil end if resp == ngx.null then return nil end return resp end
4.6.2 实现多级查询
function read_data(key, path, params) -- 先查Redis local val = read_redis("127.0.0.1", 6379, key) if not val then -- Redis未命中查HTTP val = read_http(path, params) end return val end
4.7 Nginx本地缓存
4.7.1 共享字典配置
nginx.conf
:
lua_shared_dict item_cache 150m;
4.7.2 本地缓存实现
local item_cache = ngx.shared.item_cache function read_data(key, expire, path, params) -- 1.查本地缓存 local val = item_cache:get(key) if not val then -- 2.查Redis val = read_redis("127.0.0.1", 6379, key) if not val then -- 3.查HTTP val = read_http(path, params) end -- 写入本地缓存 item_cache:set(key, val, expire) end return val end
5. 缓存同步方案
5.1 数据同步策略
策略 | 实现方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|---|
设置有效期 | 缓存设置TTL | 简单方便 | 时效性差 | 更新频率低 |
同步双写 | 修改DB同时更新缓存 | 强一致性 | 耦合度高 | 一致性要求高 |
异步通知 | 通过MQ或Canal通知 | 低耦合 | 最终一致性 | 多个服务需要同步 |
5.2 Canal实现缓存同步
5.2.1 Canal原理
Canal伪装成MySQL从节点,监听binlog变化并通知客户端。
5.2.2 安装配置
参考相关文档安装配置Canal服务。
5.2.3 SpringBoot集成
引入依赖:
<dependency> <groupId>top.javatool</groupId> <artifactId>canal-spring-boot-starter</artifactId> <version>1.2.1-RELEASE</version> </dependency>
配置Canal:
canal: destination: heima server: 192.168.150.101:11111
编写监听器:
@CanalTable("tb_item") @Component public class ItemHandler implements EntryHandler<Item> { @Override public void update(Item before, Item after) { // 更新JVM缓存 itemCache.put(after.getId(), after); // 更新Redis redisHandler.saveItem(after); } // 实现insert/delete方法... }
总结
本文详细介绍了从浏览器到数据库的完整多级缓存架构实现,包含:
浏览器本地缓存
Nginx本地缓存
Redis分布式缓存
Tomcat JVM进程缓存
通过多级缓存架构,系统可以:
显著提高响应速度
大幅降低数据库压力
提高系统可用性和扩展性
同时,通过Canal实现的缓存同步方案,保证了缓存数据的最终一致性,使系统既保持了高性能,又保证了数据的准确性。