Files
cs-note/hhs/Redis/03-基本命令速查.md
T
2026-05-25 20:51:57 +08:00

26 KiB
Raw Blame History

tags, create time
tags create time
Redis
缓存
命令
Stream
PubSub
Lua
2026-05-15 18:12

Redis 基本命令速查

概述

本文按数据类型分类梳理 Redis 常用命令,标注时间复杂度、典型场景和生产避坑点。覆盖 String / Hash / List / Set / ZSet 五大基础类型,以及 Pub/Sub、Stream、Geo、Lua 脚本等进阶能力。每条命令配有 Go(go-redis)代码示例,方便直接复制使用。

[!NOTE] 怎么用这份速查表?

  • 日常开发直接 Ctrl+F 搜命令名
  • 遇到新业务需求先看数据类型选型流程图,再查对应章节
  • 每个章节末尾的 callout 是"老手踩过的坑",建议通读一遍

数据类型选型速查

遇到业务需求时,按以下流程选择合适的 Redis 数据结构:

flowchart TD
    start["需要存什么?"] --> q1{"单值 / 键值对?"}
    q1 -- 是 --> s1["String<br/>缓存, 计数器, 分布式锁"]
    q1 -- 否 --> q2{"多个字段 / 对象?"}
    q2 -- 是 --> s2["Hash<br/>用户信息, 配置, 表单数据"]
    q2 -- 否 --> q3{"需要排序 / 排名?"}
    q3 -- 是 --> s3["ZSet<br/>排行榜, 延迟队列, 范围查询"]
    q3 -- 否 --> q4{"需要去重 / 集合运算?"}
    q4 -- 是 --> s4["Set<br/>标签, 共同关注, UV 统计"]
    q4 -- 否 --> q5{"需要队列 / 栈?"}
    q5 -- 是 --> s5["List<br/>消息队列, 最新列表, 栈"]
    q5 -- 否 --> s6["Pub/Sub 或 Stream<br/>广播通知 / 可靠消息队列"]

[!TIP] Stream 也是数据类型 Redis 5.0 新增的 Stream 适合需要持久化和消费确认的消息场景。选型时如果 Pub/Sub 的 "fire-and-forget" 不满足需求,直接上 Stream(详见下方 Stream 章节)。

String — 字符串操作

命令 返回值 复杂度 说明
SET key value [EX s] [PX ms] [NX|XX] OK / nil O(1) 万能设置,EX/PX 设过期,NX 仅不存在时写,XX 仅存在时写
GET key value / nil O(1) 获取值
MGET key [key...] values array O(N) 批量获取
SETRANGE key offset value length O(1)* 覆盖指定偏移量字节(*近似)
APPEND key value new-length O(1)* 追加到末尾
INCR key incremented val O(1) 原子 +1
INCRBY key increment incremented val O(1) 原子 +N
DECRBY key decrement decremented val O(1) 原子 -N
STRLEN key length O(1) 获取字符串长度
GETSET key value old value O(1) 已废弃,用 SET key value NX 替代
GETRANGE key start end substring O(N) 截取子串(end 为 -1 表示末尾)
SETEX key seconds value OK O(1) 已废弃,用 SET key value EX seconds 替代
PSETEX key milliseconds value OK O(1) 已废弃,用 SET key value PX milliseconds 替代
MSET key value [key value ...] OK O(N) 原子设置多个键
// SET 条件写入——分布式锁的核心
rdb.Set(ctx, "lock:order:"+orderId, "1", 3*time.Second, redis.KeepTTL|redis.OnlyIfAbsent)

// INCR 做限流计数器
rdb.IncrBy(ctx, "rate:user:"+userID, 1)

[!WARNING] MGET ≠ 事务保证 MGET 是并发执行 N 次 GET,不保证原子性。需要原子读多个字段请用 MULTI 或 Lua 脚本。

Hash — 哈希操作

