1. 项目背景与核心价值
在餐饮外卖系统的实际运营中,随着订单量增长和用户访问量上升,系统性能往往会遇到两个典型瓶颈:一是高频访问的菜品/套餐信息查询导致数据库压力过大,二是购物车操作的实时性要求与数据一致性之间的矛盾。这个项目正是针对这两个核心痛点提出的解决方案。
我去年参与过一个日订单量突破5万单的外卖平台优化项目,当时最突出的性能问题就出现在这两个环节。每到用餐高峰期,数据库服务器的CPU使用率经常飙升到90%以上,其中近60%的负载都来自于菜品详情的重复查询。而购物车功能由于需要实时计算优惠和库存,更是成为了系统响应速度的短板。
通过引入Redis缓存和优化购物车数据结构,我们最终将平均响应时间从原来的800ms降低到了120ms左右。这个案例让我深刻认识到,合理的缓存策略和数据结构设计对餐饮系统性能提升有多么关键。
2. 缓存架构设计与实现
2.1 缓存选型与数据结构设计
在技术选型上,我们选择了Redis作为缓存解决方案,主要基于以下几个考量:
- 读写性能优异(单机版QPS可达10万+)
- 丰富的数据结构支持
- 完善的过期和淘汰机制
- 与Spring生态的良好集成
对于菜品和套餐这类相对静态的数据,我们采用经典的"旁路缓存"模式。具体数据结构设计如下:
java复制// 菜品缓存Key设计
String dishCacheKey = "dish:" + dishId + ":" + status;
// 使用Hash存储菜品详情
redisTemplate.opsForHash().putAll(dishCacheKey,
Map.of(
"name", dish.getName(),
"price", dish.getPrice().toString(),
"description", dish.getDescription(),
"image", dish.getImage()
)
);
重要提示:缓存key的设计需要包含业务状态(如上下架状态),避免缓存失效导致已下架商品被错误展示。
2.2 缓存更新策略
我们采用"双写+失效"的混合策略来保证缓存一致性:
- 新增/修改场景:
java复制@Transactional
public void updateDish(Dish dish) {
// 1. 更新数据库
dishMapper.updateById(dish);
// 2. 更新缓存
String cacheKey = "dish:" + dish.getId() + ":1";
redisTemplate.opsForHash().putAll(cacheKey,
Map.of(
"name", dish.getName(),
// 其他字段...
)
);
// 3. 设置TTL
redisTemplate.expire(cacheKey, 2, TimeUnit.HOURS);
}
- 删除场景:
java复制@Transactional
public void deleteDish(Long id) {
// 1. 数据库删除
dishMapper.deleteById(id);
// 2. 缓存删除
redisTemplate.delete("dish:" + id + ":1");
redisTemplate.delete("dish:" + id + ":0");
}
- 缓存击穿防护:
java复制public Dish getDishWithCache(Long id) {
String cacheKey = "dish:" + id + ":1";
// 1. 先查缓存
Map<Object, Object> cache = redisTemplate.opsForHash().entries(cacheKey);
if (!cache.isEmpty()) {
return convertToDish(cache);
}
// 2. 获取分布式锁
String lockKey = "lock:dish:" + id;
try {
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (locked) {
// 3. 再次检查缓存(双重检查)
cache = redisTemplate.opsForHash().entries(cacheKey);
if (!cache.isEmpty()) {
return convertToDish(cache);
}
// 4. 查数据库
Dish dish = dishMapper.selectById(id);
if (dish != null) {
// 写入缓存
redisTemplate.opsForHash().putAll(cacheKey, ...);
redisTemplate.expire(cacheKey, 2, TimeUnit.HOURS);
}
return dish;
} else {
// 等待重试或返回默认值
Thread.sleep(100);
return getDishWithCache(id);
}
} finally {
redisTemplate.delete(lockKey);
}
}
2.3 缓存预热策略
针对用餐高峰期的流量特点,我们设计了定时预热机制:
java复制@Scheduled(cron = "0 30 10 * * ?") // 每天10:30执行
public void preheatCache() {
// 1. 获取热门菜品列表(基于历史订单分析)
List<Long> hotDishIds = orderMapper.selectHotDishIds(LocalDate.now().minusDays(7), 50);
// 2. 批量查询并缓存
List<Dish> dishes = dishMapper.selectBatchIds(hotDishIds);
dishes.forEach(dish -> {
String cacheKey = "dish:" + dish.getId() + ":1";
redisTemplate.opsForHash().putAll(cacheKey, ...);
redisTemplate.expire(cacheKey, 4, TimeUnit.HOURS);
});
}
3. 购物车系统优化
3.1 购物车数据结构设计
传统电商购物车设计在外卖场景下会遇到几个特殊问题:
- 需要实时计算餐盒费、配送费
- 需要校验菜品库存和起送价
- 优惠券计算复杂度高
我们采用分层存储方案:
java复制// 用户购物车元信息
String cartMetaKey = "cart:meta:" + userId;
redisTemplate.opsForHash().put(cartMetaKey,
"shopId", shopId,
"addressId", addressId,
"couponId", couponId
);
// 购物车商品明细
String cartItemsKey = "cart:items:" + userId;
redisTemplate.opsForHash().put(cartItemsKey,
"dish:" + dishId, quantity,
"combo:" + comboId, quantity
);
3.2 实时计算实现
购物车的核心难点在于各种实时计算逻辑,我们采用Lua脚本保证原子性:
lua复制-- 计算购物车总价脚本
local metaKey = KEYS[1]
local itemsKey = KEYS[2]
local shopId = redis.call('HGET', metaKey, 'shopId')
-- 获取店铺基础信息
local deliveryFee = tonumber(redis.call('HGET', 'shop:'..shopId, 'deliveryFee'))
local minOrderAmount = tonumber(redis.call('HGET', 'shop:'..shopId, 'minOrderAmount'))
-- 计算商品总价
local items = redis.call('HGETALL', itemsKey)
local total = 0
for i = 1, #items, 2 do
local itemKey = items[i]
local quantity = tonumber(items[i+1])
if string.find(itemKey, 'dish:') then
local dishId = string.match(itemKey, 'dish:(%d+)')
local price = tonumber(redis.call('HGET', 'dish:'..dishId..':1', 'price'))
total = total + price * quantity
elseif string.find(itemKey, 'combo:') then
-- 类似处理套餐...
end
end
-- 应用优惠券逻辑(简化版)
local couponId = redis.call('HGET', metaKey, 'couponId')
if couponId and couponId ~= '' then
-- 优惠券校验和计算...
end
-- 返回计算结果
return {total, deliveryFee, math.max(0, minOrderAmount - total)}
3.3 库存预扣减方案
针对外卖场景的高并发库存竞争,我们实现了二级库存机制:
- 展示库存:Redis缓存中的库存值,用于快速展示
- 真实库存:数据库中的实际库存值
- 预扣库存:订单创建时占用的中间状态
java复制public boolean reserveInventory(Long dishId, int quantity) {
String stockKey = "dish:stock:" + dishId;
String lockKey = "lock:dish:stock:" + dishId;
try {
// 获取分布式锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS);
if (!locked) return false;
// 检查剩余库存
Long remaining = redisTemplate.opsForValue().decrement(stockKey, quantity);
if (remaining < 0) {
// 回滚
redisTemplate.opsForValue().increment(stockKey, quantity);
return false;
}
// 异步更新数据库
asyncUpdateDbStock(dishId, quantity);
return true;
} finally {
redisTemplate.delete(lockKey);
}
}
4. 性能优化与问题排查
4.1 缓存性能监控指标
我们建立了以下监控指标体系:
| 指标名称 | 计算方式 | 健康阈值 |
|---|---|---|
| 缓存命中率 | 缓存请求成功次数/总请求次数 | >85% |
| 平均缓存响应时间 | 缓存操作耗时总和/操作次数 | <5ms |
| 缓存穿透次数 | 查询不存在的key次数 | <100次/分钟 |
| Redis内存使用率 | used_memory/maxmemory | <70% |
4.2 典型问题排查案例
案例1:缓存雪崩
- 现象:每日凌晨缓存批量过期后,数据库负载飙升
- 解决方案:
- 对缓存过期时间添加随机偏移(基础TTL ± 随机10分钟)
- 采用多级缓存策略(本地缓存+Redis)
案例2:购物车计算延迟
- 现象:促销活动期间购物车计算响应变慢
- 根因分析:
- Lua脚本中优惠券计算逻辑过于复杂
- 单个脚本执行时间超过Redis单线程处理能力
- 优化方案:
- 将优惠计算拆分为两个阶段(基础计算+优惠计算)
- 使用Redis集群分散压力
4.3 压测数据对比
优化前后的关键指标对比:
| 指标 | 优化前 | 优化后 | 提升幅度 |
|---|---|---|---|
| 菜品查询平均RT | 320ms | 28ms | 91%↓ |
| 购物车计算平均RT | 650ms | 75ms | 88%↓ |
| 数据库QPS峰值 | 12,000 | 3,200 | 73%↓ |
| 下单成功率 | 98.2% | 99.7% | 1.5%↑ |
5. 扩展优化方向
在实际项目中,我们还尝试了以下进阶优化手段:
- 热点数据发现:基于Redis的访问日志分析,自动识别热点菜品并实施特殊缓存策略
java复制// 使用Redis的HyperLogLog统计访问频率
String accessKey = "access:dish:" + dishId;
redisTemplate.opsForHyperLogLog().add(accessKey, userId);
Long accessCount = redisTemplate.opsForHyperLogLog().size(accessKey);
-
缓存分区策略:按照菜品类别将缓存分散到不同的Redis实例,避免单个实例成为瓶颈
-
购物车持久化:对长时间未结算的购物车进行持久化存储,避免Redis内存浪费
java复制@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
public void backupInactiveCarts() {
// 找出24小时未活跃的购物车
Set<String> keys = redisTemplate.keys("cart:items:*");
keys.forEach(key -> {
Long lastActive = redisTemplate.getExpire(key);
if (lastActive < 86400) {
String userId = key.substring("cart:items:".length());
// 持久化到数据库
cartBackupService.backup(userId);
redisTemplate.delete(key);
redisTemplate.delete("cart:meta:" + userId);
}
});
}
- 智能缓存预热:基于用户行为预测模型,在用餐高峰前预加载可能需要的菜品数据
经过这些优化,系统在"双十一"等大促期间也保持了稳定的性能表现,高峰期订单处理能力提升了3倍以上。这让我深刻体会到,在高并发场景下,合理的缓存设计和数据结构选择往往比单纯的硬件扩容更有效。