Redis 完全讲解:从数据结构到生产部署

2024-09-10 后端

一、Redis 简介

Redis(Remote Dictionary Server)是一个开源的内存数据结构存储系统,通常被称为缓存数据库或数据结构服务器。

1.1 核心特性

特性 说明
内存存储 数据主要存储在内存中,读写速度极快(百万级 QPS)
数据结构丰富 支持 String、Hash、List、Set、ZSet、HyperLogLog、Geo、Stream 等
持久化 支持 RDB 快照和 AOF 日志两种持久化方式
高可用 主从复制、Sentinel 哨兵、Cluster 集群
原子性 所有操作都是原子性的,支持 Lua 脚本和事务
发布订阅 支持 Pub/Sub 消息模式
分布式 原生支持 Cluster 分片

1.2 安装与启动

Docker 安装(推荐):

docker run -d --name redis -p 6379:6379 redis:7-alpine

Ubuntu/Debian:

apt install redis-server
systemctl start redis

macOS:

brew install redis
brew services start redis

连接测试:

redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> PING
PONG

1.3 redis-cli 常用命令

# 连接
redis-cli -h host -p port -a password

# 信息查看
INFO             # 服务器信息
INFO memory      # 内存信息
DBSIZE           # 当前数据库 key 数量
CLIENT LIST      # 客户端列表

# 键操作
KEYS pattern     # 查找键(生产环境慎用)
SCAN cursor      # 游标遍历(推荐)
TYPE key         # 查看类型
TTL key          # 查看过期时间
EXPIRE key sec   # 设置过期时间
DEL key          # 删除键

# 服务器管理
FLUSHDB          # 清空当前数据库
FLUSHALL         # 清空所有数据库
MONITOR          # 实时监控命令
CONFIG GET *     # 查看配置
SHUTDOWN         # 关闭

二、五种基本数据结构

2.1 String(字符串)

String 是 Redis 最基本的数据类型,最大存储 512MB。

适用场景:缓存、计数器、分布式锁、Session 共享。

常用命令:

SET key value                    # 设置值
SET key value EX 10              # 设置值并设置过期时间(秒)
SET key value PX 10000           # 设置值并设置过期时间(毫秒)
SETNX key value                  # 不存在时才设置(分布式锁关键)
GET key                          # 获取值
GETSET key value                 # 获取旧值并设置新值
MSET k1 v1 k2 v2                 # 批量设置
MGET k1 k2                       # 批量获取
INCR key                         # 自增 1
INCRBY key 10                    # 自增指定值
DECR key                         # 自减 1
DECRBY key 10                    # 自减指定值
STRLEN key                       # 获取字符串长度
APPEND key value                 # 追加字符串

实战示例:商品点赞:

# 点赞
INCR article:1001:likes
# 获取点赞数
GET article:1001:likes

2.2 Hash(哈希)

Hash 是一个 field-value 映射表,适合存储对象。

适用场景:用户信息、商品详情、配置项。

常用命令:

HSET user:1001 name "张三" age 25
HGET user:1001 name           # 获取单个字段
HMGET user:1001 name age      # 获取多个字段
HGETALL user:1001             # 获取所有字段
HINCRBY user:1001 age 1       # 字段自增
HDEL user:1001 age            # 删除字段
HEXISTS user:1001 name        # 检查字段是否存在
HLEN user:1001                # 获取字段数量
HKEYS user:1001               # 获取所有字段名
HVALS user:1001               # 获取所有字段值

实战示例:用户会话:

# 存储用户会话
HSET session:abc123 user_id 1001 login_time "2024-09-10 10:00:00" ip "192.168.1.1"
# 过期
EXPIRE session:abc123 3600

2.3 List(列表)

List 是双向链表,支持两端插入弹出。

适用场景:消息队列、时间线、最新消息。

常用命令:

LPUSH queue task1              # 左侧压入
RPUSH queue task1              # 右侧压入
LPOP queue                     # 左侧弹出
RPOP queue                     # 右侧弹出
LRANGE queue 0 -1              # 遍历所有元素
LLEN queue                     # 获取长度
LINDEX queue 0                 # 按下标获取
LTRIM queue 0 99              # 截取前100个
BLPOP queue 5                  # 阻塞式弹出(5秒超时)
BRPOP queue 5                  # 阻塞式右侧弹出

实战示例:最新消息列表:

# 始终保持前100条
LPUSH news:list "news-1"
LTRIM news:list 0 99

