Redis 设计购物车(基于 Hash)详解

Redis 设计购物车(基于 Hash)详解

在电商系统中,购物车是一个高频读写、要求低延迟的核心模块。使用 Redis 存储购物车数据,可以显著提升性能,避免频繁访问数据库。而 Redis 的 Hash 数据结构 是实现购物车的最佳选择,因为它天然适合存储“用户-商品-数量”的映射关系。

本文将详细介绍如何使用 Redis Hash 设计高性能、可扩展的购物车系统,涵盖数据结构设计、核心命令、业务场景、并发控制与最佳实践。


一、为什么选择 Redis Hash?

特性与购物车需求的匹配度
✅ 键值对结构(field-value)field=商品ID, value=数量,完美匹配
✅ 支持单字段操作可单独增减某商品数量,无需读取整个购物车
✅ 内存效率高相比多个 String,Hash 更节省内存
✅ 原子操作HINCRBY 保证数量变更的原子性
✅ 支持过期时间可设置未登录用户的临时购物车自动过期

结论:Hash 是实现购物车的最优雅、最高效的数据结构。


二、数据结构设计

1. Key 设计

cart:{userId}
  • 示例:cart:1001
  • 说明:以用户 ID 为 key,避免冲突

💡 未登录用户可使用 cart:temp_{sessionId} 临时存储,登录后合并

2. Field-Value 设计

Field(字段)Value(值)说明
item:10012商品 1001 的数量为 2
item:10021商品 1002 的数量为 1

✅ 支持后续扩展:可用 JSON 存储更多属性(如价格、规格),但会牺牲原子性


三、核心 Redis 命令与操作

1. 添加商品(+1 或新增)

HINCRBY cart:1001 item:1001 1
  • 如果 item:1001 不存在,自动创建并设为 1
  • 如果存在,数量 +1
  • ✅ 原子操作,无需加锁

2. 修改商品数量

HSET cart:1001 item:1001 3
  • 直接设置商品数量为 3

3. 删除商品

HDEL cart:1001 item:1001
  • 删除指定商品

4. 查询购物车所有商品

HGETALL cart:1001
  • 返回所有 field-value 对
  • 示例输出:
    1) "item:1001"
    2) "2"
    3) "item:1002"
    4) "1"
    

5. 查询某商品数量

HGET cart:1001 item:1001
  • 返回数量,如 "2"

6. 获取购物车商品总数(总件数)

HVALS cart:1001

然后在应用层求和:

List<String> values = redis.hvals("cart:1001");
int totalCount = values.stream().mapToInt(Integer::parseInt).sum();

或使用 Lua 脚本原子计算:

EVAL "
  local sum = 0
  local vals = redis.call('HVALS', KEYS[1])
  for i, v in ipairs(vals) do
    sum = sum + tonumber(v)
  end
  return sum
" 1 cart:1001

7. 清空购物车

DEL cart:1001

四、典型业务场景实现

场景 1:用户添加商品到购物车

public void addToCart(long userId, long itemId, int quantity) {
    String key = "cart:" + userId;
    String field = "item:" + itemId;

    // 原子增加数量
    redisTemplate.opsForHash().increment(key, field, quantity);

    // 设置过期时间(可选,如 7 天)
    redisTemplate.expire(key, 7, TimeUnit.DAYS);
}

场景 2:用户修改商品数量

public void updateCartItem(long userId, long itemId, int quantity) {
    String key = "cart:" + userId;
    String field = "item:" + itemId;

    if (quantity <= 0) {
        // 删除商品
        redisTemplate.opsForHash().delete(key, field);
    } else {
        redisTemplate.opsForHash().put(key, field, String.valueOf(quantity));
    }
}

场景 3:获取购物车详情

public Map<String, Integer> getCart(long userId) {
    String key = "cart:" + userId;
    Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);

    Map<String, Integer> cart = new HashMap<>();
    for (Map.Entry<Object, Object> entry : entries.entrySet()) {
        String field = (String) entry.getKey();
        int qty = Integer.parseInt((String) entry.getValue());
        cart.put(field, qty);
    }
    return cart;
}

场景 4:未登录用户购物车合并(登录后)

