当前位置: 首页 > news >正文

微服务多级缓存:从问题到实战(小白也能看懂的亿级流量方案)

一、先搞懂:为啥要搞 “多级缓存”?

先从 “传统缓存” 的坑说起 —— 以前做缓存,基本是 “请求→Tomcat→查 Redis→没命中就查数据库”。但这方案扛不住大流量,问题很明显:

  1. Tomcat 成瓶颈:所有请求都要经过 Tomcat 处理,Tomcat 的性能有限,一旦流量大了就卡壳;
  2. 数据库遭 “暴击”:要是 Redis 缓存过期 / 没命中,所有请求会直接冲去查数据库,数据库很容易崩。
    而 “多级缓存” 的思路很简单:在请求经过的每一步都加缓存,能在前面拦住的请求,就别往后传。既减轻 Tomcat 压力,又保护数据库,还能提速。

二、多级缓存有哪些 “层级”?

核心是 5 个层级,从用户端到数据库依次拦截,咱们按 “从近到远” 排:

  1. 浏览器缓存:用户电脑里存一份(比如图片、静态数据),下次打开页面直接用,不用问服务器要;
  2. Nginx 缓存:请求到了服务器入口(Nginx),直接从 Nginx 拿数据,不用传给后面的服务;
  3. Redis 缓存:分布式缓存,多台服务能共享数据,比查数据库快;
  4. JVM 进程缓存:服务自己内存里的缓存(比如 Tomcat 的内存),拿数据不用走网络,最快;
  5. 数据库:最后兜底的,实在没缓存才查。

注意:用作缓存的Nginx是业务Nginx,需要部署为集群,再有专门的Nginx用来做反向代理:
在这里插入图片描述

三、逐个拆解:每个层级怎么实现?

1. 最 “近” 的缓存:JVM 进程缓存(服务自己的内存)

啥是进程缓存?
就是把数据存在服务的内存里(比如 Tomcat 的内存),查数据不用走网络(不用连 Redis / 数据库),速度最快。但缺点也明显:只能自己用(多台服务不共享)、存不下太多数据。

对比一下常用的两种缓存,小白一看就懂:

缓存类型优点缺点适合场景
分布式缓存(Redis)存得多、多服务共享、靠谱查数据要走网络,稍慢数据量大、要共享的场景
进程缓存(本地如HashMap、GuavaCache)查数据不用走网络,最快存得少、不共享、重启就没了数据量小、要极速查的场景

Caffeine

Caffeine是一个基于Java8开发的, 提供了近乎最佳命中率的高性能的本地缓存库。Caffeine 是 Java 里最常用的进程缓存工具(Spring 内部也用它)。
Caffeine示例