2.4 Set(集合)

Set 是无序、不重复的字符串集合,支持集合运算。

适用场景:标签系统、好友关系、去重、抽奖。

常用命令:

SADD user:1001:tags "python" "redis"          # 添加元素
SREM user:1001:tags "redis"                   # 移除元素
SMEMBERS user:1001:tags                       # 获取所有元素
SISMEMBER user:1001:tags "python"             # 判断是否存在
SCARD user:1001:tags                          # 获取元素数量
SPOP user:1001:tags 2                         # 随机弹出2个
SRANDMEMBER user:1001:tags 3                  # 随机获取3个(不删除)

# 集合运算
SINTER set1 set2                              # 交集
SUNION set1 set2                              # 并集
SDIFF set1 set2                               # 差集

实战示例:共同好友:

SADD user:1:friends 2 3 4 5
SADD user:2:friends 3 4 6 7
SINTER user:1:friends user:2:friends          # 结果: 3 4

2.5 ZSet(有序集合)

ZSet 在 Set 基础上增加了 score(分数),按分数排序。

适用场景:排行榜、延时队列、范围查询。

常用命令:

ZADD leaderboard 100 "player1" 200 "player2"   # 添加成员
ZINCRBY leaderboard 10 "player1"                # 增加分数
ZSCORE leaderboard "player1"                    # 获取分数
ZRANK leaderboard "player1"                     # 获取排名(升序)
ZREVRANK leaderboard "player1"                  # 获取排名(降序)
ZRANGE leaderboard 0 9 WITHSCORES               # 前10名(升序)
ZREVRANGE leaderboard 0 9 WITHSCORES            # 前10名(降序)
ZRANGEBYSCORE leaderboard 100 200               # 按分数范围查询
ZCARD leaderboard                               # 获取成员数量
ZREM leaderboard "player1"                      # 删除成员
ZCOUNT leaderboard 100 200                      # 统计分数范围内数量

实战示例:实时排行榜:

ZINCRBY game:score:20240910 150 "user:1001"
ZINCRBY game:score:20240910 200 "user:1002"
ZINCRBY game:score:20240910 80 "user:1003"
# 获取前三名
ZREVRANGE game:score:20240910 0 2 WITHSCORES

三、高级数据结构

3.1 HyperLogLog

用于基数统计,固定占用 12KB 内存,标准误差约 0.81%。

适用场景:UV 统计、独立访客计数。

PFADD uv:20240910 "user1" "user2" "user3" "user1"
PFCOUNT uv:20240910          # 返回 3(去重)
PFMERGE uv:total uv:0901 uv:0902  # 合并

3.2 Bitmap(位图)

基于 String 的位操作,每个 bit 代表一个状态。

适用场景:签到、在线状态、日活统计。

# 用户签到(第 100 天签到)
SETBIT user:sign:1001 99 1
GETBIT user:sign:1001 99     # 检查
BITCOUNT user:sign:1001      # 统计总签到天数
BITPOS user:sign:1001 0      # 查找第一个未签到

# 日活统计
SETBIT dau:20240910 1001 1
BITCOUNT dau:20240910

3.3 Geo(地理空间)

存储经纬度坐标和计算距离。

适用场景:附近的人、门店查询。

GEOADD stores 116.397128 39.916527 "天安门"
GEOADD stores 116.418078 39.921490 "王府井"
GEODIST stores "天安门" "王府井" km
GEOPOS stores "天安门"
GEOSEARCH stores FROMLONLAT 116.397 39.916 BYRADIUS 5 km

3.4 Stream(消息流)

Redis 5.0 引入,持久化的、支持多消费者的消息队列。

适用场景:消息队列、事件溯源、日志收集。

# 生产者
XADD mystream * user_id 1001 action login
# 消费者
XRANGE mystream - +
# 消费者组
XGROUP CREATE mystream mygroup 0
XREADGROUP GROUP mygroup consumer1 COUNT 1 BLOCK 5000 STREAMS mystream >

四、持久化

4.1 RDB(快照)

将内存数据定期保存到磁盘(dump.rdb)。

优点:文件紧凑、恢复快、子进程写入不影响主进程。
缺点:可能丢失两次快照间数据、fork 耗时。

配置示例:

save 900 1        # 900秒内至少1个key变化
save 300 10       # 300秒内至少10个key变化
save 60 10000     # 60秒内至少10000个key变化

4.2 AOF(追加文件)

记录每次写入命令,以追加方式写入文件。

