购物车系统设计解析:数据结构选择与临时方案实现

220 阅读3分钟

购物车系统设计解析:数据结构选择与临时方案实现

一、核心数据结构设计

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的核心考量

  1. 读写效率:一次操作完成整个购物车的存取
  2. 数据结构简单:适合购物车商品数量较少的场景(<100件)
  3. 序列化灵活:可存储复杂对象结构
  4. 内存优化:使用压缩算法后存储效率接近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 架构升级路径

  1. 初期:全内存方案(String存储)
  2. 中期:引入本地缓存(Caffeine)+ 二级存储(MySQL)
  3. 后期:分片集群(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;
}