购物车系统设计解析:数据结构选择与临时方案实现
一、核心数据结构设计
1.1 Redis存储方案对比
graph TD
A[数据结构] --> B[String]
A --> C[Hash]
A --> D[ZSet]
B --> E[序列化存储]
B --> F[整体读写]
C --> G[字段级操作]
C --> H[内存优化]
D --> I[排序需求]
1.1.1 为什么选择String类型?
// 购物车数据结构示例
public class CartItem {
private String skuId;
private Integer quantity;
private LocalDateTime addTime;
// 其他业务字段...
}
// Redis存储结构
String key = "cart:user_1234";
String value = JSON.toJSONString(cartItemList);
redis.set(key, value);
选择String的核心考量:
- 读写效率:一次操作完成整个购物车的存取
- 数据结构简单:适合购物车商品数量较少的场景(<100件)
- 序列化灵活:可存储复杂对象结构
- 内存优化:使用压缩算法后存储效率接近Hash
1.2 Hash类型的潜在问题
HSET cart:user_1234 sku_5678 '{"quantity":2,"addTime":1630000000}'
- 优势:支持单个商品操作
- 劣势:
- 每个字段需要单独序列化
- 获取全量数据需要HGETALL操作
- 内存碎片率较高
二、临时购物车设计方案
2.1 架构设计
sequenceDiagram
participant User
participant App
participant Redis
User->>App: 未登录状态操作购物车
App->>Redis: 写入临时购物车(temp:session_id)
User->>App: 登录操作
App->>Redis: 合并临时购物车到正式购物车
App->>Redis: 删除临时购物车
2.2 关键实现代码
// 临时购物车服务
public class TempCartService {
// 临时购物车有效期(7天)
private static final int TEMP_CART_TTL = 604800;
public void addItem(HttpServletRequest request, CartItem item) {
String cartKey = getTempCartKey(request);
List<CartItem> items = getCartItems(cartKey);
// 合并相同商品
items.removeIf(i -> i.getSkuId().equals(item.getSkuId()));
items.add(item);
redis.setex(cartKey, TEMP_CART_TTL, JSON.toJSONString(items));
}
private String getTempCartKey(HttpServletRequest request) {
String sessionId = request.getSession().getId();
return "temp_cart:" + sessionId;
}
}
三、生产环境优化策略
3.1 性能优化方案
// 使用压缩提升存储效率
public class CartSerializer {
private static final CompressionCodec compressor = new SnappyCodec();
public byte[] serialize(List<CartItem> items) {
byte[] json = JSON.toJSONBytes(items);
return compressor.compress(json);
}
public List<CartItem> deserialize(byte[] data) {
byte[] json = compressor.decompress(data);
return JSON.parseArray(json, CartItem.class);
}
}
3.2 合并逻辑实现
public void mergeCart(String userId, String tempCartKey) {
// 获取临时购物车
String tempData = redis.get(tempCartKey);
List<CartItem> tempItems = parseCartItems(tempData);
// 获取正式购物车
String userCartKey = "cart:user_" + userId;
List<CartItem> userItems = getCartItems(userCartKey);
// 合并策略:以最新添加为准
Map<String, CartItem> merged = new LinkedHashMap<>();
userItems.forEach(item -> merged.put(item.getSkuId(), item));
tempItems.forEach(item -> merged.put(item.getSkuId(), item));
// 写回Redis
redis.set(userCartKey, JSON.toJSONString(new ArrayList<>(merged.values())));
redis.del(tempCartKey);
}
四、方案对比与选型建议
| 维度 | String方案 | Hash方案 | ZSet方案 |
|---|---|---|---|
| 读取性能 | O(1) 整体读取 | O(n) 全量读取 | O(logN) 范围读取 |
| 写入性能 | O(1) 整体写入 | O(1) 单个字段写入 | O(logN) 插入排序 |
| 内存占用 | 中等(依赖压缩) | 较高(字段元数据开销) | 最高(存储分数) |
| 适用场景 | 商品数量少,整体操作多 | 频繁修改单个商品属性 | 需要排序的购物车 |
五、异常场景处理方案
5.1 数据一致性保障
// 使用Lua脚本保证原子性
String script =
"local cart = redis.call('GET', KEYS[1])\n" +
"local items = cjson.decode(cart)\n" +
"for i, item in ipairs(items) do\n" +
" if item.skuId == ARGV[1] then\n" +
" table.remove(items, i)\n" +
" break\n" +
" end\n" +
"end\n" +
"redis.call('SET', KEYS[1], cjson.encode(items))\n" +
"return 1";
redis.eval(script, Collections.singletonList(cartKey),
Collections.singletonList(skuId));
5.2 容灾方案设计
graph TD
A[客户端] -->|主写| B[Redis主节点]
B -->|异步复制| C[Redis从节点]
A -->|降级写| D[本地存储]
D -->|网络恢复| B
六、技术演进方向
6.1 架构升级路径
- 初期:全内存方案(String存储)
- 中期:引入本地缓存(Caffeine)+ 二级存储(MySQL)
- 后期:分片集群(Redis Cluster)+ 持久化策略
6.2 混合存储方案
public List<CartItem> getCart(String userId) {
// 第一层:本地缓存
List<CartItem> items = localCache.get(userId);
if (items != null) return items;
// 第二层:Redis缓存
items = redisCartService.getCart(userId);
if (items != null) {
localCache.put(userId, items);
return items;
}
// 第三层:数据库
items = databaseCartService.getCart(userId);
redisCartService.saveCart(userId, items);
return items;
}