命令 返回值 复杂度 说明
HSET key field value [field value ...] added / changed count O(N) 设置一个或多个字段
HGET key field value / nil O(1) 获取单个字段
HMGET key field [field ...] values array O(N) 批量获取多个字段
HGETALL key all fields O(N) 获取全部键值对 ⚠️ 大 hash 慎用
HDEL key field [field ...] deleted count O(N) 删除字段
HEXISTS key field 1 or 0 O(1) 判断字段是否存在
HINCRBY key field increment new value O(1) 原子递增字段值
HSCAN key cursor MATCH pattern COUNT n cursor, entries O(N) 安全遍历,不用 HKEYS/HVALS 全量扫
// 用户信息缓存——部分更新
pipe := rdb.Pipeline()
pipe.HSet(ctx, "user:42", "email", "new@email.com")
pipe.HSet(ctx, "user:42", "phone", "1380000")
pipe.Exec(ctx) // 管道内原子执行

[!TIP] HGETALL vs HSCAN HGETALL 一步到位获取全部字段,但大 hash(>10K 字段)会阻塞主线程。生产环境推荐 HSCAN 分片读取。

List — 列表操作

命令 返回值 复杂度 说明
LPUSH key elem [elem ...] list length O(N) 从左侧推入
RPUSH key elem [elem ...] list length O(N) 从右侧推入
LPOP key element / nil O(1) 弹出左侧元素
RPOP key element / nil O(1) 弹出右侧元素
LRANGE key start stop subarray O(S+N) 范围查询 S=start, N=stop-start
LLEN key length O(1) 列表长度
LINDEX key index element O(N) 按索引获取
LTRIM key start stop OK O(N) 只保留指定范围(固定队列长度必备)
BLPOP key [key ...] timeout element / nil O(timeout) 阻塞式 LPOP
BRPOP key [key ...] timeout element / nil O(timeout) 阻塞式 RPOP
RPOPLPUSH src dst element O(1) 已废弃,用 LMOVE 替代
LMOVE key where1 where2 element O(1) 弹出一端、push 到另一端(支持 LEFT/RIGHT)
// BRPOP 消费者模型
val, err := rdb.BRPop(ctx, 5*time.Second, "task_queue").Result()
if err == redis.Nil {
    return nil // 超时,无消息
}

[!QUESTION] LPUSH + RPOP 还是 RPUSH + LPOP? 两者性能相同(都是两端操作)。选择取决于语义——"先到的先处理"用 LPUSH+RPOP(FIFO),反之则相反。

Set — 集合操作

命令 返回值 复杂度 说明
SADD key member [member ...] added count O(N) 添加成员
SREM key member [member ...] removed count O(N) 移除成员
SMEMBERS key all members O(N) 获取全部 ⚠️
SISMEMBER key member 1 or 0 O(1) 判断成员
SCARD key cardinality O(1) 元素个数
SRANDMEMBER key [count] member(s) O(N) 随机取(不删除)
SPOP key [count] member(s) O(N) 随机取并删除
SINTER key [key ...] intersection O(N*M) 交集 ⚠️ 跨节点成本高
SUNION key [key ...] union O(N*M) 并集
SDIFF key [key ...] difference O(N*M) 差集
SINTERSTORE dst key [key ...] result count O(N*M) 交集结果存入新 key
// 共同关注——用户 A 和用户 B 都关注的人
common := rdb.SInter(ctx, "follow:userA", "follow:userB").Val()

// 抽奖系统——随机取 N 个不重复中奖者
winners := rdb.SPopN(ctx, "entry:lottery", 5).Val()

[!TIP] Set 的典型应用场景

  • 去重:UV 统计(SADD + SCARD)
  • 交集/并集:共同好友、标签聚合
  • 随机抽样:SPOP / SRANDMEMBER 做抽奖或推荐

ZSet —— 有序集合