public void mergeCartAfterLogin(String tempCartKey, long userId) {
    String userCartKey = "cart:" + userId;

    // 将临时购物车合并到用户购物车
    Map<Object, Object> tempItems = redisTemplate.opsForHash().entries(tempCartKey);
    for (Map.Entry<Object, Object> entry : tempItems.entrySet()) {
        String itemKey = (String) entry.getKey();
        int qty = Integer.parseInt((String) entry.getValue());
        redisTemplate.opsForHash().increment(userCartKey, itemKey, qty);
    }

    // 删除临时购物车
    redisTemplate.delete(tempCartKey);

    // 设置长期过期
    redisTemplate.expire(userCartKey, 30, TimeUnit.DAYS);
}

五、并发与一致性控制

1. 原子性保障

  • HINCRBY 是原子操作,多个用户同时加购同一商品不会出错
  • HSETHDEL 也是原子的

2. 避免超卖(需结合库存系统)

  • 购物车只存数量,不保证库存
  • 下单时需校验真实库存(可使用 Redis + Lua 或数据库)

3. 使用 Lua 脚本保证复合操作原子性

-- 添加商品,但最多不超过 99 件
EVAL "
  local current = tonumber(redis.call('HGET', KEYS[1], ARGV[1]) or 0)
  if current + ARGV[2] > 99 then
    return -1
  else
    redis.call('HINCRBY', KEYS[1], ARGV[1], ARGV[2])
    return current + ARGV[2]
  end
" 1 cart:1001 item:1001 1

六、性能优化与最佳实践

✅ 1. 合理设置过期时间

  • 未登录用户:EXPIRE cart:temp_xxx 1d
  • 登录用户:可设为 7~30 天,或永久(由业务决定)

✅ 2. 避免大购物车

  • 限制单用户商品数(如最多 100 种商品)
  • 使用 HLEN cart:1001 判断数量

✅ 3. 批量操作优化

  • 获取多个商品信息时,使用 HMGET(虽不直接支持,可通过 pipeline)
  • 使用 Pipeline 批量执行命令,减少网络开销

✅ 4. 监控大 key

  • 使用 redis-cli --bigkeys 检测大购物车
  • HGETALL 可能阻塞,建议分页或限制商品数

✅ 5. 数据一致性

  • 购物车数据以 Redis 为准
  • 异步同步到数据库(如每天备份)
  • 下单成功后清除购物车中已购商品

✅ 6. 扩展性考虑

  • 用户量大时可按 userId % 100 分片存储
  • 使用 Redis Cluster 自动分片

七、与其他数据结构对比

数据结构是否适合购物车原因
🟠 Hash✅ 最佳选择字段对应商品,操作原子,内存高效
🟡 String(JSON)⚠️ 可用但不推荐每次读写整个购物车,不原子
🔵 List❌ 不适合无法快速查找/修改指定商品
🔴 Set❌ 不适合无数量概念,不能重复
🟣 Sorted Set⚠️ 可用score 存数量,但语义不清晰,操作复杂

八、完整数据结构示例

# 用户 1001 的购物车
HSET cart:1001 item:1001 "2"
HSET cart:1001 item:1002 "1"
HSET cart:1001 item:1003 "5"

# 查询
HGETALL cart:1001
# 输出:
# item:1001 -> 2
# item:1002 -> 1
# item:1003 -> 5

# 总件数:2+1+5 = 8

九、总结:Redis Hash 实现购物车的优势

优势说明
⚡ 高性能内存操作,响应时间在毫秒级
🔐 原子性HINCRBY 保证数量变更安全
💾 节省内存Hash 编码优化,比多个 String 更高效
📦 易扩展支持商品增删改查,逻辑清晰
🔁 易集成与 Spring Data Redis、Redisson 等无缝集成

结语
使用 Redis Hash 实现购物车,是电商系统的标准实践。它简单、高效、可靠,能够支撑高并发场景下的购物车操作。结合合理的过期策略、并发控制和监控,可构建一个稳定、高性能的购物车服务。

💡 进阶建议

  • 结合 Redisson 使用 RMap 提供更高级的 API
  • 使用 Lua 脚本 实现复杂业务逻辑的原子性
  • 购物车与 库存服务 联动,防止超卖
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值