void testBasicOps({//创建缓存对象Cache<StringString> cache =Caffeine.newBuilder().build();//存数据cache.put("food", "螺蛳粉");//取数据,不存在则返回nullString food=cache.getIfPresent("gf");System.out.println("food ="+food);//取数据,不存在则去数据库查询String defaultFood=cache.get"defaultFood", key->{//这里可以去数据库根据key查询valuereturn "巧克力";);System.out.println("defaultGF="+defaultFood);
}

实战:用 Caffeine 实现进程缓存

简单 3 步搞定:

第一步:导入依赖(Spring 项目直接加)

<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId>
</dependency>

第二步:配置缓存(告诉 Caffeine 存多少、怎么删旧数据)

写个配置类,定义两个缓存:一个存商品信息,一个存库存信息:

import com.github.ben-manes.caffeine.cache.Cache; import com.github.ben-manes.caffeine.cache.Caffeine; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; public class CaffeineConfig {// 商品信息缓存:初始存100条,最多存10000条public Cache<Long, Item> itemCache() {return Caffeine.newBuilder().initialCapacity(100)  // 初始容量.maximumSize(10000)    // 最大容量(超了就删旧数据).build();}// 库存信息缓存:同上public Cache<Long, ItemStock> stockCache() {return Caffeine.newBuilder().initialCapacity(100).maximumSize(10000).build();} }

第三步:在 Controller 里用缓存
查数据时先看缓存,没有再查数据库,小白也能看懂的逻辑:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.github.ben-manes.caffeine.cache.Cache;
public class ItemController {// 注入配置好的缓存private Cache<Long, Item> itemCache;private Cache<Long, ItemStock> stockCache;// 查商品信息:先查缓存,没再查数据库("/item/{id}")public Item findById( Long id) {// 缓存里有就直接拿,没有就执行后面的“查数据库”逻辑return itemCache.get(id, key -> itemService.query().ne("status", 3)  // 排除下架商品.eq("id", key)     // 按ID查.one());           // 查一条}// 查库存:逻辑同上("/item/stock/{id}")public ItemStock findStockById( Long id) {return stockCache.get(id, key -> stockService.getById(key));}
}

Caffeine 的 “删旧数据” 策略
缓存满了 / 数据过期了,Caffeine 会自动删,常用 3 种方式:

  1. 按容量删:比如最多存 10000 条,超了删最久不用的;
  2. 按时间删:比如数据存 10 秒就过期(适合时效性强的 data);
  3. 按引用删:靠 Java 的 GC 回收(性能差,不用)。

例子:设置 “数据最多存1条”:

Cache<String, String> cache = Caffeine.newBuilder()// 设置缓存大小上限为 1.maximumSize(1).build();

tip:容量为1,只有最后设置的一个key可以保存下来

例子:设置 “数据存 10 秒过期”:

Cache<String, String> cache = Caffeine.newBuilder().expireAfterWrite(Duration.ofSeconds(10))  // 从最后一次存数据开始算,10秒过期.build();

2. 服务入口的缓存:Nginx 缓存(用 OpenResty)

Nginx 是服务的 “大门”,但普通 Nginx 不能写逻辑,所以用OpenResty(带 Lua 脚本的 Nginx,能自定义缓存逻辑)。
先搞懂:OpenResty 是啥?
简单说:OpenResty = Nginx + Lua 脚本,能在 Nginx 里写代码,实现 “查本地缓存→查 Redis→查 Tomcat” 的逻辑,不用把请求传给 Tomcat。

实战:用 OpenResty 实现 Nginx 缓存

核心逻辑:用户请求来的时候,OpenResty 先查自己的本地缓存,没有就查 Redis,再没有就查 Tomcat,最后把数据存到缓存里。
第一步:安装 OpenResty
不用记命令,跟着步骤走(以 Linux 为例):

  1. 下载安装包
  2. 解压后执行 ./configure;
  3. 执行 make && make install;
  4. 启动:/usr/local/openresty/nginx/sbin/nginx。
    OpenResty底层是基于Nginx的,查看OPenResty目录的nginx目录,结构与windows中安装的nginx基本一致:
    在这里插入图片描述

第二步:配置 Nginx(让它能跑 Lua 脚本)
修改 OpenResty 的 Nginx 配置文件(路径:/usr/local/openresty/nginx/conf/nginx.conf):

  1. http节点下加 Lua 模块路径(让 Nginx 能找到 Lua 库):
    在这里插入图片描述
http {# 加载Lua模块lua_package_path "/usr/local/openresty/lualib/?.lua;;";lua_package_cpath "/usr/local/openresty/lualib/?.so;;";# 开启Nginx本地缓存(给缓存起个名:item_cache,大小150M)lua_shared_dict item_cache 150m;    # 其他配置...
}
  1. server节点下加 “监听 /api/item 路径”(用户查商品走这个逻辑):
server {listen 8081;  # 端口server_name localhost;# 用户请求/api/item/xxx时,用Lua脚本处理location ~ /api/item/(\d+) {  # 正则匹配:比如/api/item/1001,\d+就是商品IDdefault_type application/json;  # 返回JSON格式content_by_lua_file lua/item.lua;  # 用lua/item.lua脚本处理}
}
  1. 第三步:写 Lua 脚本(实现缓存逻辑)

在 nginx 目录下建lua文件夹,新建item.lua脚本,核心代码如下:

-- 1. 导入需要的库:操作JSON、Redis、本地缓存
local cjson = require "cjson"  -- 处理JSON
local redis = require "resty.redis"  -- 操作Redis
local item_cache = ngx.shared.item_cache  -- 获取Nginx本地缓存-- 2. 封装:查Redis的函数
local function read_redis(ip, port, key)local red = redis:new()red:set_timeouts(1000, 1000, 1000)  -- 超时时间-- 连接Redislocal ok, err = red:connect(ip, port)if not ok thenngx.log(ngx.ERR, "连Redis失败:", err)return nilend--Redislocal val, err = red:get(key)if not val or val == ngx.null thenngx.log(ngx.ERR, "Redis没找到key:", key)val = nilend--Redis连接放回连接池(复用,不浪费)red:set_keepalive(10000, 100)return val
end-- 3. 封装:查Tomcat的函数(用Nginx内部请求)
local function read_http(path)--Tomcat发请求(比如Tomcat地址是192.168.150.1:8081)local resp = ngx.location.capture(path, {method = ngx.HTTP_GET})if not resp or resp.status ~= 200 thenngx.log(ngx.ERR, "查Tomcat失败:", path)return nilendreturn resp.body
end-- 4. 核心逻辑:先查本地缓存→再查Redis→最后查Tomcat
local function read_data(key, expire, path)-- 第一步:查Nginx本地缓存local val = item_cache:get(key)if val thenreturn val  -- 有缓存直接返回end-- 第二步:查Redisval = read_redis("127.0.0.1", 6379, key)if val thenitem_cache:set(key, val, expire)  -- 存到本地缓存return valend-- 第三步:查Tomcatval = read_http(path)if val thenitem_cache:set(key, val, expire)  -- 存到本地缓存和Redis(这里省略Redis存逻辑)return valendreturn nil  -- 都没查到,返回空
end-- 5. 处理请求:获取商品ID,调用上面的逻辑
local id = ngx.var[1]  -- 从URL里拿商品ID(比如/api/item/1001,这里就是1001)
local item_key = "item:id:" .. id  -- Redis的key:item:id:1001
local item_path = "/item/" .. id   --Tomcat的路径:/item/1001-- 查商品信息:本地缓存存30分钟(1800秒),库存存1分钟(60秒)
local item_json = read_data(item_key, 1800, item_path)
ngx.say(item_json)  -- 返回数据给用户

3. 分布式缓存:Redis(多服务共享数据)

Redis 是多级缓存里的 “中间层”,多台服务能共享数据。关键要解决两个问题:
问题 1:服务刚启动,Redis 没数据怎么办?(缓存预热)
服务刚启动时,Redis 是空的(叫 “冷启动”),这时候请求会全冲去查数据库。解决办法是缓存预热:服务启动时,主动把热点数据(比如所有商品)查出来存到 Redis。
实战代码(Spring 项目):



public class RedisPreheat implements InitializingBean {private StringRedisTemplate redisTemplate;private ItemService itemService;// 服务启动后自动执行这个方法(InitializingBean的特性)public void afterPropertiesSet() throws Exception {// 1. 查所有商品List<Item> itemList = itemService.list();// 2. 存到Redisfor (Item item : itemList) {String key = "item:id:" + item.getId();String json = JSON.toJSONString(item);redisTemplate.opsForValue().set(key, json);}}
}

问题 2:多台 Tomcat 的进程缓存不共享怎么办?(负载均衡 hash)
多台 Tomcat 的进程缓存是各自独立的,比如商品 1001 的缓存存在 Tomcat A,但请求被分到 Tomcat B,就会查不到缓存。解决办法:让同一商品的请求永远到同一台 Tomcat
在 Nginx 配置里改负载均衡规则(用hash $request_uri):

http {# Tomcat集群:用请求URL的hash值分配,同一URL永远到同一台Tomcatupstream tomcat-cluster {hash $request_uri;  # 关键:按请求路径hashserver 192.168.150.1:8081;  # Tomcat 1server 192.168.150.1:8082;  # Tomcat 2}# 反向代理到Tomcat集群location /item {proxy_pass http://tomcat-cluster;}
}

四、关键问题:缓存和数据库怎么同步?

缓存里的数据是从数据库来的,如果数据库数据改了(比如商品价格变了),缓存里的旧数据就会导致 “数据不一致”。常用 3 种同步方案:

同步方案优点缺点适合场景
设置有效期简单,不用写额外代码过期前数据可能不一致数据更新慢(比如商品分类)
同步双写实时一致,改数据库时同步改缓存代码耦合高(改数据库要顺带改缓存)数据更新快、要实时一致(比如订单)
异步通知低耦合,改数据库后发通知就行有延迟(通知到缓存更新有时间差)数据更新快、能接受小延迟(比如商品库存)

MQ异步通知 VS Canal异步通知
在这里插入图片描述
在这里插入图片描述

实战:用 Canal 实现 “异步通知”

Canal 是阿里开源工具,能监听 MySQL 的变化(比如新增 / 修改 / 删除数据),然后通知服务更新缓存。
第一步:安装 Canal

  1. 先开启 MySQL 的 binlog(Canal 靠 binlog 监听变化);
  2. 下载 [Canal];
  3. 配置 Canal,指定要监听的 MySQL 地址和数据库;
  4. 启动 Canal。

第二步:Spring 项目监听 Canal 通知
1. 导入依赖:

<dependency><groupId>top.javatool</groupId><artifactId>canal-spring-boot-starter</artifactId><version>1.2.1-RELEASE</version>
</dependency>

2. 写配置(application.yml):

canal:destination: heima  # Canal实例名(要和Canal服务配置一致)server: 192.168.150.101:11111  # Canal服务地址

3. 写监听器(数据库变了就更新缓存):

// 监听tb_item表(商品表)的变化
("tb_item")

public class ItemCanalHandler implements EntryHandler<Item> {private StringRedisTemplate redisTemplate;private Cache<Long, Item> itemCache;// 数据库新增商品时执行public void insert(Item item) {// 更新JVM进程缓存itemCache.put(item.getId(), item);// 更新RedisredisTemplate.opsForValue().set("item:id:" + item.getId(), JSON.toJSONString(item));}// 数据库修改商品时执行public void update(Item oldItem, Item newItem) {itemCache.put(newItem.getId(), newItem);redisTemplate.opsForValue().set("item:id:" + newItem.getId(), JSON.toJSONString(newItem));}// 数据库删除商品时执行public void delete(Item item) {itemCache.invalidate(item.getId());  // 删进程缓存redisTemplate.delete("item:id:" + item.getId());  // 删Redis}
}

五、总结:多级缓存的完整流程

最后用一张图总结,小白也能记住:
用户请求 → 浏览器缓存(有就返回)→ Nginx本地缓存(有就返回)→ Redis(有就返回)→ JVM进程缓存(有就返回)→ 数据库(查完存到各级缓存)
在这里插入图片描述
核心思想:能在前面拦的,绝不往后传,用多级缓存层层拦截,扛住亿级流量。

http://www.dtcms.com/a/364415.html

相关文章:

  • MP4视频太大如何压缩?分享6种简单便捷的压缩小技巧
  • 微服务的编程测评系统20-虚拟机-nginx-部署
  • dockerfile文件的用途
  • Day20_【机器学习—逻辑回归 (2)—分类评估方法】
  • 机器学习与深度学习的 Python 基础之 NumPy(2)
  • 构建安全的自动驾驶:软件测试中的编码规范与AI验证
  • 数据结构_循环队列_牺牲一个存储空间_不牺牲额外的存储空间 Circular Queue(C语言实现_超详细)
  • 机器学习-逻辑回归
  • 数据结构:图的表示 (Representation of Graphs)
  • 下一代自动驾驶汽车系统XIL验证方法
  • 基于机器学习的Backtrader波动性预测与管理
  • MySQL--CRUD
  • Qt使用Maintenance添加、卸载组件(未完)
  • MySQL 中的窗口函数详解:从入门到实战
  • Django 命令大全:从入门到精通,开发者必备指南
  • 【QT随笔】事件过滤器(installEventFilter 和 eventFilter 的组合)之生命周期管理详解
  • 微信小程序wx.getLocation结合腾讯地图逆解析获取位置详细教程,定位授权完整流程
  • 【Qt】国际化(中英文翻译)——Qt语言家(Linguist)和QTranslator类的简单使用(内含完整源码)
  • el-tree 点击父节点无效,只能选中子节点
  • 存算一体:重构AI计算的革命性技术(3)
  • 2025 大数据时代值得考的证书排名前八​
  • 电子病历空缺句的语言学特征描述与自动分类探析(以GPT-5为例)(上)
  • 分布式AI算力系统番外篇-----超体的现实《星核》
  • 2025版基于springboot的电影购票管理系统
  • Linux_网络基础
  • Uniapp中进行微信小程序头像和昵称的更改
  • Jenkins 可观测最佳实践
  • Flutter Android真机器调式,虚拟机调试以及在Vscode中开发Flutter应用
  • 【Linux操作系统】简学深悟启示录:进程控制
  • unity中的交互控制脚本