命令 返回值 复杂度 说明
ZADD key score member [score member ...] added/changed O(N log N) 添加/更新
ZSCORE key member score / nil O(1) 获取分数
ZINCRBY key increment member new score O(log N) 增加分数
ZRANGE key start stop [WITHSCORES] members O(log N+M) 正序范围
ZREVRANGE key start stop [WITHSCORES] members O(log N+M) 倒序范围
ZRANGEBYSCORE key min max [WITHSCORES] members O(log N+M) 分数区间
ZRANK key member rank / nil O(log N) 升序排名
ZREVRANK key member reverse rank / nil O(log N) 降序排名
ZCARD key count O(1) 元素数
ZCOUNT key min max count O(log N) 分数范围内元素数
ZREM key member [member ...] removed O(M log N) 移除
ZREMRANGEBYRANK key start stop removed O(log N+M) 按排名删
ZREMRANGEBYSCORE key min max removed O(log N+M) 按分数删
ZMSCORE key member [member ...] scores O(N) 批量获取分数(7.0+)
ZPOPMAX key [count] top members O(log N+M) 弹出最高分
ZPOPMIN key [count] bottom members O(log N+M) 弹出最低分
ZINTERSTORE dst numkeys key [numkeys...] cardinality O(N×K+K log K) 多 ZSet 求交集存新 key
ZUNIONSTORE dst numkeys key [numkeys...] cardinality O(N×K+K log K) 多 ZSet 求并集存新 key
ZRANDMEMBER key [count] [WITHSCORES] members O(N) 随机取(7.0+)
ZRANGEBYLEX key min max [LIMIT offset count] members O(log N+M) 同分数按 member 字典序排
ZSCAN key cursor MATCH pattern COUNT n cursor, entries O(N) 安全遍历
// 排行榜——获取 TOP 10
top10 := rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, 9).Val()
for _, z := range top10 {
    fmt.Printf("%s: %.1f\n", z.Member, z.Score)
}

// 分数区间查询——80~100 分的学生
students := rdb.ZRangeByScore(ctx, "score:classA", redis.ZRangeBy{
    Min: "80", Max: "100",
}).Val()

