Redis在电商购物车高并发读写场景下的优化实践
一、购物车业务场景分析
-
典型操作特征
- 读/写比例 ≈ 8:2
- 高峰QPS可达10万+
- 单用户最大商品数500+
- 操作类型:增删改查、全选/反选、数量修改
-
技术挑战
- 高并发下的数据一致性
- 海量数据存储与快速访问
- 实时价格计算与库存校验
- 分布式环境下的会话管理
二、核心数据结构设计优化
1. 存储结构方案对比
方案 | 优点 | 缺点 |
---|---|---|
String+JSON | 简单直观 | 修改需反序列化整个数据 |
Hash结构 | 支持字段级操作 | 嵌套结构处理略复杂 |
Sorted Set | 天然支持排序 | 存储成本较高 |
混合结构 | 平衡性能与灵活性 | 实现复杂度略高 |
2. 最终数据结构设计
// Key设计:cart:{userType}:{userId}
String cartKey = "cart:user:10001";
// Value结构:
// Hash结构存储商品基础信息
Map<String, String> itemData = new HashMap<>();
itemData.put("sku:1001",
"{\"quantity\":2,\"selected\":1,\"price\":5999,\"timestamp\":1717025661}");
// Sorted Set维护操作顺序
jedis.zadd(cartKey + ":zset", System.currentTimeMillis(), "sku:1001");
三、读写分离架构设计
1. 多级缓存架构
2. 各层缓存配置
缓存层级 | 技术选型 | 容量 | 过期策略 |
---|---|---|---|
本地缓存 | Caffeine | 10万用户 | 基于大小+访问时间(1分钟) |
Redis缓存 | Hash+Zset | 1TB内存 | 动态TTL+LRU淘汰 |
持久化存储 | MySQL+TiDB | 无限扩展 | 事务保障 |
四、高并发写入优化
1. 批量操作管道化
public void batchAddItems(String userId, List<CartItem> items) {
try (Jedis jedis = jedisPool.getResource()) {
Pipeline pipeline = jedis.pipelined();
String cartKey = buildCartKey(userId);
items.forEach(item -> {
String field = "sku:" + item.getSkuId();
// 更新Hash
pipeline.hset(cartKey, field, serialize(item));
// 更新ZSET
pipeline.zadd(cartKey + ":zset", System.currentTimeMillis(), field);
});
pipeline.sync();
}
}
2. 异步队列削峰
@KafkaListener(topics = "cart_updates")
public void processCartUpdate(CartUpdateEvent event) {
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
event.getUpdates().forEach(update -> {
connection.hSet(
update.getCartKey().getBytes(),
update.getField().getBytes(),
serialize(update.getValue())
);
});
return null;
});
}
五、高并发读取优化
1. 热点数据预加载
@Scheduled(fixedRate = 600000) // 每10分钟执行
public void preloadActiveCarts() {
List<String> activeUsers = userService.getRecentActiveUsers(10000);
activeUsers.parallelStream().forEach(userId -> {
String cartKey = buildCartKey(userId);
Map<String, String> cartData = jedis.hgetAll(cartKey);
localCache.put(userId, cartData);
});
}
2. 分片读取优化
public Map<String, CartItem> getCartSharded(String userId) {
String cartKey = buildCartKey(userId);
List<String> fields = new ArrayList<>();
// 分片读取Hash
Map<String, CartItem> result = new ConcurrentHashMap<>();
IntStream.range(0, 4).parallel().forEach(shard -> {
ScanParams params = new ScanParams().count(100).match("sku*");
String cursor = "0";
do {
ScanResult<Map.Entry<String, String>> scanResult =
jedis.hscan(cartKey, cursor, params);
scanResult.getResult().forEach(entry -> {
if (entry.getKey().hashCode() % 4 == shard) {
result.put(entry.getKey(), deserialize(entry.getValue()));
}
});
cursor = scanResult.getCursor();
} while (!"0".equals(cursor));
});
return result;
}
六、实时库存校验方案
1. 库存缓存设计
// 库存Key结构
String stockKey = "stock:" + skuId + ":" + warehouseId;
// 原子扣减库存
Long remain = jedis.eval(
"local current = redis.call('get', KEYS[1])\n" +
"if not current then return -1 end\n" +
"if tonumber(current) < tonumber(ARGV[1]) then return -1 end\n" +
"return redis.call('decrby', KEYS[1], ARGV[1])",
Collections.singletonList(stockKey),
Collections.singletonList("1")
);
2. 库存预占机制
public boolean reserveStock(String userId, String skuId, int quantity) {
String lockKey = "stock_lock:" + skuId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(100, 1000, TimeUnit.MILLISECONDS)) {
// 检查实际库存
int realStock = getRealStock(skuId);
if (realStock < quantity) return false;
// 写入预占记录
String reserveKey = "reserve:" + userId + ":" + skuId;
jedis.setex(reserveKey, 300, String.valueOf(quantity));
// 更新显示库存
jedis.decrBy("display_stock:" + skuId, quantity);
return true;
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return false;
}
七、数据一致性保障
1. 双写一致性方案
2. 补偿对账机制
@Scheduled(cron = "0 0 2 * * ?")
public void cartReconciliation() {
// 扫描所有购物车Key
ScanParams params = new ScanParams().match("cart:*").count(100);
String cursor = "0";
do {
ScanResult<String> scanResult = jedis.scan(cursor, params);
scanResult.getResult().parallelStream().forEach(cartKey -> {
// 对比Redis与数据库
Map<String, String> redisData = jedis.hgetAll(cartKey);
Map<String, CartItem> dbData = cartDAO.getFromDB(extractUserId(cartKey));
if (!dataEquals(redisData, dbData)) {
log.warn("数据不一致:{}", cartKey);
repairData(cartKey, redisData, dbData);
}
});
cursor = scanResult.getCursor();
} while (!"0".equals(cursor));
}
八、性能压测数据
测试环境:
- Redis Cluster(6节点,32核/128GB)
- 1000并发线程
- 单用户购物车50件商品
性能指标:
操作类型 | 优化前性能 | 优化后性能 | 提升倍数 |
---|---|---|---|
添加商品 | 1200 TPS | 8500 TPS | 7.1x |
批量删除 | 800 TPS | 6800 TPS | 8.5x |
全量获取 | 300 QPS | 4500 QPS | 15x |
库存校验 | 1500 TPS | 12000 TPS | 8x |
九、生产环境最佳实践
-
容量规划
- 按每个用户购物车平均50个商品计算
- 单个Hash存储约需5KB内存
- 百万用户需预留:1,000,000 * 5KB = 5GB
-
故障应急方案
- 熔断降级:启用本地缓存应急模式
- 快速扩容:Redis Cluster在线扩容
- 数据恢复:AOF+RDB双重保障
-
监控关键指标
# 实时监控命令 redis-cli info stats | grep -E "instantaneous_ops_per_sec|keyspace_hits" redis-cli info memory | grep used_memory_human redis-cli latency doctor
十、总结与扩展
通过本方案可实现:
- 毫秒级响应:核心操作<10ms
- 99.99%可用性:双机房容灾保障
- 线性扩展:支持千万级用户购物车
- 精准库存:实时库存校验误差<0.1%
扩展优化方向:
- 结合CDN缓存静态化购物车页面
- 使用Redis Stream实现实时价格推送
- 引入机器学习预测用户购物行为
更多资源:
https://www.kdocs.cn/l/cvk0eoGYucWA