优点:数据更安全、文件可读、支持重写压缩。
缺点:文件体积较大、恢复速度较慢。

三种同步策略:

appendfsync always    # 每次写入都同步(最安全,最慢)
appendfsync everysec  # 每秒同步一次(推荐)
appendfsync no        # 由操作系统决定(最快,最不安全)

4.3 混合持久化(Redis 4.0+)

AOF 重写时先生成 RDB 快照,再追加增量日志。恢复时先加载 RDB 再回放 AOF。

aof-use-rdb-preamble yes

生产推荐:

save 900 1
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes

五、过期策略与内存淘汰

5.1 过期策略

Redis 使用定期删除 + 惰性删除结合:

  • 定期删除:每 100ms 随机抽取部分过期 key 删除
  • 惰性删除:访问 key 时检查是否过期,过期则删除

5.2 内存淘汰策略

当内存达到 maxmemory 限制时触发:

maxmemory 2gb
maxmemory-policy allkeys-lru
策略 说明
noeviction 不淘汰,写操作报错(默认)
allkeys-lru 所有 key 使用 LRU 淘汰(推荐)
volatile-lru 对有 TTL 的 key 使用 LRU
allkeys-random 随机淘汰
volatile-random 随机淘汰有 TTL 的 key
volatile-ttl 淘汰 TTL 最短的 key
allkeys-lfu 最近最少使用(Redis 4.0+)
volatile-lfu 对有 TTL 的 key 使用 LFU

六、高可用架构

6.1 主从复制

# 从库配置
replicaof 192.168.1.10 6379
replica-read-only yes

# 或命令行
SLAVEOF 192.168.1.10 6379

复制原理:

  1. 从库发送 SYNC 命令
  2. 主库执行 BGSAVE 生成 RDB
  3. 主库将 RDB 发送给从库
  4. 从库载入 RDB
  5. 主库持续发送写入命令到从库

6.2 Sentinel 哨兵

实现自动故障转移(failover):

# sentinel.conf
sentinel monitor mymaster 192.168.1.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000

# 启动
redis-sentinel sentinel.conf

作用:监控主从状态、自动故障转移、通知客户端新主库地址。

6.3 Redis Cluster 集群

Redis 3.0+ 原生分布式方案:

# 创建集群(3 主 3 从)
redis-cli --cluster create \
  192.168.1.10:6371 192.168.1.10:6372 192.168.1.10:6373 \
  192.168.1.10:6374 192.168.1.10:6375 192.168.1.10:6376 \
  --cluster-replicas 1

核心概念:

  • 16384 个哈希槽:每个 key 通过 CRC16 映射到槽
  • Gossip 协议:节点间通信
  • 客户端直连:直接连接数据所在节点
  • 自动分片:增删节点自动迁移槽

七、缓存设计最佳实践

7.1 缓存穿透

问题:查询不存在的数据,请求直达数据库。

解决方案:

  1. 缓存空值:SET user:9999 "$" EX 60
  2. 布隆过滤器:预先将存在的 key 加入过滤器
  3. 参数校验:在业务层校验非法参数

7.2 缓存击穿

问题:热点 key 过期,大量请求同时穿透到数据库。

解决方案:

  1. 互斥锁:SETNX lock:hotkey 1,获取锁的线程才查数据库
  2. 逻辑过期:不设 TTL,在 value 中存储过期时间,异步刷新
  3. 提前预热:在热点 key 过期前主动刷新

7.3 缓存雪崩

问题:大量 key 同时过期,或 Redis 宕机。

解决方案:

  1. 过期时间加随机值:SET key value EX 3600 + random(0,300)
  2. 多级缓存:本地缓存(Caffeine)+ Redis
  3. 高可用:Sentinel 或 Cluster
  4. 限流降级:数据库请求限流,返回旧缓存数据

7.4 缓存与数据库一致性

推荐方案:先更新数据库,再删除缓存

更新数据库 → 删除 Redis 缓存 → 下次读取时自动回填

如担心删除失败,可使用延迟双删:

  1. 删除缓存
  2. 更新数据库
  3. 延迟几百毫秒再次删除缓存

八、生产运维

8.1 关键配置

maxmemory 4gb
maxmemory-policy allkeys-lru
timeout 300
tcp-keepalive 300
tcp-backlog 511

8.2 Big Key 治理

什么是 Big Key:

  • String > 10KB
  • 集合类型元素 > 5000 个
  • 总内存 > 10MB

