缓存架构升级:从自建 Redis 迁移到 GeminiDB Redis 接口并实现缓存击穿防护
【摘要】 摘要:自建 Redis 集群在业务高速增长期频繁出现主节点宕机、内存溢出、跨 AZ 容灾缺失等问题,大促期间缓存击穿直接导致数据库被打穿。本文以电商商品详情页缓存为场景,基于华为云 GeminiDB Redis 接口,完成从自建 Redis 到云原生兼容 Redis 协议的平滑迁移,并结合本地缓存、互斥锁、逻辑过期三种策略实现缓存击穿防护,构建一套高可用、强一致、免运维的缓存架构。标签建议:...
摘要:自建 Redis 集群在业务高速增长期频繁出现主节点宕机、内存溢出、跨 AZ 容灾缺失等问题,大促期间缓存击穿直接导致数据库被打穿。本文以电商商品详情页缓存为场景,基于华为云 GeminiDB Redis 接口,完成从自建 Redis 到云原生兼容 Redis 协议的平滑迁移,并结合本地缓存、互斥锁、逻辑过期三种策略实现缓存击穿防护,构建一套高可用、强一致、免运维的缓存架构。
标签建议:云数据库 GeminiDB、分布式缓存、Redis 迁移、缓存击穿、云端实践
一、背景与痛点
商品详情页是电商系统的流量入口,QPS 峰值 5 万+。缓存层最初用的是 3 台 ECS 自建 Redis 3.0 主从集群:
|
痛点
|
具体表现
|
|---|---|
|
内存瓶颈
|
商品数据膨胀到 40GB,单节点内存打满,OOM 频发
|
|
主从切换慢
|
主节点宕机后 Sentinel 切换需 15-30 秒,期间大量请求穿透到数据库
|
|
跨 AZ 无保护
|
3 台机器在同一 AZ,机房抖动直接全挂
|
|
扩容要停写
|
加节点需重新分片,业务停写窗口 20 分钟以上
|
|
缓存击穿
|
热点商品(如秒杀品)过期瞬间,上千请求直击 MySQL,数据库 CPU 飙到 100%
|
|
运维靠人
|
每周手动执行
BGSAVE,备份文件拷到 OBS,恢复演练从未做过 |
核心诉求:协议兼容 Redis、无缝迁移、自动弹性、跨 AZ 高可用、击穿防护。
二、方案选型
2.1 为什么选 GeminiDB Redis 接口
|
维度
|
自建 Redis
|
华为云 DCS Redis
|
GeminiDB Redis 接口
|
|---|---|---|---|
|
兼容性
|
原生
|
兼容 Redis
|
兼容 Redis 协议
|
|
存储架构
|
内存
|
内存
|
存算分离,数据落盘
|
|
容量上限
|
受单机内存限制
|
单节点 64GB
|
TB 级,按需扩展
|
|
跨 AZ
|
需自建
|
支持
|
原生 3AZ 部署
|
|
扩容
|
停写
|
秒级
|
秒级,存储计算独立扩
|
|
持久化
|
RDB/AOF 手动配
|
自动
|
自动,RPO=0
|
|
成本
|
低(但运维成本高)
|
中
|
中高(省运维人力)
|
结论:需要大容量、高可用、免运维且兼容 Redis 协议,GeminiDB Redis 接口是平衡选择。如果数据量 < 16GB 且追求极致性能,DCS Redis 更合适。
2.2 整体架构
应用服务集群(CCE / ECS)
│
├── 本地缓存(Caffeine,一级,TTL 60s)
│
├── GeminiDB Redis 接口(二级,TTL 自定义)
│ ├── 商品基本信息 Hash
│ ├── 商品库存 String
│ ├── 商品详情 JSON String
│ └── 热点 Key 标记 Bitmap
│
└── RDS MySQL(三级,兜底)
└── 商品表
2.3 击穿防护策略选择
|
策略
|
原理
|
优点
|
缺点
|
适用场景
|
|---|---|---|---|---|
|
互斥锁
|
过期时只有一个线程查 DB 并回填
|
实现简单、数据一致
|
锁等待增加延迟
|
通用场景
|
|
逻辑过期
|
Key 不设 TTL,值里带过期时间,异步刷新
|
无等待、性能好
|
数据短暂不一致
|
热点商品
|
|
本地缓存
|
JVM 内缓存,减少 Redis 访问
|
最快、彻底防击穿
|
集群间不一致
|
读多写少
|
组合方案:本地缓存(Caffeine 60s)+ Redis 分布式锁 + 逻辑过期兜底。
三、迁移实操
3.1 创建 GeminiDB Redis 实例
- 进入云数据库 GeminiDB 控制台 → 创建实例
- 兼容接口:Redis
- 版本:Redis 5.0/6.0 兼容
- 存储类型:SSD 云盘(高性能)
- 部署模式:3AZ 高可用(生产必选)
- 容量:初始 100GB(可按需在线扩容)
- 规格:计算节点 4U16G × 3
- 网络:与业务应用同 VPC、同安全组
避坑:GeminiDB Redis 接口与开源 Redis 在部分命令上有差异(如不支持KEYS *生产环境本来也不该用),迁移前先用redis-cli跑一遍业务常用命令验证兼容性。
3.2 数据迁移方案
方案 A:双写迁移(推荐,零停机)
- 应用代码改造为双写模式:写操作同时写自建 Redis 和 GeminiDB
- 全量数据同步:用
redis-shake工具从自建 Redis 全量同步到 GeminiDB - 增量同步:
redis-shake持续追自建 Redis 的 AOF - 校验一致性:抽样对比两边 Key 数量和关键 Key 的值
- 切换读流量:应用读请求从自建 Redis 切到 GeminiDB
- 观察 48 小时无异常,停写自建 Redis,下线
方案 B:停机迁移(简单但需窗口)
- 业务低峰停写
redis-cli --rdb导出 RDB 文件- 用
redis-shake或redis-import导入 GeminiDB - 修改应用连接串,重启服务
redis-shake 配置示例:
source:
type: standalone
address: 192.168.1.100:6379
password: "old-redis-password"
target:
type: standalone
address: geminidb-redis.cn-north-4.dcs.myhuaweicloud.com:6379
password: "geminidb-password"
sync:
rdb: true
aof: true
parallel: 16
3.3 应用连接改造
连接池配置(Jedis 示例):
@Configuration
public class RedisConfig {
@Value("${redis.host}")
private String host;
@Value("${redis.port:6379}")
private int port;
@Value("${redis.password}")
private String password;
@Bean
public JedisPool jedisPool() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200);
config.setMaxIdle(50);
config.setMinIdle(10);
config.setTestOnBorrow(true);
config.setTestWhileIdle(true);
config.setTimeBetweenEvictionRunsMillis(30000);
return new JedisPool(config, host, port, 5000, password, 0, "default");
}
}
连接串变更:只需将
host 从自建 Redis IP 改为 GeminiDB 连接地址,端口和密码对应更新,业务代码无需改动。四、缓存击穿治理
4.1 互斥锁方案(核心商品)
@Service
public class ProductCacheService {
@Autowired
private JedisPool jedisPool;
@Autowired
private ProductMapper productMapper;
private static final String LOCK_PREFIX = "lock:product:";
private static final int LOCK_EXPIRE = 10; // 秒
private static final int CACHE_EXPIRE = 300; // 秒
public ProductDetail getProduct(Long productId) {
String cacheKey = "product:" + productId;
// 1. 先查 Redis
try (Jedis jedis = jedisPool.getResource()) {
String cached = jedis.get(cacheKey);
if (cached != null) {
return JSON.parseObject(cached, ProductDetail.class);
}
}
// 2. Redis 未命中,尝试获取分布式锁
String lockKey = LOCK_PREFIX + productId;
String lockValue = UUID.randomUUID().toString();
boolean locked = false;
try (Jedis jedis = jedisPool.getResource()) {
// SET NX EX 原子操作
String result = jedis.set(lockKey, lockValue, "NX", "EX", LOCK_EXPIRE);
locked = "OK".equals(result);
}
if (locked) {
try {
// 3. 拿到锁,查数据库
ProductDetail product = productMapper.selectById(productId);
if (product != null) {
// 4. 写入 Redis
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(cacheKey, CACHE_EXPIRE, JSON.toJSONString(product));
}
} else {
// 空值缓存,防止缓存穿透
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(cacheKey, 60, "NULL");
}
}
return product;
} finally {
// 5. 释放锁(Lua 脚本保证原子性)
releaseLock(lockKey, lockValue);
}
} else {
// 6. 未拿到锁,短暂休眠后重试
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getProduct(productId); // 递归重试(生产环境应设最大重试次数)
}
}
private void releaseLock(String lockKey, String lockValue) {
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
try (Jedis jedis = jedisPool.getResource()) {
jedis.eval(luaScript, 1, lockKey, lockValue);
}
}
}
4.2 逻辑过期方案(热点商品)
@Data
public class LogicalExpireCache<T> {
private T data;
private long expireTime; // 逻辑过期时间戳
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}
public ProductDetail getHotProduct(Long productId) {
String cacheKey = "product:hot:" + productId;
try (Jedis jedis = jedisPool.getResource()) {
String cached = jedis.get(cacheKey);
if (cached == null) {
// 首次加载,同步查库
return loadAndCache(productId, cacheKey, jedis);
}
LogicalExpireCache<ProductDetail> wrapper =
JSON.parseObject(cached, new TypeReference<LogicalExpireCache<ProductDetail>>() {});
if (wrapper.isExpired()) {
// 逻辑过期,异步刷新(提交到线程池)
refreshExecutor.submit(() -> {
ProductDetail fresh = productMapper.selectById(productId);
if (fresh != null) {
LogicalExpireCache<ProductDetail> newWrapper = new LogicalExpireCache<>();
newWrapper.setData(fresh);
newWrapper.setExpireTime(System.currentTimeMillis() + 3600_000);
try (Jedis j = jedisPool.getResource()) {
j.set(cacheKey, JSON.toJSONString(newWrapper));
}
}
});
}
return wrapper.getData(); // 返回旧数据,保证可用性
}
}
4.3 本地缓存 + Redis 多级缓存
@Service
public class MultiLevelCacheService {
// Caffeine 本地缓存,最大 10k 条目,60 秒过期
private final Cache<Long, ProductDetail> localCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(60, TimeUnit.SECONDS)
.build();
public ProductDetail getProduct(Long productId) {
// L1: 本地缓存
ProductDetail cached = localCache.getIfPresent(productId);
if (cached != null) {
return cached;
}
// L2: Redis
String redisKey = "product:" + productId;
try (Jedis jedis = jedisPool.getResource()) {
String value = jedis.get(redisKey);
if (value != null && !"NULL".equals(value)) {
ProductDetail product = JSON.parseObject(value, ProductDetail.class);
localCache.put(productId, product);
return product;
}
}
// L3: 数据库 + 回填
ProductDetail product = productMapper.selectById(productId);
if (product != null) {
localCache.put(productId, product);
try (Jedis jedis = jedisPool.getResource()) {
jedis.setex(redisKey, 300, JSON.toJSONString(product));
}
}
return product;
}
}
五、验证结果
|
验证项
|
操作
|
预期
|
|---|---|---|
|
协议兼容
|
业务代码不改一行,只换连接串
|
所有 Redis 命令正常执行
|
|
性能对比
|
压测 5 万 QPS
|
P99 延迟 < 5ms
|
|
容量弹性
|
在线扩容 100GB → 200GB
|
秒级生效,无业务中断
|
|
跨 AZ 容灾
|
手动停一个 AZ 的计算节点
|
自动切换,RTO < 30s
|
|
缓存击穿防护
|
热点 Key 过期瞬间 5000 QPS 并发
|
数据库 QPS 无飙升,仅 1 次查库
|
|
数据持久化
|
写入后 kill 进程
|
重启后数据不丢,RPO=0
|
六、生产避坑清单
❌ 常见错误
- 用
KEYS *排查问题:生产环境执行直接阻塞所有请求,用SCAN替代 - 大 Key 不拆分:单个 Hash 存 10 万 field,读取一次卡 200ms,拆成多个 Key
- 热 Key 不处理:某个商品被疯狂访问,单分片 CPU 打满,用本地缓存或 Key 分片
- 不设 TTL 导致内存膨胀:缓存只写不删,最终 OOM
- 分布式锁不设超时:业务异常未释放锁,后续请求全部阻塞
- 锁释放不校验 owner:误删别人的锁,用 Lua 脚本原子校验
- 迁移期间源库执行
FLUSHALL:全量同步直接废掉 - 连接池不配
testOnBorrow:拿到已关闭连接,大量SocketException - GeminiDB 和 ECS 跨 Region:延迟从 0.5ms 变 5ms,务必同 Region
- 忽略慢查询日志:不监控
slowlog,性能问题靠猜
✅ 正确姿势
- 大 Key 拆分:Hash 超 1000 field 拆成
hash:{shard1}、hash:{shard2} - 热 Key 打散:Key 后缀加随机值,读时聚合
- 锁超时时间设为业务耗时的 3-5 倍
- 迁移期间源库设为只读(从库同步)
- 连接池
maxTotal按并发量 × 2 配置 - 开启 GeminiDB 审计日志和慢查询监控
- 本地缓存容量设上限,防止 JVM OOM
- 缓存预热:大促前提前加载热点数据到 Redis
七、成本结构梳理
|
计费项
|
说明
|
优化建议
|
|---|---|---|
|
计算节点
|
按规格和数量
|
3 节点起步,读多可加只读节点
|
|
存储
|
按实际使用量
|
存算分离,存储按需增长
|
|
跨 AZ 部署
|
3AZ 比单 AZ 贵约 30%
|
核心业务必选,非核心可单 AZ
|
|
备份存储
|
自动备份保留
|
按合规要求设保留天数
|
|
流量
|
内网流量免费
|
公网访问才计费
|
对比自建 Redis:省掉 3 台 ECS 费用 + 运维人力成本,综合 TCO 降低约 40%。
八、总结与扩展
缓存架构的演进路径是:本地缓存 → 分布式缓存 → 多级缓存 → 智能缓存。
这次改造的核心收获:
- 迁移零停机:双写 + 增量同步,业务无感知切换
- 击穿有防护:互斥锁 + 逻辑过期 + 本地缓存三层兜底
- 运维自动化:扩容、备份、故障切换全部平台托管
- 容量无上限:存算分离架构,存储按需扩展不受单节点限制
后续演进方向:
- 缓存预热自动化:基于历史访问日志,大促前自动预热热点数据
- 热点 Key 自动发现:接入 Redis 监控,自动识别热 Key 并推送到本地缓存
- 多级缓存统一失效:通过 Canal 监听 MySQL binlog,统一失效各级缓存
- 混合持久化:GeminiDB 本身已持久化,可评估是否仍需 MySQL 兜底
- 异地多活:跨 Region 部署 GeminiDB,通过全球加速实现就近访问
本文为原创缓存架构实践,基于华为云 GeminiDB Redis 接口整理,操作步骤以当前控制台页面为准。转载请注明出处。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)