(三) Redis 高级数据结构与常用场景实践

(三) Redis 高级数据结构与常用场景实践

Redis 除了五中基础的数据结构(String、Set、sortedSet、Hash、List) 外,还提供了一些高级的数据结构,帮我们高效地处理大数据量、地理坐标、消息流、并发控制等业务场景。

Bitmaps: 用极小的空间存储大量用户状态

  1. 工作原理

    bitmaps 是对String 的操作拓展,允许通过偏移量操作某一位的值(0或者 1),每个位只需 1bit 存储空间,因此在记录海量布尔状态时非常高效。

  2. 常用命令

    1
    2
    3
    SETBIT sign:20250513 12345 1     # 用户 ID 12345 在 2025-05-13 签到
    GETBIT sign:20250513 12345 # 判断是否签到
    BITCOUNT sign:20250513 # 统计总签到人数
  3. 应用场景

    1. 用户签到系统

      每天一个 Key,offset 代表用户 ID,value 代表是否签到。

    2. 统计日活用户(DAU)

      每天记录用户是否访问网站,BITCOUNT 获取当天活跃用户总数。

    3. 权限开关系统

      对于某类功能,快速记录用户是否开启。

HyperLogLog:百万级数据去重统计神器

  1. 工作原理

    HyperLogLog 是 Redis 基于概率统计的算法结构,专门用于估算集合中不重复元素的个数,特点是内存恒定(12KB)速度极快,但存在小误差(~0.81%)。

    它利用哈希函数将输入元素映射到一串二进制位,通过记录这些位的“最长前缀 0”来反推出集合的基数。

  2. 常用命令

    1
    2
    3
    PFADD uv:20250513 192.168.1.100
    PFCOUNT uv:20250513 # 估算今日独立访客数
    PFMERGE uv:all uv:20250513 uv:20250514 # 估算20250513 - 20250514 的独立访客数
  3. 应用场景

    1. 网站 UV 去重统计

      页面访问量中同一 IP/用户 ID 多次访问只计一次。

    2. 营销活动用户去重

      记录参与人数(手机号/ID)不重复。

Geo:地理位置存储与空间查询

  1. 工作原理

    Redis GEO 模块是基于 Geohash + Sorted Set 实现的,将经纬度编码为 52bit 的整数,然后存入有序集合,支持高效的范围搜索和排序。

  2. 常用命令

    1
    2
    3
    GEOADD shop:location 116.40 39.90 "北京店" # 添加地理坐标
    GEORADIUS shop:location 116.39 39.91 10 km # 查找范围距离
    GEODIST shop:location "北京店" "上海店" # 计算两个坐标之间的距离。
  3. 应用场景

    1. 附近店铺/服务/司机查询

      外卖、打车、快递系统的核心功能。

    2. LBS 服务(Location-Based Service)

      结合定位服务为用户推荐附近商家或优惠。

Stream:原生分布式消息队列系统

  1. 工作原理

    Redis Stream 类似 Kafka 的日志系统,支持持久化、消费组、消费确认、阻塞读取等功能,适合构建轻量级消息处理系统

    每条消息都有一个唯一 ID(类似时间戳 + 序号),支持自动追踪消费进度、消息重试、死信处理等功能。

  2. 常用命令

    1
    2
    3
    4
    5
    XADD orders * userId 123 itemId 456

    XGROUP CREATE orders groupA $ # 创建消费组
    XREADGROUP GROUP groupA consumer1 STREAMS orders > # 读取未读消息
    XACK orders groupA <msg_id> # 确认消费
  3. 应用场景

    1. 异步任务处理系统

      订单生成后推送任务:发送短信、打印、库存更新等。

    2. 日志采集系统

      前端或后端埋点数据通过 Stream 聚合到后端统一处理

分布式锁:Redis 实现高性能互斥控制

  1. 工作原理

    使用 SET NX EX 原子指令实现加锁,配合唯一标识判断解锁权限,防止误删他人锁。可选用 Lua 脚本保证释放锁的原子性。

  2. 常用命令

    1. 加锁

      1
      SET lock:resource <uuid> NX EX 10

      含义:

      • lock:resource:锁的 Key
      • :唯一值,用于标记谁持有了这个锁(一般为线程 ID 或服务 ID)
      • NX:只有当 key 不存在时才设置成功(避免重复加锁)
      • EX 10:自动过期时间(避免死锁)
    2. 释放锁

      1
      2
      3
      4
      5
      if redis.call("GET", KEYS[1]) == ARGV[1] then
      return redis.call("DEL", KEYS[1])
      else
      return 0
      end
  3. 示例展示

    Redis 分布式锁实践用例:防止订单重复支付

    🧩 背景

    在电商系统中,用户在结算时可能由于网络卡顿、浏览器重复提交等情况重复点击“支付”按钮。如果没有妥善处理,可能导致:

    • 多次创建支付请求
    • 重复扣款或下单
    • 支付状态混乱,影响用户体验

    🎯 目标

    我们希望在支付接口中使用 Redis 分布式锁,确保同一个订单号只处理一次支付逻辑

    ✅ 实现方案

    1. 加锁逻辑(伪代码)

    1
    2
    3
    4
    5
    6
    7
    String lockKey = "lock:pay:" + orderId;
    String uuid = UUID.randomUUID().toString();
    boolean success = redis.set(lockKey, uuid, "NX", "EX", 10);

    if (!success) {
    return "支付处理中,请勿重复点击";
    }

    使用 SET NX EX 原子操作保证:只有第一个线程可以处理支付,其他线程直接返回提示信息

    2. 支付处理逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    try {
    // 校验订单状态
    if (orderService.isPaid(orderId)) {
    return "订单已支付";
    }

    // 调用第三方支付接口
    paymentService.call(orderId);

    // 修改订单状态
    orderService.markAsPaid(orderId);

    return "支付成功";

    } finally {
    // 使用 Lua 脚本安全释放锁
    String unlockScript =
    "if redis.call('get', KEYS[1]) == ARGV[1] then " +
    " return redis.call('del', KEYS[1]) " +
    "else " +
    " return 0 " +
    "end";
    redis.eval(unlockScript, Collections.singletonList(lockKey), Collections.singletonList(uuid));
    }
  4. 应用场景

    1. 防止秒杀超卖

      并发下操作库存前加锁,避免重复扣减。

    2. 防止重复提交订单/评论/投票

      同一用户在极短时间内可能多次提交。

限流系统设计

  1. 固定窗口限流(简单粗暴)

    1
    2
    INCR req:user:1
    EXPIRE req:user:1 60 # 设置有效期为60秒
  2. 滑动窗口限流(精确控制)

    1
    2
    3
    ZADD req:user:1 <当前时间戳> <随机ID>
    ZREMRANGEBYSCORE req:user:1 0 <当前时间戳 - 60秒>
    ZCARD req:user:1

    只统计过去 60 秒内的请求数量,更精确防刷。