第五章:Redis多级缓存

目录

概述

一、多级缓存架构原理

[1.1 传统缓存策略的局限](#1.1 传统缓存策略的局限)

[1.2 多级缓存架构优势](#1.2 多级缓存架构优势)

[1.3 架构演进](#1.3 架构演进)

二、JVM进程缓存实现

[2.1 缓存类型对比](#2.1 缓存类型对比)

分布式缓存(如Redis)

进程本地缓存(如Caffeine)

[2.2 Caffeine框架](#2.2 Caffeine框架)

基本API使用

缓存驱逐策略

[2.3 实战:商品查询JVM缓存](#2.3 实战:商品查询JVM缓存)

配置类

业务实现

三、Lua语法基础(Nginx编程必备)

[3.1 Lua简介](#3.1 Lua简介)

[3.2 基本语法](#3.2 基本语法)

数据类型

变量声明

循环结构

函数定义

条件控制

逻辑运算符

四、OpenResty与多级缓存实现

[4.1 OpenResty简介](#4.1 OpenResty简介)

[4.2 Nginx配置](#4.2 Nginx配置)

加载Lua模块

业务路由配置

负载均衡配置

[4.3 Lua工具类封装](#4.3 Lua工具类封装)

通用工具模块(common.lua)

商品查询业务(item.lua)

五、缓存预热与同步

[5.1 Redis缓存预热](#5.1 Redis缓存预热)

实现方案

[5.2 缓存同步策略](#5.2 缓存同步策略)

策略对比

Canal实现数据库变更监听

配置使用

六、多级缓存架构总结

[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 多级缓存架构优势

多级缓存充分利用请求处理各环节,形成分层缓存体系:

  1. 浏览器本地缓存:静态资源优先读取本地

  2. Nginx本地缓存:非静态请求先查询Nginx缓存

  3. Redis分布式缓存:Nginx未命中则查询Redis

  4. JVM进程缓存:请求进入Tomcat后优先查询本地缓存

  5. 数据库查询:最后才访问数据库

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);
});
缓存驱逐策略
  1. 基于容量:设置缓存数量上限

    java 复制代码
    Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(1000)
        .build();
  2. 基于时间:设置缓存有效期

    java 复制代码
    Cache<String, String> cache = Caffeine.newBuilder()
        .expireAfterWrite(Duration.ofSeconds(60))
        .build();
  3. 基于引用:软引用/弱引用(性能较差,不推荐)

注意: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 缓存同步策略

策略对比
  1. 设置有效期

    • 优点:简单方便

    • 缺点:时效性差

    • 场景:更新频率低,时效要求低

  2. 同步双写

    • 优点:强一致性

    • 缺点:代码侵入,耦合度高

    • 场景:一致性、时效性要求高

  3. 异步通知

    • 优点:低耦合,可多服务同步

    • 缺点:存在中间不一致状态

    • 场景:时效性要求一般,多服务需要同步

Canal实现数据库变更监听

基于MySQL主从同步原理,伪装为Slave节点监听Binlog变化。

配置使用
  1. 添加依赖
XML 复制代码
<dependency>
    <groupId>top.javatool</groupId>
    <artifactId>canal-spring-boot-starter</artifactId>
    <version>1.2.1-RELEASE</version>
</dependency>
  1. 配置文件
XML 复制代码
# yaml(yml)配置文件

canal:
  destination: example  # Canal集群名称
  server: 127.0.0.1:11111  # Canal服务地址
  1. 实体类映射
java 复制代码
@Data
@TableName("tb_item")
public class Item {
    @TableId(type = IdType.AUTO)
    @Id
    private Long id;
    
    @Column(name = "name")
    private String name;
    
    // ...其他字段
}
  1. 变更处理器
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 核心要点

  1. 分层缓存:浏览器 → Nginx → Redis → JVM → 数据库

  2. 就近原则:优先访问距离用户最近的缓存

  3. 性能阶梯:访问速度逐级递减,成本逐级降低

  4. 容错机制:上级缓存失效时自动降级到下级

6.2 技术选型

  • JVM缓存:Caffeine(高性能本地缓存)

  • Nginx扩展:OpenResty + Lua

  • 分布式缓存:Redis

  • 缓存同步:Canal(数据库变更监听)

6.3 最佳实践

  1. 缓存粒度:根据业务场景选择合适的缓存粒度

  2. 过期策略:热点数据长过期,频繁变化数据短过期

  3. 降级方案:缓存失效时要有降级策略

  4. 监控告警:监控各层缓存命中率,及时预警

  5. 容量规划:根据数据量和访问模式合理规划缓存容量

6.4 性能优化建议

  1. Nginx本地缓存:适合变化不频繁的静态数据

  2. Redis预热:避免冷启动导致的数据库压力

  3. 缓存key设计:遵循命名规范,便于管理和清理

  4. 批量操作:减少网络往返,提升效率

  5. 连接池配置:合理配置连接池参数,避免连接泄露

通过多级缓存架构,系统可以获得显著的性能提升,有效应对高并发场景,同时保证系统的可用性和扩展性。

相关推荐
我命由我123453 天前
Android 控件 - 最简单的 Notification、Application Context 应用于 Notification
android·java·开发语言·junit·android studio·android jetpack·android-studio
android_cai_niao3 天前
JUnit 4.x最新版本
junit·junit5·junit4
快乐肚皮4 天前
OpenResty:Nginx的进化之路
nginx·junit·openresty
别会,会就是不问4 天前
Junit4下Mockito包的使用
java·junit·单元测试
我命由我123455 天前
JUnit - 自定义 Rule
android·java·开发语言·数据库·junit·java-ee·android-studio
剑之所向6 天前
嵌入式之lua脚本
开发语言·junit·lua
weixin_4624462312 天前
在宝塔 Nginx 上安装与配置 lua-cjson 教程
nginx·junit·lua
移幻漂流15 天前
Lua的现状与机遇:技术生态全景及高潜力领域分析
junit·单元测试·lua
IMPYLH16 天前
Lua 的 Table 模块
开发语言·笔记·后端·junit·游戏引擎·lua