[!TIP] 分数边界写法 [ 包含、( 不包含。例如 "[80" 表示 ≥80,"(100" 表示 <100。-inf 和 +inf 表示无穷大。

通用命令

命令 返回值 复杂度 说明
DEL key [key ...] deleted count O(N) N = key 的平均大小,大 key 会阻塞
EXPIRE key seconds 1 or 0 O(1) 设过期时间(秒)
PEXPIRE key milliseconds 1 or 0 O(1) 毫秒级过期
TTL key seconds / -1 / -2 O(1) 剩余存活秒数
PTTL key milliseconds O(1) 剩余存活毫秒
EXPIREAT key timestamp 1 or 0 O(1) 按 Unix 时间戳过期
KEYS pattern matching keys O(N) ⛔ 全库扫描,生产禁用!
SCAN cursor MATCH pattern COUNT n cursor, keys 增量迭代 O(1) ✅ 安全的模糊查找
DBSIZE count O(1) 当前数据库 key 总数
SELECT index OK O(1) 切换数据库(0~15)
FLUSHDB OK O(N) 清空当前库
FLUSHALL OK O(N) 清空所有库
TYPE key string/hash/list... O(1) 查看数据类型
EXISTS key [key ...] existing count O(N) 判断 key 是否存在,支持批量
OBJECT ENCODING key encoding name O(1) 查看底层编码(排查性能利器)
UNLINK key [key ...] deleted count O(N) 异步删除,不阻塞主线程 ✅
MOVE key db 1 or 0 O(1) 移动到其他数据库(集群不支持)
RENAME key newkey OK O(1) 重命名(不管类型)
RANDOMKEY key / nil O(1) 随机返回一个 key(集群不支持)
// 优雅删除大 key——首选 UNLINK(异步删除,不阻塞主线程)
rdb.Unlink(ctx, "large:key1", "large:key2")

// 或者用 SSCAN/HSCAN 分批删除字段
iter := rdb.HScan(ctx, "big:hash", 0, "", 100).Iterator()
for iter.Next(ctx) {
    rdb.HDel(ctx, "big:hash", iter.Val()...)
}

[!TIP] DEL vs UNLINK DEL 是同步删除,大 key(如百万元素的 Set)会阻塞主线程。UNLINK(4.0+)在后台线程异步释放内存,生产环境优先用 UNLINK。

[!WARNING] TTL 的陷阱

  • TTL 返回 -1 表示 key 存在但无过期时间,-2 表示 key 不存在
  • Redis 的过期键删除是惰性删除 + 定期删除混合策略,不能保证设置 EXPIRE 后立即清理
  • 集群模式下 MOVE、RANDOMKEY、FLUSHALL 不可用

SCAN vs KEYS 深度对比

# ⛔ KEYS — 阻塞整个服务
KEYS user:*
# 100 万 key 中匹配 1000 个 → 主线程阻塞数百毫秒甚至秒级

# ✅ SCAN — 增量迭代,每次 O(1)
SCAN 0 MATCH user:* COUNT 100
# 返回游标 + 一小批匹配 key → 用游标继续,直到游标回到 0
// Go 中的 SCAN 用法
iter := rdb.Scan(ctx, 0, "user:prefix:*", 100).Iterator()
for iter.Next(ctx) {
    fmt.Println(iter.Val())
}
// 最后检查 iter.Err()

[!WARNING] SCAN 的不确定性

  • 某个 key 可能返回多次(中间被修改或删除)
  • 某些匹配的 key 可能不返回
  • 因此 不要用 SCAN 做统计计数,只能用于清理或迁移等容错场景

管道 Pipeline vs 事务 Transaction

先理解问题:为什么要"打包"命令?

Redis 通信遵循一问一答(request/response)模式——客户端发一条命令,等回复,再发下一条。每次"发送 → 等待 → 接收"的网络往返叫做一次 RTT(Round-Trip Time,往返时延)。

[!QUESTION] RTT 有多贵? 假设内网 RTT = 0.5ms,发送 1000 条命令逐条执行需要 500ms。如果把它们打包一次发送,只需要 ~0.5ms——性能差 1000 倍。

Pipeline 和 Transaction 都是"打包"方案,但它们解决的问题不同:

flowchart LR
    A["客户端发送 N 条命令"] --> B{"核心诉求?"}
    B -- "快!" --> C["Pipeline<br/>减少网络往返"]
    B -- "安全!" --> D["Transaction<br/>保证命令原子执行"]

Pipeline —— 批量发送,减少 RTT

Pipeline 的原理很简单:客户端先把 N 条命令存在本地缓冲区,一次性发送给 Redis 服务端,服务端依次执行后把 N 个回复一次性返回。

[!NOTE] Pipeline 的两个关键点

  1. 不保证原子性——Pipeline 中的命令仍然可能被其他客户端的命令穿插执行
  2. 不关心业务逻辑——它纯粹是一个"网络优化层",减少的是等待时间
pipe := rdb.Pipeline()
pipe.Set(ctx, "k1", "v1", 0)
pipe.Set(ctx, "k2", "v2", 0)
pipe.Set(ctx, "k3", "v3", 0)
results, err := pipe.Exec(ctx) // 一次性发送,一次性接收所有结果
# CLI 模式
redis-cli --pipeline <<< $'SET k1 v1\nSET k2 v2\nSET k3 v3'

[!TIP] Pipeline 不是银弹 单次 batch 控制在 50~200 条为佳。过大反而会增加 Redis 服务端的内存压力和等待时间。

MULTI / EXEC —— 原子化执行

事务解决的是另一个问题:我想让一组命令"一起执行",中间不能被别人插队。

MULTI → 命令1 → 命令2 → EXEC 的执行流程是:

sequenceDiagram
    participant C as "客户端"
    participant S as "Redis 服务端"
    C->>S: "MULTI 开启事务"
    S-->>C: OK
    C->>S: "SET acc:A 900"
    S-->>C: "QUEUED 排队,不立即执行"
    C->>S: "SET acc:B 1100"
    S-->>C: QUEUED
    C->>S: "EXEC 提交"
    S-->>C: "[OK, OK] 依次执行,一次性返回"

[!QUESTION] "QUEUED" 是什么意思? MULTI 之后、EXEC 之前的命令不会立即执行,而是进入服务端的队列。只有收到 EXEC 后,Redis 才会依次执行队列中的所有命令,并把结果一次性返回。

pipe, cancel := rdb.TxPipeline() // TxPipeline = Transaction Pipeline,即 MULTI/EXEC 包裹的 Pipeline
defer cancel()

pipe.Set(ctx, "acc:A", "900", 0)
pipe.Set(ctx, "acc:B", "1100", 0)
_, err := pipe.Exec(ctx) // 触发 EXEC,所有命令原子执行
# Redis CLI 中的事务
MULTI
> SET acc:A 900       # QUEUED
> SET acc:B 1100      # QUEUED
EXEC                   # 原子提交

WATCH —— 乐观锁(防止"并发踩踏")

事务保证了"不插队",但如果另一个客户端在我 WATCH 到 EXEC 之间修改了数据怎么办?WATCH 就是为此设计的——它在 EXEC 前检查被监视的 key 是否被改过,如果改了就自动放弃整个事务。

sequenceDiagram
    participant A as "客户端 A"
    participant S as "Redis 服务端"
    participant B as "客户端 B"
    A->>S: WATCH stock:1001
    A->>S: MULTI
    A->>S: DECR stock:1001
    B->>S: "SET stock:1001 999 被其他客户端修改"
    A->>S: EXEC
    S-->>A: "nil 事务被取消,stock:1001 已变"

[!NOTE] WATCH 的使用范式

  1. WATCH key —— 监视目标 key
  2. 读取 key 的值,在客户端做判断
  3. MULTI → 写命令 → EXEC
  4. 如果 EXEC 返回 nil,说明数据被改过,需要重试整个流程

这就是经典的 CAS(Compare-And-Swap) 模式。实际开发中,Lua 脚本通常更简洁可靠。

对比总结

维度 Pipeline Transaction (MULTI/EXEC)
核心目的 减少网络往返(性能优化) 保证命令连续执行(一致性)
原子性 ❌ 不保证 ✅ 保证(不会被其他命令插队)
命令失败处理 单个失败不影响其他命令 语法错误 → 整个事务取消;运行时错误 → 仅该命令失败,其他继续
WATCH 支持 ❌ ✅ 乐观锁
适用场景 批量写入、批量查询 转账、库存扣减等需要原子性的操作
性能开销 几乎无额外开销 略高于 Pipeline(需要服务端队列)

[!WARNING] Redis 事务不是"数据库事务" 传统数据库的事务有 ACID 保证,失败会回滚。Redis 事务只保证不中断(队列中的命令会全部执行),中间命令出错不会回滚。如果需要"出错就回滚",请使用 Lua 脚本。

Pub/Sub — 发布订阅

Pub/Sub 是 Redis 内置的消息广播模型:发布者向 channel 发消息,所有订阅者实时收到。注意它是 fire-and-forget——不持久化、不确认、离线消息会丢失。

命令 返回值 复杂度 说明
SUBSCRIBE channel [channel ...] messages O(N) 订阅一个或多个 channel
UNSUBSCRIBE channel [channel ...] OK O(N) 取消订阅
PUBLISH channel message receivers count O(N+M) 发布消息,N=channel 订阅者数,M=pattern 匹配数
PSUBSCRIBE pattern [pattern ...] messages O(N) 按 glob 模式订阅(如 news.*)
PUNSUBSCRIBE pattern [pattern ...] OK O(N) 取消模式订阅
PUBSUB CHANNELS [pattern] channels O(N) 列出活跃 channel
PUBSUB NUMSUB [channel ...] channel-count pairs O(N) 查询各 channel 订阅者数量
// 订阅端——监听消息
sub := rdb.Subscribe(ctx, "order:events")
ch := sub.Channel()
for msg := range ch {
    fmt.Printf("channel=%s payload=%s\n", msg.Channel, msg.Payload)
}

// 发布端——广播事件
receivers, _ := rdb.Publish(ctx, "order:events", `{"orderId":"1001","status":"paid"}`).Result()
fmt.Printf("delivered to %d subscribers\n", receivers)

[!QUESTION] Pub/Sub vs Stream,怎么选?

  • Pub/Sub:实时广播,不关心历史,用完即丢(适合通知、缓存失效广播)
  • Stream:持久化消息队列,支持消费组、ACK、回溯(适合业务事件、任务队列)
  • 简单规则:需要"至少一次"保证就用 Stream,否则 Pub/Sub 足够
  • 两者命令对比详见 hhs/Redis/15-Stream

Stream — 消息队列

Redis 5.0 引入的日志型数据结构,弥补了 Pub/Sub 不持久化的缺陷。底层用 Radix Tree + Listpack 实现,支持消费组(Consumer Group)和消息确认(ACK),可胜任轻量级任务队列。

命令 返回值 复杂度 说明
XADD key [MAXLEN ~ n] ID field value [field value ...] entry ID O(1) 追加消息,* 自动生成 ID(时间戳-序号)
XLEN key length O(1) 消息总数
XRANGE key start end [COUNT n] entries O(N) 按 ID 范围正序读取(-/+ 表示最小/最大)
XREVRANGE key end start [COUNT n] entries O(N) 按 ID 范围倒序读取
XREAD [COUNT n] [BLOCK ms] STREAMS key [key ...] ID [ID ...] entries O(N) 读取消息,$ 表示仅新消息,BLOCK 阻塞等待
XGROUP CREATE key groupname ID [MKSTREAM] OK O(1) 创建消费者组,$ 只消费新消息,0 从头消费
XREADGROUP GROUP group consumer [COUNT n] [BLOCK ms] [NOACK] STREAMS key [key ...] ID entries O(N) 组内消费,> 表示未分配的新消息
XACK key group ID [ID ...] acked count O(N) 确认消费,释放消息
XDEL key ID [ID ...] deleted count O(N) 删除指定消息
XTRIM key MAXLEN [~] n trimmed count O(N) 裁剪流长度,~ 近似裁剪(性能更好)
XPENDING key group [start end count] [consumer] pending info O(N) 查看未确认(pending)消息
XCLAIM key group consumer min-idle-time ID [ID ...] entries O(N) 认领超时未 ACK 的消息(故障转移)
XINFO STREAM key stream info O(1) 查看流元信息
XINFO GROUPS key group info O(N) 查看所有消费者组
// 生产者——追加消息
id, _ := rdb.XAdd(ctx, &redis.XAddArgs{
    Stream: "order:events",
    MaxLen: 10000,   // 近似裁剪,控制内存
    Approx: true,
    Values: map[string]interface{}{"orderId": "1001", "status": "paid"},
}).Result()
fmt.Println("entry ID:", id)

// 消费者组——组内竞争消费
msgs, _ := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
    Group:    "order-consumers",
    Consumer: "worker-1",
    Streams:  []string{"order:events", ">"},
    Count:    10,
    Block:    5 * time.Second,
}).Result()

