Skip to content

7.10 Redis 教程 ​

简介 ​

Redis(Remote Dictionary Server)是一个开源的内存型键值数据库,读写延迟极低,广泛用于缓存、会话存储、计数器、排行榜、轻量队列等场景。

与 MySQL、PostgreSQL 等关系库相比,Redis 不承担复杂查询与强一致事务,而是作为"加速层"与"辅助存储"存在:

  • 热数据与派生视图放 Redis,减轻主库压力;
  • 权威数据与事务一致仍由关系库承担(参见 7.8 MySQL 数据库教程)。

Redis 提供 String、Hash、List、Set、Sorted Set 五种主要数据类型,以及过期、持久化、事务、发布订阅、Lua 脚本等能力,上手简单、使用面广。本章覆盖核心概念、redis-cli 操作、数据类型、过期与持久化、事务与管道、在 Go 中使用 Redis(包括缓存穿透/击穿/雪崩的处理)以及分布式锁、限流、队列、排行榜等典型应用。

一、核心概念 ​

概念说明
键值(Key-Value)每个键对应一种数据类型的值;键为二进制安全字符串,业务上常用带前缀命名,如 user:1001:profile。
内存存储读写主要在内存完成,需结合 TTL、淘汰策略与持久化控制成本与可恢复性。
单线程执行命令命令在主线程串行执行,单条命令天然原子;组合多条需用事务、Lua 或应用层协议。
逻辑库(database)默认 0~15 共 16 个库,用 SELECT 切换;集群模式下通常只用 0 库。
redis-server / cli服务端守护进程与命令行客户端。

二、安装与运行 ​

推荐使用 Docker 拉起 Redis,环境干净且版本可控(参见 7.9 Docker 基础入门):

bash
docker run -d \
  --name redis-dev \
  -p 6379:6379 \
  redis:7-alpine \
  redis-server --appendonly yes

需要密码时附加 --requirepass:

bash
docker run -d --name redis-dev -p 6379:6379 redis:7-alpine \
  redis-server --requirepass 'your_strong_password' --appendonly yes

本机安装也很简单:macOS 下 brew install redis && brew services start redis;Linux 直接用发行版包 redis-server。Redis 默认端口为 6379。

自检 ​

bash
redis-cli -h 127.0.0.1 -p 6379 ping
# 若设置了密码:
redis-cli -a 'your_strong_password' ping

返回 PONG 即服务可用。命令行的 -a 参数可能被写入 shell 历史,生产环境更推荐进入交互模式后执行 AUTH,或从配置文件读取。

三、redis-cli 基础 ​

1. 连接 ​

bash
redis-cli -h 127.0.0.1 -p 6379
# 进入后可执行:
AUTH your_strong_password
SELECT 0

2. 查看类型与帮助 ​

text
TYPE mykey          # 查看键的数据类型
HELP @string        # 查看 string 相关命令帮助

3. 遍历键:用 SCAN 代替 KEYS ​

  • KEYS pattern:阻塞式全表扫描,键多时会拖慢线上延迟,生产禁用。
  • SCAN cursor [MATCH pattern] [COUNT hint]:游标式迭代,分批返回,对线上友好。
text
SCAN 0 MATCH user:* COUNT 100

四、数据类型与常用命令 ​

1. String(字符串) ​

最基础的类型,值可以是字符串、数字、二进制数据。

命令说明
SET key value [EX sec]设置值,可选过期秒数
GET key获取值
INCR key / DECR key原子自增 / 自减(值为整数时)
EXPIRE key sec / TTL key设置过期 / 查看剩余秒数
text
SET page:home "<html>...</html>" EX 300   # 写入并设置 300 秒过期
GET page:home                             # 读取值
INCR api:visits                           # 原子自增,常用于访问计数
TTL page:home                             # 查看剩余 TTL(秒)

2. Hash(哈希) ​

适合存储结构化对象,避免为每个字段单独建键:

text
HSET user:1001 name Alice age 30   # 写入一个用户对象的多个字段
HGET user:1001 name                # 取单个字段:Alice
HGETALL user:1001                  # 取所有字段与值
HINCRBY user:1001 age 1            # 对 age 字段原子自增 1
HDEL user:1001 age                 # 删除某个字段

3. List(列表) ​

双端链表,可作为 FIFO / LIFO 队列使用:

text
LPUSH jobs:pending job-a job-b   # 从左侧依次插入,队头变为 job-b
RPOP  jobs:pending               # 从右侧弹出一个元素(FIFO 消费)
LLEN  jobs:pending               # 当前队列长度
LRANGE jobs:pending 0 -1         # 查看全部元素;长列表慎用,建议分页
BRPOP jobs:pending 10            # 阻塞式弹出,最多等 10 秒,避免空轮询

