🛒 前言:小小的购物车,大大的坑
在面试中,我最喜欢问候选人一个问题:“设计一个京东/淘宝级别的购物车,数据应该存在哪里?”
- 回答 A:“存在 Cookie 里,省服务器资源。” ——错。用户换个手机登录,购物车空了,体验极差。
- 回答 B:“存在 MySQL 里,安全。” ——错。双 11 千万级并发,每次加购都写库,数据库直接火葬场。
- 回答 C:“存在 Redis 里,快。” ——对了一半。Redis 挂了怎么办?数据丢失会导致严重的客诉。
真正的购物车系统,是一个**“浏览器 + Redis + MySQL + 消息队列”**的混合架构。今天我们就来拆解这个架构是如何一步步搭建出来的。
🥈 方案一:纯数据库架构 (早期/低流量)
在创业初期,流量不大,直接用 MySQL 也是没问题的。
表结构设计:
CREATETABLE`cart_item`(`id`bigintNOTNULLAUTO_INCREMENT,`user_id`bigintNOTNULLCOMMENT'用户ID',`sku_id`bigintNOTNULLCOMMENT'商品ID',`count`intNOTNULLCOMMENT'数量',`checked`tinyintDEFAULT1COMMENT'是否勾选',`update_time`datetime,PRIMARYKEY(`id`),UNIQUEKEY`idx_user_sku`(`user_id`,`sku_id`));致命缺陷:
购物车是**“读写极高频”**的业务。
用户反复加减数量、勾选商品,如果每一次操作都去 Update 数据库,数据库的TPS (Transcation Per Second)撑不住。而且这属于“临时数据”,占用大量宝贵的数据库存储空间。
🥇 方案二:纯 Redis 架构 (中等流量)
为了快,我们将数据全部迁移到 Redis。
数据结构选型:Hash
- Key:
cart:{userId} - Field:
skuId - Value: JSON 字符串 (包含数量、勾选状态、加入时间)
Redis 命令演示:
# 添加商品 1001,数量 2HSET cart:88881001"{'count':2, 'checked':1}"# 增加商品数量HINCRBY cart:888810011# 获取购物车所有商品HGETALL cart:8888# 删除商品HDEL cart:88881001优点:性能极其彪悍,支持 10 万+ QPS。
缺点:Redis 内存贵。且 RDB/AOF 持久化有滞后,极端宕机情况下会丢数据(虽然购物车数据丢失通常可接受,但对于大厂这是 P0 级事故)。
💎 方案三:终极混合架构 (Cookie + Redis + MySQL + MQ)
这是目前主流电商(淘宝、京东)的通用架构。
1. 核心策略
- 未登录状态:数据保存在客户端Cookie/LocalStorage中。
- 登录状态:数据保存在Redis中(热数据)。
- 持久化:通过MQ 异步将 Redis 的变更写入MySQL(冷数据备份,防丢失,做数据分析)。
2. 关键流程:登录合并 (Merge)
这是最复杂的逻辑。当用户在“未登录”时往 Cookie 加了 3 个商品,然后点击“登录”。
系统必须将 Cookie 里的数据 + Redis 里原有的数据进行合并。
架构流程图:
🛠️ 核心代码实战:Redis 封装
我们使用 Spring Boot + RedisTemplate 来实现核心操作。
@ServicepublicclassCartService{@AutowiredprivateStringRedisTemplateredisTemplate;privatestaticfinalStringCART_PREFIX="cart:";/** * 添加购物车 */publicvoidaddCart(LonguserId,LongskuId,Integercount){Stringkey=CART_PREFIX+userId;BoundHashOperations<String,Object,Object>cartOps=redisTemplate.boundHashOps(key);StringskuIdStr=skuId.toString();// 1. 判断商品是否存在if(cartOps.hasKey(skuIdStr)){// 2. 存在则累加数量Stringjson=(String)cartOps.get(skuIdStr);CartItemitem=JSON.parseObject(json,CartItem.class);item.setCount(item.getCount()+count);cartOps.put(skuIdStr,JSON.toJSONString(item));}else{// 3. 不存在则新增CartItemnewItem=newCartItem(skuId,count);cartOps.put(skuIdStr,JSON.toJSONString(newItem));}// 4. 发送 MQ 消息做异步持久化 (伪代码)// producer.send("cart_update_topic", new CartUpdateEvent(userId, skuId));}/** * 登录合并逻辑 */publicvoidmergeCart(LonguserId,List<CartItem>cookieItems){if(CollectionUtils.isEmpty(cookieItems))return;for(CartItemcookieItem:cookieItems){addCart(userId,cookieItem.getSkuId(),cookieItem.getCount());}// 合并完成后,通知前端清除 Cookie}}🧐 进阶思考:Redis 存满了怎么办?
购物车数据有一个特点:僵尸数据多。很多用户几年前加的商品还在购物车里。
如果所有数据都堆在 Redis,内存会爆炸。
优化策略:LRU + 自动过期
- 设置 TTL:给
cart:{userId}设置过期时间(例如 30 天)。 - 自动续期:每次用户查看购物车或加购时,重置 TTL 为 30 天。
- 兜底:如果 Redis Key 过期了(用户 30 天没来了),再次访问时,从MySQL把数据加载回 Redis(缓存预热)。
这样既保证了活跃用户的极速体验,又节省了昂贵的 Redis 内存。
📝 总结
设计购物车系统的 4 个金科玉律:
- 读写分离:Redis 抗并发,MySQL 兜底。
- 异步写入:使用 MQ 解耦,不要让 DB 拖慢加购速度。
- 端云协同:未登录存前端,登录后做合并。
- 冷热分离:利用 TTL 清理僵尸购物车。
架构没有最好的,只有最合适的。理解了这套逻辑,你就能轻松应对大多数电商面试题。