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 基础入门):
docker run -d \
--name redis-dev \
-p 6379:6379 \
redis:7-alpine \
redis-server --appendonly yes需要密码时附加 --requirepass:
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。
自检
redis-cli -h 127.0.0.1 -p 6379 ping
# 若设置了密码:
redis-cli -a 'your_strong_password' ping返回 PONG 即服务可用。命令行的 -a 参数可能被写入 shell 历史,生产环境更推荐进入交互模式后执行 AUTH,或从配置文件读取。
三、redis-cli 基础
1. 连接
redis-cli -h 127.0.0.1 -p 6379
# 进入后可执行:
AUTH your_strong_password
SELECT 02. 查看类型与帮助
TYPE mykey # 查看键的数据类型
HELP @string # 查看 string 相关命令帮助3. 遍历键:用 SCAN 代替 KEYS
KEYS pattern:阻塞式全表扫描,键多时会拖慢线上延迟,生产禁用。SCAN cursor [MATCH pattern] [COUNT hint]:游标式迭代,分批返回,对线上友好。
SCAN 0 MATCH user:* COUNT 100四、数据类型与常用命令
1. String(字符串)
最基础的类型,值可以是字符串、数字、二进制数据。
| 命令 | 说明 |
|---|---|
SET key value [EX sec] | 设置值,可选过期秒数 |
GET key | 获取值 |
INCR key / DECR key | 原子自增 / 自减(值为整数时) |
EXPIRE key sec / TTL key | 设置过期 / 查看剩余秒数 |
SET page:home "<html>...</html>" EX 300 # 写入并设置 300 秒过期
GET page:home # 读取值
INCR api:visits # 原子自增,常用于访问计数
TTL page:home # 查看剩余 TTL(秒)2. Hash(哈希)
适合存储结构化对象,避免为每个字段单独建键:
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 队列使用:
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(集合)
无序、成员唯一,适合标签、去重、共同好友等:
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 按分数有序存储,天然适合排行榜:
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
MULTI
SET a 1
INCR b
EXEC要点:
MULTI开启后,命令只入队不执行;EXEC时依次执行,期间不会被其他客户端命令插队。- 与关系库不同,Redis 事务不支持运行时错误自动回滚——某条命令执行失败不会影响其他命令。
- 需要"判断再写"的原子逻辑时,优先使用 Lua 脚本,或用
SET NX这类自带原子语义的命令。
2. Pipeline(管道)
客户端一次发送多条命令、一次读回结果,减少网络往返,适合批量读写场景:
redis-cli <<EOF
SET a 1
SET b 2
SET c 3
EOFPipeline 只是传输层的批处理,不提供原子边界,如需原子性仍须用 MULTI/EXEC 或 Lua。
八、在 Go 中使用 Redis
Go 生态中最常用的客户端是 go-redis(模块路径 github.com/redis/go-redis/v9):
go get github.com/redis/go-redis/v91. 最小示例
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,不是普通错误,需要单独分支:
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(旁路缓存)
读路径:
- 根据业务主键构造缓存键,如
cache:user:1001; GET命中直接返回;- 未命中时查 MySQL,取到后
SET key value EX ttl回填缓存,再返回给调用方; - 若数据库也不存在,按业务决定是否缓存"空值"(短 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 实现示例
下面是一个典型的"读缓存 → 未命中则回源"的完整代码片段(省略数据库访问细节):
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 防止持有者崩溃后死锁。
SET lock:order:1001 token-uuid-abc NX EX 30
# OK 表示抢到,(nil) 表示已被占用释放时必须先校验"值是不是自己的 token"再删除,避免误删他人的锁。这一步应用 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. 计数与限流
按用户、按日计数:
INCR api:2026-04-17:user:1001
EXPIRE api:2026-04-17:user:1001 86400固定窗口限流:键名携带时间窗口,INCR 后若返回 1 则设置 EXPIRE 为窗口长度;超过阈值即拒绝请求。更平滑的限流可用 Sorted Set 记录每次请求的时间戳,或在应用层实现令牌桶。
3. 幂等 / 防重
对"请求 id"占位,处理前执行:
SET idempotency:order:1001:reqA 1 NX EX 3600返回 OK 继续处理,返回 (nil) 说明已在处理或已完成,直接拒绝或返回上次结果。
4. 轻量队列
生产者 LPUSH,消费者 BRPOP 阻塞弹出,避免空轮询:
LPUSH jobs:pending '{"type":"email","to":"a@b.com"}'
BRPOP jobs:pending 10与 Kafka、RabbitMQ 相比,Redis 队列没有投递确认、重试、复杂路由等能力,适合对可靠性要求不高或可自行补偿的场景;正式消息链路建议选用专业 MQ。
5. 排行榜
借助 Sorted Set:
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。