4. Set(集合) ​

无序、成员唯一,适合标签、去重、共同好友等:

text
SADD tags:post:1 go redis        # 添加成员(重复会被忽略)
SMEMBERS tags:post:1             # 列出全部成员
SISMEMBER tags:post:1 go         # 判断成员是否存在:1 存在 / 0 不存在
SCARD    tags:post:1             # 集合大小
SINTER   tags:post:1 tags:post:2 # 取交集,常用于"共同标签/好友"

5. Sorted Set(有序集合) ​

每个成员带一个分数(score),Redis 按分数有序存储,天然适合排行榜:

text
ZADD      leaderboard 100 player:1 200 player:2   # 写入成员及分数
ZINCRBY   leaderboard 50  player:1                # 给 player:1 的分数加 50
ZREVRANGE leaderboard 0 9 WITHSCORES              # 取分数最高的前 10 名及分数
ZRANK     leaderboard player:1                    # 按分数升序的排名(从 0 开始)
ZREVRANK  leaderboard player:1                    # 按分数降序的排名
ZSCORE    leaderboard player:1                    # 查询某成员的分数

6. Stream(了解即可) ​

Redis 5 引入的日志型数据结构,支持消费组与持久化消息流,语义接近轻量版 Kafka。入门阶段理解它与 List + MQ 的定位差异即可,深入可查官方 Streams 文档。

7. Key 设计建议 ​

  • 使用业务前缀避免键名冲突,如 cache:user:{id}、lock:order:{id}。
  • 为缓存键设置 TTL,避免内存无限增长。
  • 控制单个 value 体积,大 Key 会阻塞主线程(详见最佳实践)。

五、过期与内存管理 ​

1. 过期 ​

  • EXPIRE key sec / PEXPIRE key ms / 在写入时用 SET ... EX 均可设置 TTL;
  • TTL key 返回剩余秒数:-1 表示未设过期,-2 表示键不存在;
  • PERSIST key 移除过期时间,使键永久存在。

2. 淘汰策略 ​

配置 maxmemory 后,当内存打满时 Redis 会按 maxmemory-policy 淘汰数据:

策略含义
noeviction默认;写满时拒绝写入并返回错误
allkeys-lru全部键中按 LRU 近似淘汰
allkeys-lfu全部键中按访问频率淘汰(Redis 4+)
volatile-lru仅在带过期的键中按 LRU 淘汰
volatile-ttl仅在带过期的键中淘汰剩余 TTL 最短的

缓存场景常搭配 allkeys-lru;关键数据库用途则用 noeviction 配合监控预警。

六、持久化 ​

方式说明
RDB定时生成快照文件,恢复快;两次快照间的数据在宕机时会丢失
AOF追加写操作日志,可配置 appendfsync(如 everysec)折中性能与丢失窗口

两者可同时开启,RDB 用于快速恢复,AOF 提供更小的丢失窗口。上文 Docker 示例中 --appendonly yes 即开启 AOF。生产环境还需配合备份策略与 AOF 重写参数调优,超出本章范围。

七、事务与管道 ​

1. 事务:MULTI / EXEC / DISCARD ​

text
MULTI
SET a 1
INCR b
EXEC

要点:

  • MULTI 开启后,命令只入队不执行;EXEC 时依次执行,期间不会被其他客户端命令插队。
  • 与关系库不同,Redis 事务不支持运行时错误自动回滚——某条命令执行失败不会影响其他命令。
  • 需要"判断再写"的原子逻辑时,优先使用 Lua 脚本,或用 SET NX 这类自带原子语义的命令。

2. Pipeline(管道) ​

客户端一次发送多条命令、一次读回结果,减少网络往返,适合批量读写场景:

bash
redis-cli <<EOF
SET a 1
SET b 2
SET c 3
EOF

Pipeline 只是传输层的批处理,不提供原子边界,如需原子性仍须用 MULTI/EXEC 或 Lua。

八、在 Go 中使用 Redis ​

Go 生态中最常用的客户端是 go-redis(模块路径 github.com/redis/go-redis/v9):

bash
go get github.com/redis/go-redis/v9

1. 最小示例 ​

go
package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/redis/go-redis/v9"
)

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr:     "127.0.0.1:6379",
        Password: "",
        DB:       0,
    })
    defer rdb.Close()

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    if err := rdb.Set(ctx, "hello", "redis", time.Minute).Err(); err != nil {
        log.Fatal(err)
    }

    val, err := rdb.Get(ctx, "hello").Result()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("hello =", val)
}