查找:redis-cli --bigkeys 或 MEMORY USAGE key
解决:拆分(按时间/类型分 key)、压缩、清理

8.3 慢查询日志

CONFIG SET slowlog-log-slower-than 10000   # 单位微秒
CONFIG SET slowlog-max-len 128
SLOWLOG GET 10
SLOWLOG RESET

8.4 监控命令

redis-cli --stat                 # 实时统计
redis-cli MONITOR                # 实时命令监控
redis-cli --latency              # 延迟检查
INFO memory                      # 内存分析
MEMORY DOCTOR                    # 内存诊断

九、实战场景

9.1 分布式锁

# 加锁(原子操作)
SET lock:order:1001 "thread-1" NX EX 10

# 解锁(Lua 脚本保证原子性)
-- 判断锁持有者,防止误删
local key = KEYS[1]
local expected = ARGV[1]
local current = redis.call("GET", key)
if current == expected then
    return redis.call("DEL", key)
end
return 0

9.2 接口限流

简单计数器:

INCR rate:api:user:1001
EXPIRE rate:api:user:1001 60
# 检查值是否超过阈值

令牌桶限流(Lua):

local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local last = redis.call("HGET", key, "last") or now
local tokens = redis.call("HGET", key, "tokens") or capacity
local elapsed = math.max(0, now - last)
tokens = math.min(capacity, tokens + elapsed * rate)
if tokens >= 1 then
    tokens = tokens - 1
    redis.call("HMSET", key, "last", now, "tokens", tokens)
    return 1
else
    return 0
end

9.3 延时队列

利用 ZSet 按时间排序的特性:

# 生产者:任务延迟 1 小时
ZADD delay:queue (timestamp + 3600) "task-1"

# 消费者:轮询到期任务
local tasks = redis.call("ZRANGEBYSCORE", "delay:queue", 0, now)
for _, task in ipairs(tasks) do
    local removed = redis.call("ZREM", "delay:queue", task)
    if removed > 0 then process(task) end
end

9.4 全局 ID 生成器

local date = ARGV[1]
local key = "id:gen:" .. date
local id = redis.call("INCR", key)
redis.call("EXPIRE", key, 86400)
return date .. "-" .. string.format("%04d", id)

十、面试高频问题

Q1:Redis 为什么这么快?

  1. 纯内存操作:避开磁盘 IO
  2. 单线程模型:无上下文切换和锁竞争
  3. IO 多路复用:基于 epoll 的事件驱动
  4. 高效数据结构:SDS、跳表、压缩列表

注意:Redis 6.0+ 引入了多线程 IO,但命令执行仍然是单线程。

Q2:RDB 和 AOF 如何选择?

维度 RDB AOF
数据安全性 可能丢失最后一次快照后数据 最多丢 1 秒(everysec)
恢复速度 快 慢
文件大小 小 大
可读性 二进制 文本

推荐:两者同时开启(混合持久化)。

Q3:如何保证缓存和数据库一致?

先更新数据库,再删除缓存。如果担心删除失败,用延迟双删:

  1. 删除缓存 → 2. 更新数据库 → 3. 延迟 500ms 再次删除缓存

Q4:跳跃表是如何实现的?

ZSet 使用跳跃表(Skip List)实现 O(log N) 查找。每层是下一层的索引,从顶层逐层下降定位目标,相当于链表的二分查找。

Level 3:  1 ------------------------> 9
Level 2:  1 -----> 5 -----> 7 -----> 9
Level 1:  1 -> 3 -> 5 -> 6 -> 7 -> 8 -> 9

Q5:Redis 事务和关系型数据库事务有何区别?

  • 不支持回滚:语法错误全部不执行,运行时错误不回滚已执行命令
  • 无隔离级别:单线程自然隔离
  • 原子性有限:EXEC 阶段出错不影响其他命令
  • 支持 WATCH 实现乐观锁(CAS)

十一、总结

特性 推荐配置
持久化 RDB + AOF 混合持久化
内存淘汰 allkeys-lru
高可用 Sentinel 或 Cluster
部署 Docker + Docker Compose
容量规划 预留 2 倍内存(写时复制)
监控 Prometheus + Redis Exporter + Grafana
客户端 redis-py、ioredis、Jedis

Redis 是目前最流行的内存数据库,掌握其数据结构、持久化机制、高可用部署和缓存设计模式,是每个后端工程师的必修课。文中所有命令均可在 redis-cli 中直接运行,建议边学边练。