for _, stream := range msgs {
    for _, msg := range stream.Messages {
        // 处理消息...
        rdb.XAck(ctx, "order:events", "order-consumers", msg.ID) // 确认消费
    }
}

[!WARNING] 消息丢失的两个陷阱

  1. 生产端:XADD 返回后消息已写入内存,但若 Redis 崩溃且未配置 AOF,消息会丢失。对可靠性要求高的场景请开启 appendfsync everysec
  2. 消费端:XREADGROUP 读取后消息进入 Pending List(PEL),必须显式 XACK 才算消费完成。忘记 ACK 会导致消息堆积在 PEL 中,需要用 XPENDING + XCLAIM 清理

[!TIP] MAXLEN vs MINID

  • XADD key MAXLEN ~ 10000:限制流最大约 10000 条(近似裁剪,性能好)
  • XADD key MINID ~ 1685000000000-0:限制最小消息 ID(按时间裁剪,7.0+)
  • 两者都建议加 ~ 做近似裁剪,避免精确裁剪带来的 O(N) 阻塞

Geo — 地理位置

Redis 3.2+ 引入的地理位置类型,底层用 ZSet 实现(GeoHash 编码为 score),支持范围查询和距离计算。

命令 返回值 复杂度 说明
GEOADD key longitude latitude member [lon lat member ...] added count O(log N) 添加地理位置
GEOPOS key member [member ...] coordinates O(N) 获取坐标
GEODIST key member1 member2 [m|km|ft|mi] distance O(1) 两点间距离
GEORADIUS key lon lat radius m|km|ft|mi [WITHCOORD] [WITHDIST] [ASC|DESC] [COUNT n] members O(N+log N) 以坐标为中心搜索 ⚠️ 6.2+ 已废弃,用 GEOSEARCH
GEORADIUSBYMEMBER key member radius m|km|ft|mi members O(N+log N) 以成员为中心搜索 ⚠️ 6.2+ 已废弃
GEOSEARCH key FROMLONLAT lon lat BYRADIUS radius m|km [ASC|DESC] [COUNT n] [WITHCOORD] [WITHDIST] members O(N+log N) ✅ 6.2+ 推荐,统一搜索命令
GEOSEARCH key FROMMEMBER member BYRADIUS radius m|km members O(N+log N) 以成员为中心搜索
GEOSEARCHSTORE dst src ... stored count O(N+log N) 搜索结果存入新 key(6.2+)
GEOHASH key member [member ...] geohash strings O(N) 返回 GeoHash 编码字符串
// 门店入库——添加地理位置
rdb.GeoAdd(ctx, "stores:shanghai",
    &redis.GeoLocation{Name: "store-001", Longitude: 121.4737, Latitude: 31.2304},
    &redis.GeoLocation{Name: "store-002", Longitude: 121.4897, Latitude: 31.2397},
)

