系统设计实战 19:设计购物车系统(Shopping Cart)
🚀 系统设计实战 19:设计购物车系统(Shopping Cart)
摘要:购物车是电商系统的流量入口,涉及离线/在线购物车合并、多端同步、高并发读写、价格计算等核心问题。本文深入剖析 Redis Hash 存储方案、离线合并策略、购物车结算优化,并提供完整的 Java 实现。
🎯 场景引入
你往购物车加了 3 件商品,关掉 App 第二天打开还在。换到电脑端,购物车也同步了。
核心挑战:
- 多端同步:手机、电脑、iPad 上的购物车如何实时一致?
- 库存联动:购物车里的商品下架了、涨价了,怎么处理?
- 高并发:双十一前夜,上亿用户疯狂加购,系统如何扛住?
🎯 场景引入
你打开手机准备使用设计购物车系统服务。看似简单的操作背后,系统面临三大核心挑战:
- 挑战一:高并发——如何在百万级 QPS 下保持低延迟?
- 挑战二:高可用——如何在节点故障时保证服务不中断?
- 挑战三:数据一致性——如何在分布式环境下保证数据正确?
一、问题背景
1.1 核心挑战
购物车的特点: 1. 读写极高:用户频繁添加、修改数量、删除商品 2. 临时性:大部分商品会被删除或遗忘,只有少数转化为订单 3. 多端同步:手机加购,电脑可见 4. 合并问题:未登录加购 → 登录后合并 核心矛盾: 高频读写 vs 数据持久化 未登录体验 vs 登录后同步 实时价格 vs 缓存性能1.2 核心需求
| 需求 | 说明 |
|---|---|
| 添加商品 | 加入购物车,支持 SKU 选择 |
| 修改数量 | 增减商品数量 |
| 删除商品 | 移除购物车中的商品 |
| 查看列表 | 展示商品列表、实时价格、总价 |
| 离线合并 | 登录后合并未登录时的购物车 |
| 多端同步 | 手机、电脑、平板数据一致 |
| 结算 | 选择商品生成订单 |
1.3 容量估算
| 指标 | 数值 |
|---|---|
| DAU | 5000 万 |
| 购物车操作 QPS | ~10 万 |
| 峰值 QPS(大促) | ~100 万 |
| 平均每人购物车商品数 | ~15 |
| 购物车数据总量 | ~7.5 亿条 |
| 单条数据大小 | ~200 字节 |
| 总存储量 | ~150 GB |
二、整体架构
┌─────────────────────────────────────────────────────────────┐ │ 客户端 │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │ │ 未登录购物车 │ │ 已登录购物车 │ │ 结算页 │ │ │ │ (LocalStorage)│ │ (服务端) │ │ │ │ │ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │ └─────────┼─────────────────┼────────────────────┼─────────────┘ │ │ │ ▼ ▼ ▼ ┌───────────────────────────────────────────────────────────┐ │ API Gateway │ └───────────────────────┬───────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────┐ │ Cart Service(购物车服务) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │ │ │ 增删改查 │ │ 合并服务 │ │ 价格计算 │ │ 结算服务 │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬──────┘ │ └───────┼─────────────┼────────────┼──────────────┼─────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────┐ │ 存储层 │ │ ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌──────────┐ │ │ │ Redis │ │ MySQL │ │ 商品服务 │ │ 促销服务 │ │ │ │ (主存储) │ │(持久化) │ │ (价格/库存)│ │ (优惠) │ │ │ └──────────┘ └──────────┘ └────────────┘ └──────────┘ │ └─────────────────────────────────────────────────────────┘三、数据模型设计
// 时间复杂度:O(N),空间复杂度:O(1)
/** * 购物车项 */publicclassCartItem{privateLonguserId;privateLongskuId;privateIntegerquantity;privateBooleanselected;// 是否选中(结算用)privateLongaddTime;// 加入时间// 以下字段不存储,查询时实时获取privatetransientStringskuName;privatetransientLongprice;// 实时价格(分)privatetransientIntegerstock;// 实时库存privatetransientStringimageUrl;}/** * 购物车(Redis Hash 结构) * Key: cart:{userId} * Field: {skuId} * Value: JSON {quantity, selected, addTime} */四、核心模块实现
4.1 购物车 CRUD 服务
@ServicepublicclassCartService{@AutowiredprivateStringRedisTemplateredisTemplate;@AutowiredprivateProductServiceproductService;privatestaticfinalintMAX_CART_SIZE=100;privatestaticfinalStringCART_KEY_PREFIX="cart:";/** * 添加商品到购物车 */publicCartItemaddItem(LonguserId,LongskuId,intquantity){StringcartKey=CART_KEY_PREFIX+userId;// 1. 检查购物车容量Longsize=redisTemplate.opsForHash().size(cartKey);if(size!=null&&size>=MAX_CART_SIZE){thrownewCartFullException("购物车已满,最多 "+MAX_CART_SIZE+" 件商品");}// 2. 检查商品是否存在且有库存ProductInfoproduct=productService.getProduct(skuId);if(product==null||product.getStock()<=0){thrownewProductUnavailableException("商品不存在或已售罄");}// 3. 如果已在购物车中,数量累加Stringexisting=(String)redisTemplate.opsForHash().get(cartKey,skuId.toString());CartItemDatadata;if(existing!=null){data=JsonUtils.fromJson(existing,CartItemData.class);data.setQuantity(Math.min(data.getQuantity()+quantity,99));// 最多 99 件}else{data=newCartItemData(quantity,true,System.currentTimeMillis());}// 4. 写入 RedisredisTemplate.opsForHash().put(cartKey,skuId.toString(),JsonUtils