1. 烘焙坊项目后端缓存架构设计背景
在电商类项目中,缓存系统如同面包房里的预备发酵面团——提前准备好高频使用的数据,避免每次都要从零开始和面。我们的烘焙坊项目当前面临三个典型痛点:
- 商品详情页访问延迟:当用户浏览马卡龙、法棍等热门商品时,MySQL直接查询导致响应时间波动在300-500ms
- 购物车操作并发冲突:促销活动期间多个用户同时修改购物车导致超卖
- 促销规则计算压力:会员折扣、满减等组合优惠策略需要频繁计算
经过压力测试,在500并发用户场景下,无缓存系统的订单提交成功率仅有72%。这就像面包店在早高峰时只有一个收银台,必然导致顾客排队流失。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Redis缓存实现核心技术方案
2.1 缓存选型与部署拓扑
我们采用Redis 6.2作为核心缓存组件,具体部署架构如下:
plaintext复制[应用服务器]
│
├─ [Redis Sentinel集群] (3节点)
│ ├─ Master: 16G内存/8核心
│ ├─ Replica×2: 8G内存/4核心
│
└─ [本地Caffeine缓存] (二级缓存)
选择Redis而非Memcached的关键考量:
- 需要持久化购物车数据
- 未来可能使用Lua脚本实现复杂促销逻辑
- 原生支持JSON格式存储商品详情
2.2 缓存数据结构设计
针对不同业务场景采用差异化结构:
| 业务场景 | 数据结构 | Key命名规则 | TTL设置 |
|---|---|---|---|
| 商品详情 | Hash | product: | 30分钟 |
| 用户购物车 | Hash | cart: | 7天(持久化) |
| 促销规则 | ZSET | promo:rules | 1小时 |
| 库存预热 | String | stock:hot: | 5分钟 |
特别在购物车实现中,采用Hash结构存储每个用户的商品项:
redis复制HSET cart:10086
"baguette" '{"count":2,"selected":true}'
"croissant" '{"count":5,"selected":false}'
2.3 缓存读写策略实现
采用经典的Cache-Aside模式,但针对电商场景做了优化:
java复制public ProductDetail getProductDetail(String skuId) {
// 1. 先查本地缓存
ProductDetail detail = caffeineCache.get(skuId);
if (detail != null) return detail;
// 2. 查Redis集群
String redisKey = "product:" + skuId;
detail = redisTemplate.opsForHash().entries(redisKey);
if (!detail.isEmpty()) {
caffeineCache.put(skuId, detail); // 回填二级缓存
return detail;
}
// 3. 回源数据库
detail = productMapper.selectBySku(skuId);
if (detail != null) {
// 异步更新缓存
CompletableFuture.runAsync(() -> {
redisTemplate.opsForHash().putAll(redisKey, detail);
redisTemplate.expire(redisKey, 30, TimeUnit.MINUTES);
});
}
return detail;
}
关键优化点:引入二级缓存降低Redis压力,采用异步写策略避免阻塞主流程
3. 购物车功能深度实现
3.1 并发控制方案对比测试
我们对比了三种方案在200并发下的表现:
| 方案 | 成功率 | 平均RT | 实现复杂度 |
|---|---|---|---|
| 乐观锁 | 89% | 120ms | ★★☆ |
| Redis事务 | 93% | 85ms | ★★★ |
| Lua脚本原子操作 | 99.7% | 65ms | ★★★★ |
最终选用Lua脚本实现原子化操作:
lua复制-- 添加商品到购物车脚本
local key = KEYS[1]
local field = ARGV[1]
local increment = tonumber(ARGV[2])
local maxCount = tonumber(ARGV[3])
local current = redis.call('HGET', key, field) or '0'
local newVal = tonumber(current) + increment
if newVal <= 0 then
redis.call('HDEL', key, field)
return 0
elseif newVal > maxCount then
return tonumber(current)
else
redis.call('HSET', key, field, newVal)
return newVal
end
3.2 购物车与库存的联动设计
为避免超卖,建立库存预占机制:
- 用户加购时检查剩余库存
- 在Redis中预扣库存(SETNX实现分布式锁)
- 15分钟未结算自动释放
java复制public boolean addToCart(Long userId, String skuId, int num) {
String lockKey = "lock:stock:" + skuId;
String stockKey = "stock:real:" + skuId;
// 获取分布式锁
String token = UUID.randomUUID().toString();
try {
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, token, 10, TimeUnit.SECONDS);
if (!locked) throw new BusyException("操作太频繁");
// 检查实际库存
Integer stock = (Integer)redisTemplate.opsForValue().get(stockKey);
if (stock == null) {
stock = stockMapper.getRealStock(skuId);
redisTemplate.opsForValue().set(stockKey, stock, 1, TimeUnit.MINUTES);
}
if (stock >= num) {
// 扣减Redis库存
redisTemplate.opsForValue().decrement(stockKey, num);
// 更新购物车(调用前述Lua脚本)
cartService.addItem(userId, skuId, num);
return true;
}
return false;
} finally {
// 释放锁
if (token.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
4. 缓存治理与异常处理
4.1 缓存雪崩预防方案
我们采用分层过期策略避免集中失效:
- 基础商品数据:30分钟+随机5分钟偏移
- 促销数据:1小时整点刷新
- 库存数据:5分钟固定过期
同时配置Sentinel自动故障转移,当主节点宕机时能在15秒内完成切换。监控指标包括:
- 缓存命中率(维持在85%以上)
- 慢查询数量(阈值50ms)
- 连接池利用率(警告线80%)
4.2 热点Key发现与处理
通过监控发现马卡龙商品在促销期间成为热点Key(QPS>3000),解决方案:
- 本地缓存备份
- Redis分片:
product:macaron_{0..2} - 客户端随机访问不同分片
python复制# 热点Key分片访问示例
def get_hot_product(sku):
slot = hash(sku) % 3
shard_key = f"product:{sku}_{slot}"
return redis_client.hgetall(shard_key)
5. 性能优化实测数据
上线前后关键指标对比:
| 指标 | 无缓存时期 | 缓存优化后 | 提升幅度 |
|---|---|---|---|
| 商品查询P99 | 420ms | 68ms | 84% |
| 购物车操作TPS | 120 | 2100 | 1650% |
| 订单超卖率 | 8.3% | 0.02% | 99.8% |
| 数据库QPS | 3500 | 600 | 83% |
这个优化效果相当于把传统面包店的纸质订单系统升级为数字化POS系统——不仅处理速度更快,还能避免人工计算错误。特别是在黑色星期五的流量洪峰中,系统始终保持平稳运行,没有出现任何缓存穿透导致的数据库崩溃。