// 附近 3km 的门店——GEOSEARCH
stores, _ := rdb.GeoSearchLocation(ctx, "stores:shanghai", &redis.GeoSearchLocationQuery{
    GeoSearchQuery: redis.GeoSearchQuery{
        Longitude:  121.4800,
        Latitude:   31.2350,
        Radius:     3,
        RadiusUnit: "km",
        Sort:       "ASC",
        Count:      10,
    },
    WithCoord: true,
    WithDist:  true,
}).Result()

[!TIP] GEO 底层就是 ZSet 可以用 ZRANGE、ZREM 等 ZSet 命令操作 Geo key。GEODIST 计算的是球面距离(Haversine 公式),精度在 ~0.5% 以内。

Lua 脚本速查

Redis 2.6+ 内置 Lua 解释器,脚本在服务端原子执行,是实现 CAS、限流器、排行榜原子操作的利器。

命令 返回值 说明
EVAL "script" numkeys key [key ...] arg [arg ...] script result 执行 Lua 脚本
EVALSHA sha1 numkeys key [key ...] arg [arg ...] script result 按 SHA1 执行(脚本已缓存)
SCRIPT LOAD "script" SHA1 缓存脚本,返回 SHA1
SCRIPT EXISTS sha1 [sha1 ...] 1 or 0 array 检查脚本是否已缓存
SCRIPT FLUSH OK 清空脚本缓存
SCRIPT KILL OK 终止正在执行的脚本(只限无写操作时)
// 原子扣减库存——Lua 实现 check-and-set
script := redis.NewScript(`
    local stock = tonumber(redis.call('GET', KEYS[1]))
    if stock and stock >= tonumber(ARGV[1]) then
        redis.call('DECRBY', KEYS[1], ARGV[1])
        return 1
    end
    return 0
`)
result, _ := script.Run(ctx, rdb, []string{"stock:sku:1001"}, 1).Int()
// result == 1 表示扣减成功,0 表示库存不足

[!WARNING] Lua 脚本的限制

  • 脚本执行期间阻塞整个 Redis,严禁写耗时逻辑(循环上限建议 < 5000 次迭代)
  • 脚本中不能访问外部网络或文件系统
  • 集群模式下所有 KEYS 必须在同一个 slot(用 {hash_tag} 保证)
  • SCRIPT KILL 只能终止未执行写操作的脚本;已写数据的脚本只能等它跑完或重启 Redis

[!NOTE] EVAL vs EVALSHA EVAL 每次发送完整脚本源码,EVALSHA 只发送 40 字节的 SHA1 摘要。高并发场景务必用 EVALSHA + SCRIPT LOAD,减少网络开销。go-redis 的 NewScript 会自动处理 EVALSHA 失败后回退 EVAL。

关联笔记