几点说明:

  • 所有命令都接受 context.Context,用于超时与取消;
  • 返回的 *Cmd 通过 .Err() / .Result() / .Val() 取结果;
  • 连接地址、密码、库编号建议来自环境变量或配置中心,不要写死在代码里。

2. 判断"键不存在" ​

Get 在键不存在时返回 redis.Nil,不是普通错误,需要单独分支:

go
val, err := rdb.Get(ctx, key).Result()
switch {
case err == redis.Nil:
    // 键不存在
case err != nil:
    return err
default:
    // 正常使用 val
}

九、使用 Redis 做缓存 ​

把 Redis 当作关系库前面的读缓存是最常见的落地场景。本节介绍职责划分、Cache-Aside 流程、键与 TTL 设计,以及缓存穿透/击穿/雪崩的应对思路。

1. 与 MySQL 的分工 ​

存储适合
MySQL权威数据、复杂查询、事务与持久化要求高的记录
Redis热点读、可容忍短暂不一致、可按 TTL 重建的视图或会话数据

缓存里的数据是派生副本,业务应以"数据库成功写入"为最终一致目标,Redis 只负责加速访问。

2. Cache-Aside(旁路缓存) ​

读路径:

  1. 根据业务主键构造缓存键,如 cache:user:1001;
  2. GET 命中直接返回;
  3. 未命中时查 MySQL,取到后 SET key value EX ttl 回填缓存,再返回给调用方;
  4. 若数据库也不存在,按业务决定是否缓存"空值"(短 TTL)以防穿透。

写路径:先写库成功,再处理缓存,常见两种做法:

  • 删缓存:对受影响的键执行 DEL,下次读会回源重建,实现最简单;
  • 更新缓存:写库成功后 SET 新值,要保证与库字段同源,避免应用层算错。

"先删缓存还是先写库"在并发下各有极端窗口期,团队应统一规范,必要时采用版本号、延迟双删、消息驱动失效等方案。入门阶段掌握**"写库为准 + 显式失效或覆盖缓存"**即可。

3. 键与 TTL 设计 ​

  • 命名:带业务前缀与主键,如 cache:article:{id}、v1:cache:user:{id},方便批量失效;
  • TTL:按业务可接受的最大陈旧时间设置;避免大量键设成同一 TTL,建议加随机抖动;
  • 结构选型:整对象常用 String(配合 JSON / MessagePack 序列化);字段独立更新频繁时用 Hash 分字段缓存,减少整包重写。

4. 穿透、击穿、雪崩 ​

现象含义常见应对
穿透查询根本不存在的数据,缓存与库都没,每次请求都打到 DB缓存短 TTL 的"空值";或在入口用布隆过滤器拦截
击穿单个热点键过期瞬间,大量请求同时未命中并发压库用互斥锁(SET NX)只允许一个请求回源;或逻辑过期异步刷新;或热点键不设过短 TTL
雪崩大量键同时过期或 Redis 宕机,请求洪峰压垮数据库TTL 加随机抖动;Redis 高可用部署;关键路径限流降级,不唯依赖缓存

5. Go 实现示例 ​

下面是一个典型的"读缓存 → 未命中则回源"的完整代码片段(省略数据库访问细节):

go
func GetUser(ctx context.Context, rdb *redis.Client, db *sql.DB, id int) (*User, error) {
    key := fmt.Sprintf("cache:user:%d", id)

    // 1. 读缓存
    switch data, err := rdb.Get(ctx, key).Bytes(); {
    case err == nil:
        var u User
        if err := json.Unmarshal(data, &u); err == nil {
            return &u, nil
        }
    case err != redis.Nil:
        log.Printf("redis get failed, fallback to db: %v", err) // 降级直读库
    }

    // 2. 回源查库
    u, err := loadUserFromDB(ctx, db, id)
    if err != nil {
        return nil, err
    }
    if u == nil {
        // 数据库也没有:缓存空值防穿透
        rdb.Set(ctx, key, "null", 30*time.Second)
        return nil, nil
    }

    // 3. 回写缓存(TTL 加随机抖动,降低雪崩风险)
    payload, _ := json.Marshal(u)
    ttl := 5*time.Minute + time.Duration(rand.Intn(60))*time.Second
    rdb.Set(ctx, key, payload, ttl)
    return u, nil
}

写路径通常在 UPDATE / DELETE 成功后调用 rdb.Del(ctx, key) 使缓存失效。

十、典型应用 ​

1. 分布式锁 ​

抢锁用 SET lock:资源 token NX EX 秒:NX 保证只有一个客户端能设置成功,EX 防止持有者崩溃后死锁。

text
SET lock:order:1001 token-uuid-abc NX EX 30
# OK 表示抢到,(nil) 表示已被占用

释放时必须先校验"值是不是自己的 token"再删除,避免误删他人的锁。这一步应用 Lua 脚本保证原子性:

lua
if redis.call("GET", KEYS[1]) == ARGV[1] then
  return redis.call("DEL", KEYS[1])
else
  return 0
end

单机 Redis 上述方案可以防日常并发误用,但在主从切换、客户端 GC 暂停、锁持有时间超过 TTL 等边界条件下仍可能失效。跨多节点的强一致锁需要 Redlock 等更复杂的方案,具体可参考官方 Distributed locks with Redis。

2. 计数与限流 ​

按用户、按日计数:

text
INCR api:2026-04-17:user:1001
EXPIRE api:2026-04-17:user:1001 86400

固定窗口限流:键名携带时间窗口,INCR 后若返回 1 则设置 EXPIRE 为窗口长度;超过阈值即拒绝请求。更平滑的限流可用 Sorted Set 记录每次请求的时间戳,或在应用层实现令牌桶。

3. 幂等 / 防重 ​

对"请求 id"占位,处理前执行:

text
SET idempotency:order:1001:reqA 1 NX EX 3600

返回 OK 继续处理,返回 (nil) 说明已在处理或已完成,直接拒绝或返回上次结果。

4. 轻量队列 ​

生产者 LPUSH,消费者 BRPOP 阻塞弹出,避免空轮询:

text
LPUSH jobs:pending '{"type":"email","to":"a@b.com"}'
BRPOP jobs:pending 10

与 Kafka、RabbitMQ 相比,Redis 队列没有投递确认、重试、复杂路由等能力,适合对可靠性要求不高或可自行补偿的场景;正式消息链路建议选用专业 MQ。

5. 排行榜 ​

借助 Sorted Set:

text
ZADD leaderboard 100 player:1 200 player:2
ZREVRANGE leaderboard 0 9 WITHSCORES

分数可为销量、积分、时间戳等。注意控制成员数量,单个 ZSet 过大会成为大 Key。

十一、安全与部署注意点 ​

  • 绑定地址:默认监听 127.0.0.1 仅本机可连;监听 0.0.0.0 时必须配合防火墙限制来源,严禁无认证暴露公网。
  • 认证:requirepass 设置访问密码;Redis 6+ 可使用 ACL 做更细粒度的用户与权限控制。
  • TLS:敏感环境可开启传输加密,具体配置参考官方文档与云厂商说明。
  • 危险命令:对 FLUSHALL、CONFIG、KEYS 等可通过 rename-command 改名或禁用,防止误操作和入侵。

十二、最佳实践 ​

  • 禁用 KEYS *,用 SCAN 迭代;线上排查键时给 COUNT 一个合适的值。
  • 避免大 Key:单 value 过大会阻塞持久化与主线程,宜拆分或压缩。
  • 避免热 Key:单键 QPS 过高时考虑本地缓存前置、拆分(hash tag)或读写分离。
  • 长列表慎用 LRANGE 0 -1:必要时分页取。
  • 缓存一致性:明确 TTL 与淘汰策略,关键业务不唯一依赖 Redis 持久化;穿透/击穿/雪崩在接口设计阶段就应考虑。
  • 连接与超时:go-redis 请始终传入带超时的 context,避免慢 Redis 拖死调用方。

小结 ​

本章从 Redis 的定位、安装运行、redis-cli 基础,到五种主要数据类型、过期与淘汰、持久化、事务与管道,再到在 Go 中使用 go-redis、Cache-Aside 缓存模式及穿透/击穿/雪崩应对,最后覆盖了分布式锁、限流、幂等、队列、排行榜等典型落地场景与安全要点。

可用以下清单自检:

  • 能用 Docker 启动 Redis,并通过 redis-cli ping 完成自检;
  • 会用 String、Hash、List、Set、Sorted Set 完成基本操作;
  • 理解 SCAN 为什么要代替 KEYS;
  • 能描述 Cache-Aside 的读写路径,并说出穿透、击穿、雪崩的含义与一条应对手段;
  • 会用 SET NX EX 抢锁,并知道 Lua 释放锁的原因和单机锁的边界;
  • 能说出 RDB 与 AOF 的取舍、maxmemory-policy 的存在意义;
  • 能在 Go 里用 go-redis 完成一次带超时的 Set / Get,并正确处理 redis.Nil。

参考资料 ​

  1. Redis 官方文档
  2. Redis 命令参考
  3. go-redis
  4. Redis 持久化
  5. Distributed locks with Redis