Redis基础

Redis基础

Redis是一款以内存为主要工作介质的高性能键值数据库。它常被用作缓存、计数器、分布式锁、排行榜、会话存储、轻量消息通道、地理位置索引等。

这篇笔记面向后端开发实践,不只罗列命令,而是围绕三个问题展开:

  • Redis怎么跑起来,并完成最基本的K-V读写?
  • 不同数据结构适合建模什么业务,背后的复杂度有什么差异?
  • 在微服务、云原生、物联网、低空轨迹等场景里,Redis哪些能力能扩展系统边界,哪些地方必须谨慎?

版本说明:本文以Redis官方文档为主要参考,基础命令在Redis 6/7/8中大多通用;GEOSEARCH、ACL、脚本缓存等细节和版本有关,生产环境应以实际部署版本为准。

1. 整体认识

1.1 Redis解决什么问题

Redis的核心特点可以用一句话概括:把常用数据结构放进内存,并提供原子命令操作

它不像MySQL/PostgreSQL那样围绕表、行、SQL和事务查询构建,而是围绕key -> value构建。这里的value不是只能是字符串,也可以是Hash、List、Set、Sorted Set、Bitmap、HyperLogLog、GEO等结构。

1
2
3
4
5
6
7
8
9
10
Redis
└── key
├── String -> 计数器、缓存值、分布式锁
├── Hash -> 对象字段
├── List -> 队列、最新消息流
├── Set -> 去重集合、标签、共同好友
├── Sorted Set -> 排行榜、延时队列
├── Bitmap -> 签到、活跃状态
├── HyperLogLog -> UV等基数估算
└── GEO -> 附近的人、设备位置距离
flowchart LR
    A["业务请求"] --> B["Redis命令"]
    B --> C{"数据结构"}
    C --> D["String: cache / lock / counter"]
    C --> E["Hash: object fields"]
    C --> F["List: queue"]
    C --> G["Set: relation / tags"]
    C --> H["ZSet: rank / schedule"]
    C --> I["GEO: location index"]

1.2 Redis不是万能数据库

Redis很快,但“快”不是免费午餐。它适合做高频、轻量、结构相对清晰的数据操作,不适合替代所有数据库能力。

适合Redis 不适合只依赖Redis
热点缓存 复杂多表查询
原子计数 强一致长事务
分布式锁辅助 需要完整审计的业务事实
排行榜、集合关系 大规模离线分析
地理位置粗筛 高精度GIS几何计算
Pub/Sub轻量通知 需要可靠投递的消息系统

工程上常见的组合是:

1
2
3
4
5
业务事实       -> MySQL / PostgreSQL
热点读写 -> Redis
可靠消息 -> Kafka / RabbitMQ / RocketMQ
地理精算 -> PostGIS
实时轻量距离筛选 -> Redis GEO

2. 环境搭建与基石建立

这个阶段的目标是让Redis跑起来,并掌握最基础的K-V交互模型。

2.1 Linux源码编译安装

在高性能Linux服务器上,比如多核EPYC环境,源码编译安装能让你更清楚Redis的运行文件、配置文件和启动方式。

常见流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 安装编译依赖,不同发行版命令略有差异
sudo apt update
sudo apt install -y build-essential tcl pkg-config

# 下载源码
wget https://download.redis.io/redis-stable.tar.gz
tar -xzvf redis-stable.tar.gz
cd redis-stable

# 编译
make -j "$(nproc)"

# 可选:运行测试,时间会比较长
make test

# 安装到系统路径,通常会安装 redis-server / redis-cli 等命令
sudo make install

启动一个前台Redis:

1
redis-server

指定配置文件启动:

1
redis-server /path/to/redis.conf

客户端连接:

1
2
redis-cli -h 127.0.0.1 -p 6379
PING

如果返回:

1
PONG

说明Redis服务已正常响应。

多核服务器上的一个关键认知

Redis的命令执行模型长期以单线程事件循环为核心。Redis 6以后支持I/O线程来改善网络读写开销,但这不等于所有命令会在多个CPU核心上并行执行。

所以在EPYC这类多核机器上,常见优化思路不是简单把一个Redis实例“开满所有核心”,而是:

  • 使用更快的CPU核心和更好的内存;
  • 对大Key、慢命令、阻塞操作做治理;
  • 通过主从、集群、分片、多实例提高整体吞吐;
  • 对网络I/O压力较大的场景再考虑I/O线程配置;
  • 把复杂计算放到业务层或专门计算服务,不要塞进Redis长时间执行。

2.2 Docker快速部署

开发环境里更推荐用Docker快速拉起Redis。

1
2
3
docker run --name redis-dev \
-p 6379:6379 \
-d redis:latest

连接:

1
docker exec -it redis-dev redis-cli

如果需要密码,可以这样启动:

1
2
3
4
docker run --name redis-dev \
-p 6379:6379 \
-d redis:latest \
redis-server --requirepass "<password>"

连接时:

1
redis-cli -h 127.0.0.1 -p 6379 -a "<password>"

注意:不要把真实密码写进笔记、日志、Git仓库或命令历史里。生产环境更推荐通过环境变量、密钥管理系统或编排平台Secret注入。

2.3 Docker Compose部署

一个最小可用的docker-compose.yml

1
2
3
4
5
6
7
8
9
10
services:
redis:
image: redis:latest
container_name: redis-dev
ports:
- "6379:6379"
volumes:
- ./redis-data:/data
- ./redis.conf:/usr/local/etc/redis/redis.conf
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]

启动:

1
2
3
docker compose up -d
docker compose ps
docker compose logs redis

停止:

1
docker compose down

如果只是本机开发,也可以不挂配置文件,直接使用镜像默认配置。生产环境则应该显式管理配置、密码、持久化、内存上限和网络暴露范围。

2.4 redis.conf核心配置项

Redis配置项很多,初学阶段先掌握下面这些。

配置项 示例 含义 实践建议
bind bind 127.0.0.1 绑定监听地址 只给本机用就绑定127.0.0.1;暴露到内网要配合防火墙和认证
port port 6379 监听端口 默认6379;生产环境不应只靠改端口做安全
daemonize daemonize no 是否后台运行 容器里通常用no;传统Linux服务可用yes或交给systemd
requirepass requirepass <password> 设置默认用户密码 Redis 6+还有ACL;不要使用弱密码
maxmemory maxmemory 4gb 最大内存限制 生产必须设置,避免Redis吃满机器内存
maxmemory-policy allkeys-lru 达到内存上限后的淘汰策略 缓存场景常用LRU/LFU;业务数据场景要谨慎
appendonly appendonly yes AOF持久化 需要更强恢复能力时开启
save save 900 1 RDB快照策略 适合定期快照,不等于每次写入都落盘

最小开发配置示例:

1
2
3
4
5
6
7
bind 127.0.0.1
port 6379
daemonize no
requirepass <password>
maxmemory 512mb
maxmemory-policy allkeys-lru
appendonly yes

几个关键提醒:

  • bind 0.0.0.0会让Redis监听所有网卡,必须确认网络隔离、密码和防火墙。
  • requirepass不是万能安全方案,Redis不应该直接暴露到公网。
  • maxmemory不设置时,Redis可能持续占用内存直到系统发生压力。
  • 容器中daemonize通常应保持no,否则主进程退出会导致容器异常。

3. 基础数据类型与命令:String和Hash

3.1 String

String是Redis最基础的数据类型。它可以存普通字符串、JSON文本、数字、二进制内容等。

常用命令:

命令 作用 常见复杂度
SET key value 设置值 O(1)
GET key 获取值 O(1)
INCR key 整数自增 O(1)
SETNX key value key不存在时设置 O(1)
MGET key ... 批量获取 O(N)
MSET key value ... 批量设置 O(N)

基础示例:

1
2
3
4
5
6
7
SET user:1:name "Tom"
GET user:1:name

SET page:view 0
INCR page:view
INCR page:view
GET page:view

3.2 String做计数器

INCR是原子命令,很适合做计数器。

1
2
INCR article:1001:views
INCR api:/orders:qps

限流场景可以配合过期时间:

1
2
INCR rate:user:1001:2026-06-03T10:00
EXPIRE rate:user:1001:2026-06-03T10:00 60

但这段命令有一个隐患:INCREXPIRE是两条命令,如果中间客户端崩溃,可能留下没有过期时间的key。更稳妥的做法是用Lua脚本把“自增 + 设置过期时间”合成一个原子操作。

3.3 String做分布式锁

很多教程会先讲:

1
2
SETNX lock:order:1001 request-id
EXPIRE lock:order:1001 30

但这不是最佳实践,因为SETNXEXPIRE分开执行存在竞态条件。

更推荐使用SET的扩展参数:

1
SET lock:order:1001 request-id NX PX 30000

含义:

  • NX:只有key不存在时才设置;
  • PX 30000:设置毫秒级过期时间,避免死锁;
  • request-id:锁的持有者标识,释放锁时要校验。

释放锁不能简单执行:

1
DEL lock:order:1001

因为可能误删别人后来加上的锁。正确思路是用Lua脚本保证“比较持有者 + 删除”原子完成:

1
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:order:1001 request-id

分布式锁要记住两点:

  • Redis锁适合保护短时间、低延迟的临界区;
  • 真正强一致的业务约束,最终还要落到数据库唯一索引、状态机、幂等表等机制上。

3.4 Hash

Hash适合存储对象结构。它的结构类似:

1
2
3
4
user:1001
name -> Tom
age -> 20
city -> Hangzhou

常用命令:

命令 作用 常见复杂度
HSET key field value 设置字段 O(1)
HGET key field 获取字段 O(1)
HMGET key field ... 批量获取字段 O(N)
HGETALL key 获取全部字段和值 O(N)
HDEL key field ... 删除字段 O(N)
HINCRBY key field increment 字段整数自增 O(1)

示例:

1
2
3
4
HSET user:1001 name "Tom" age 20 city "Hangzhou"
HGET user:1001 name
HGETALL user:1001
HINCRBY user:1001 login_count 1

3.5 Hash与String存对象的对比

对象可以用String存JSON,也可以用Hash拆字段。

方案 示例 优点 缺点
String存JSON SET user:1001 '{"name":"Tom","age":20}' 简单,适合整体读写;跨语言容易处理 更新单个字段要读出、反序列化、再写回
Hash存字段 HSET user:1001 name Tom age 20 可单字段读写;字段级自增方便 结构复杂时不如JSON直观;字段多时HGETALL成本会上升

实践建议:

  • 配置、会话、简单缓存对象:String JSON更直接;
  • 需要频繁更新单个字段的对象:Hash更合适;
  • 需要复杂查询的对象:不要指望Redis代替关系型数据库查询能力;
  • 大对象不适合无脑塞进Redis,序列化成本和网络传输成本都要算。

4. 通用键值操作

这些命令不关心value是什么类型,是日常排查和生命周期管理的基础。

命令 作用 示例
DEL key [key ...] 删除key DEL user:1001
EXISTS key [key ...] 判断key是否存在 EXISTS user:1001
EXPIRE key seconds 设置秒级过期时间 EXPIRE token:abc 3600
PEXPIRE key milliseconds 设置毫秒级过期时间 PEXPIRE lock:1 30000
TTL key 查看剩余秒数 TTL token:abc
PTTL key 查看剩余毫秒数 PTTL lock:1
TYPE key 查看value类型 TYPE user:1001
RENAME key newkey 重命名key RENAME a b

TTL返回值要会看:

1
2
3
4
5
6
7
8
> TTL token:abc
3600 # 还有3600秒过期

> TTL token:no-expire
-1 # key存在,但没有过期时间

> TTL token:not-exists
-2 # key不存在

生命周期管理的核心经验:

  • 缓存key通常必须有过期时间;
  • 锁key必须有过期时间;
  • 长期业务数据如果放Redis,要明确持久化和恢复策略;
  • 不要让大量key在同一秒同时过期,容易造成瞬时压力,可以给TTL加随机抖动。

5. 进阶数据结构与业务建模

Redis的强大不在于“内存快”这一个点,而在于它把多种数据结构做成了原子命令。建模时要先问:这个业务需要的是顺序、去重、排序、计数、集合运算,还是地理距离?

5.1 List

List是双端列表,可以从左侧或右侧推入、弹出元素。

常用命令:

命令 作用 常见复杂度
LPUSH key value ... 从左侧插入 O(N),N为插入元素个数
RPUSH key value ... 从右侧插入 O(N)
LPOP key 从左侧弹出 O(1)
RPOP key 从右侧弹出 O(1)
BLPOP key timeout 阻塞式左弹出 O(N),N为检查的key数量
LRANGE key start stop 范围读取 O(S+N)
LLEN key 列表长度 O(1)

消息队列

生产者从右侧写入,消费者从左侧弹出:

1
2
RPUSH queue:email '{"to":"user","template":"welcome"}'
BLPOP queue:email 5

List可以做轻量队列,但要注意:

  • 没有Kafka那样的消费位点、重平衡、消息持久化语义;
  • 消费者取出后如果崩溃,消息可能丢失;
  • 对可靠队列要求高时,应使用专业MQ,或考虑Redis Streams而不是List。

最新动态展示

1
2
3
4
LPUSH feed:user:1001 post:9001
LPUSH feed:user:1001 post:9002
LTRIM feed:user:1001 0 99
LRANGE feed:user:1001 0 19

LTRIM用于只保留最新100条,避免列表无限增长。

5.2 Set

Set是无序去重集合,适合表达“某个对象属于某个集合”。

常用命令:

命令 作用 常见复杂度
SADD key member ... 添加成员 O(N)
SREM key member ... 删除成员 O(N)
SISMEMBER key member 判断成员是否存在 O(1)
SINTER key ... 求交集 与集合大小相关
SUNION key ... 求并集 与集合大小相关
SCARD key 获取成员数 O(1)
SPOP key 随机弹出 O(1)
SRANDMEMBER key count 随机取成员 与count相关

用户标签

1
2
3
SADD tag:backend user:1 user:2 user:3
SADD tag:redis user:2 user:3 user:4
SINTER tag:backend tag:redis

SINTER可以找同时具有多个标签的用户。

社交关系:共同好友

1
2
3
SADD user:1:follows user:2 user:3 user:4
SADD user:9:follows user:3 user:4 user:8
SINTER user:1:follows user:9:follows

交集结果就是共同关注或共同好友。

抽奖系统

1
2
SADD lottery:20260603 user:1 user:2 user:3
SPOP lottery:20260603 1

SPOP会把抽中的人移出集合,天然避免重复中奖。

5.3 Sorted Set

Sorted Set也叫ZSet。它是“成员 + 分数”的有序集合,成员唯一,按照score排序。

常用命令:

命令 作用 常见复杂度
ZADD key score member 添加或更新成员分数 O(log N)
ZRANGE key start stop 按分数从小到大取范围 O(log N + M)
ZREVRANGE key start stop 按分数从大到小取范围 O(log N + M)
ZINCRBY key increment member 增加成员分数 O(log N)
ZRANK key member 获取正序排名 O(log N)
ZREVRANK key member 获取倒序排名 O(log N)
ZREM key member ... 删除成员 O(M log N)

实时排行榜

1
2
3
4
ZINCRBY rank:game:daily 10 user:1001
ZINCRBY rank:game:daily 30 user:1002
ZREVRANGE rank:game:daily 0 9 WITHSCORES
ZREVRANK rank:game:daily user:1001

排行榜要注意两个设计点:

  • 排名范围要有边界,比如日榜、周榜、赛季榜;
  • 分数相同时的排序规则要提前定义,必要时把时间因素编码进score或另存字段。

延时队列

把“执行时间戳”作为score:

1
2
ZADD delay:jobs 1780473600000 job:1001
ZRANGE delay:jobs 0 0 WITHSCORES

消费者不断取score小于当前时间的任务:

1
ZRANGEBYSCORE delay:jobs 0 1780473600000 LIMIT 0 1

取到后要用Lua脚本或WATCH事务保证“读取 + 删除”不会被多个消费者重复执行。

跳跃表是怎么帮ZSet快速查询的

Redis的ZSet在较大规模时会使用跳跃表等结构来维护有序性。跳跃表可以理解为“多层链表”:

1
2
3
level 3:  1 ----------------------> 9
level 2: 1 ---------> 5 ---------> 9
level 1: 1 ---> 3 ---> 5 ---> 7 ---> 9

查找时不是从头一个个扫,而是从高层开始跳跃,发现跳过头了再下降一层。它用概率换结构简单度,平均查询、插入、删除都能接近O(log N)

ZSet适合排行榜、时间调度、权重排序,但不适合做复杂多字段排序查询。如果需要“按城市、类型、状态、时间、价格组合筛选再排序”,应该优先考虑数据库索引或搜索引擎。

5.4 Bitmaps

Bitmap不是单独的数据类型,本质上是在String上按位操作。它特别适合表达大量用户的布尔状态。

常用命令:

1
2
3
SETBIT active:2026-06-03 1001 1
GETBIT active:2026-06-03 1001
BITCOUNT active:2026-06-03

签到场景:

1
2
3
4
5
# 用户1001在6月第3天签到,offset可以用day-1
SETBIT sign:user:1001:2026-06 2 1

# 统计本月签到天数
BITCOUNT sign:user:1001:2026-06

千万级用户活跃统计:

1
2
SETBIT active:2026-06-03 10000001 1
BITCOUNT active:2026-06-03

优点是极度节省内存。缺点是offset必须设计清楚,用户ID如果非常稀疏,会造成空间浪费。比如直接拿很大的数据库ID当offset,可能把Bitmap撑得很大。

5.5 HyperLogLog

HyperLogLog用于基数统计,也就是估算“去重后的数量”。典型场景是网站UV、接口独立访问用户数、设备活跃数。

常用命令:

1
2
3
4
5
PFADD uv:2026-06-03 user:1 user:2 user:3
PFCOUNT uv:2026-06-03

PFMERGE uv:2026-06 uv:2026-06-01 uv:2026-06-02 uv:2026-06-03
PFCOUNT uv:2026-06

它的核心取舍:

  • 优点:非常省内存;
  • 缺点:结果是估算值,允许少量误差;
  • 适合:大规模UV、独立设备数等统计;
  • 不适合:抽奖去重、权限判断、精确计费等必须精确的场景。

5.6 KEYS与SCAN

KEYS pattern可以按模式查找key:

1
KEYS user:*

但生产环境要极其谨慎,尤其不能随手执行:

1
KEYS *

原因是KEYS会遍历整个keyspace,在key很多时可能阻塞Redis主线程,导致线上请求抖动甚至超时。

生产环境更推荐SCAN

1
SCAN 0 MATCH user:* COUNT 100

SCAN是游标式迭代:

1
2
3
4
第一次: SCAN 0 MATCH user:* COUNT 100      -> 返回 next_cursor
第二次: SCAN next_cursor MATCH user:* COUNT 100
...
直到: next_cursor = 0,说明本轮迭代结束

使用SCAN也要注意:

  • 它不保证每次返回数量严格等于COUNT
  • 一轮迭代过程中,key如果被新增或删除,结果可能有重复或遗漏;
  • 客户端要能处理重复key;
  • 它适合渐进式巡检、清理、统计,不适合强一致快照。

6. 高级特性与功能扩展

6.1 Pub/Sub

Pub/Sub是发布订阅模型:发布者把消息发到频道,订阅者监听频道。

常用命令:

1
2
3
4
SUBSCRIBE device:status
PSUBSCRIBE device:*

PUBLISH device:status '{"id":"uav-001","state":"online"}'
sequenceDiagram
    participant P as Publisher
    participant R as Redis
    participant S1 as Subscriber A
    participant S2 as Subscriber B

    S1->>R: SUBSCRIBE device:status
    S2->>R: SUBSCRIBE device:status
    P->>R: PUBLISH device:status message
    R-->>S1: message
    R-->>S2: message

特性与局限

Pub/Sub的核心特点是:即发即弃

能力 Pub/Sub表现
在线订阅者实时收到消息 支持
离线后补收历史消息 不支持
消息持久化 不支持
消费确认ACK 不支持
消费失败重试 不支持
多消费者组 不支持

所以它适合:

  • 本地服务间轻量通知;
  • 配置刷新广播;
  • WebSocket网关间广播;
  • 设备在线状态变更提示;
  • MQTT业务里做内部非关键事件通知。

不适合:

  • 订单创建;
  • 支付结果;
  • 任务调度;
  • 设备指令可靠下发;
  • 必须审计和追踪的消息链路。

这些场景应该使用Kafka、RabbitMQ、RocketMQ,或根据需求考虑Redis Streams。

6.2 Redis GEO

Redis GEO用于存储地理坐标并做距离计算和附近搜索。它适合轻量位置索引,比如附近的人、附近车辆、无人机或设备的粗筛。

常用命令:

命令 作用
GEOADD key longitude latitude member 添加位置
GEODIST key member1 member2 unit 计算两个成员距离
GEOSEARCH key ... 按半径或矩形范围搜索
GEORADIUS key ... 旧的半径搜索命令,Redis 6.2起建议用GEOSEARCH替代

示例:

1
2
3
4
5
6
GEOADD vehicles 120.1551 30.2741 car:1001
GEOADD vehicles 120.1600 30.2800 car:1002

GEODIST vehicles car:1001 car:1002 km

GEOSEARCH vehicles FROMMEMBER car:1001 BYRADIUS 5 km WITHDIST ASC COUNT 20

在无人机/车辆轨迹场景中,Redis GEO可以做“附近目标粗筛”:

1
2
3
4
5
设备上报位置
-> Redis GEO更新当前位置
-> 按半径查附近设备/站点/任务
-> 业务层进一步判断高度、时间窗、权限、禁飞区
-> 精确空间计算交给PostGIS或专业GIS服务

注意:

  • Redis GEO主要处理经纬度位置和距离,不处理复杂多边形拓扑;
  • 低空业务里还需要高度、时间窗、空域规则,不能只靠二维距离;
  • 如果要保存完整轨迹,Redis不应作为唯一事实库,通常还要落PostgreSQL/PostGIS、时序库或对象存储。

6.3 事务

Redis事务相关命令:

命令 作用
MULTI 开启事务,后续命令进入队列
EXEC 执行队列中的命令
DISCARD 放弃事务队列
WATCH key ... 监听key,实现乐观锁
UNWATCH 取消监听

基础示例:

1
2
3
4
MULTI
INCR account:1:balance
INCR account:2:balance
EXEC

MULTI之后,命令只是进入队列:

1
2
3
4
5
6
7
8
9
> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

WATCH用于乐观锁。假设要扣减库存:

1
2
3
4
5
WATCH stock:item:1001
GET stock:item:1001
MULTI
DECR stock:item:1001
EXEC

如果WATCH后的key在EXEC前被其他客户端修改,EXEC会失败,客户端需要重新读取、判断、重试。

Redis事务为什么不支持传统数据库回滚

Redis事务和关系型数据库事务不是一回事。

关系型数据库事务强调ACID,支持复杂回滚。Redis事务更像是“把一组命令排队,然后一次性按顺序执行”,它保证执行期间不会被其他客户端命令插入,但不提供传统意义上的回滚。

例如:

1
2
3
4
5
SET foo 1
MULTI
INCR foo
LPUSH foo bar
EXEC

如果LPUSH foo bar因为foo是String而失败,前面的INCR foo可能已经执行成功。Redis不会自动把INCR回滚掉。

所以Redis事务适合:

  • 少量命令的原子顺序执行;
  • 配合WATCH做乐观锁;
  • 实现轻量并发控制。

不适合:

  • 替代数据库事务;
  • 承载复杂业务状态回滚;
  • 在失败后自动恢复业务一致性。

6.4 Lua脚本

Lua脚本是Redis里非常重要的原子性工具。它通过EVAL执行:

1
EVAL script numkeys key [key ...] arg [arg ...]

简单示例:

1
EVAL "return redis.call('GET', KEYS[1])" 1 user:1001:name

Lua脚本的价值不是“能写复杂代码”,而是把多次Redis操作合并成一次服务器端原子执行,减少并发竞态。

限流脚本示例

目标:一个用户60秒内最多访问100次。

1
2
3
4
5
6
7
EVAL "
local current = redis.call('INCR', KEYS[1])
if current == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
return current
" 1 rate:user:1001 60

业务层拿返回值判断:

1
2
current <= 100  -> 放行
current > 100 -> 拒绝或降级

延时队列抢占脚本示例

目标:多个消费者并发时,同一个到期任务只能被一个消费者拿走。

1
2
3
4
5
6
7
8
9
10
11
EVAL "
local jobs = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1], 'LIMIT', 0, 1)
if #jobs == 0 then
return nil
end
local removed = redis.call('ZREM', KEYS[1], jobs[1])
if removed == 1 then
return jobs[1]
end
return nil
" 1 delay:jobs 1780473600000

Lua脚本实践规则

  • 脚本要短,避免长时间阻塞Redis;
  • 所有访问的key应通过KEYS传入,不要在脚本里拼接动态key;
  • 参数值通过ARGV传入,不要每次拼出不同脚本文本;
  • Redis Cluster中,多key脚本通常要求key落在同一个hash slot;
  • 高频脚本可考虑SCRIPT LOAD + EVALSHA,减少脚本传输;
  • Redis 7.4开始,EVAL加载的脚本缓存会有LRU淘汰机制,但仍不应滥用动态脚本。

7. 常见业务建模速查

场景 推荐结构 关键命令 注意事项
缓存对象 String / Hash SETGETHSETHGET TTL、缓存穿透、击穿、雪崩
分布式锁 String SET NX PX、Lua释放 锁值校验、过期时间、幂等兜底
接口限流 String INCREXPIRE、Lua 避免INCR后无TTL
最新动态 List LPUSHLTRIMLRANGE 控制长度
轻量队列 List RPUSHBLPOP 不适合强可靠消息
用户标签 Set SADDSINTER 大集合运算成本
共同好友 Set SINTER 结果集可能很大
抽奖 Set SPOPSRANDMEMBER 是否允许重复中奖
排行榜 ZSet ZINCRBYZREVRANGE score设计、榜单周期
延时任务 ZSet ZADDZRANGEBYSCORE、Lua 抢占原子性、失败重试
签到统计 Bitmap SETBITGETBITBITCOUNT offset设计
UV统计 HyperLogLog PFADDPFCOUNT 估算值,不适合精确计费
附近设备 GEO GEOADDGEODISTGEOSEARCH 只做粗筛,复杂GIS另算
服务广播 Pub/Sub PUBLISHSUBSCRIBE 无持久化、无ACK

8. 生产实践清单

8.1 Key命名

推荐用冒号表达层级:

1
2
3
4
5
user:1001
user:1001:profile
order:20260603:1001
rank:game:daily:2026-06-03
lock:order:1001

命名原则:

  • 能从key看出业务含义;
  • 避免过长key,key本身也占内存;
  • 用统一前缀方便SCAN MATCH巡检;
  • 多租户、多环境要有隔离前缀;
  • Redis Cluster中需要多key同槽时,可用hash tag,例如order:{1001}:lockorder:{1001}:items

8.2 大Key与热Key

大Key是value过大或集合成员过多的key。它会带来:

  • 网络传输慢;
  • 删除阻塞;
  • 持久化压力;
  • 主从同步压力;
  • 集群迁移压力。

热Key是访问量极高的key。它会带来:

  • 单节点CPU或网络瓶颈;
  • 缓存击穿;
  • 集群分片不均。

治理思路:

  • 大对象拆分;
  • 限制List/Set/ZSet长度;
  • UNLINK异步删除大Key;
  • 热点缓存加本地缓存;
  • 对高频计数做分片计数再汇总;
  • 用监控发现慢命令、热Key和内存异常。

8.3 慢命令意识

Redis很快,但不是所有命令都快。以下命令在大数据量下要谨慎:

1
2
3
4
5
6
KEYS *
HGETALL big_hash
SMEMBERS big_set
LRANGE big_list 0 -1
ZRANGE big_zset 0 -1
SINTER huge_set_a huge_set_b

不是说这些命令永远不能用,而是要问:

  • 这个key可能有多大?
  • 这个命令会不会扫全量?
  • 有没有分页、游标或限制数量?
  • 有没有在生产高峰期执行?
  • 返回结果会不会把客户端或网络打满?

8.4 缓存常见问题

问题 现象 常见处理
缓存穿透 查询不存在的数据,每次都打到DB 空值缓存、布隆过滤器、参数校验
缓存击穿 热点key过期瞬间大量请求打到DB 互斥锁、逻辑过期、热点预热
缓存雪崩 大量key同时过期或Redis故障 TTL随机抖动、多级缓存、限流降级
缓存不一致 DB和缓存短时间不一致 先更新DB再删缓存、消息补偿、延迟双删视场景使用

缓存一致性没有银弹。大多数业务要接受“短时间最终一致”,并通过幂等、重试、补偿和过期机制兜底。

9. 学习路线建议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
第一阶段:跑起来
-> 源码 / Docker / Compose
-> redis-cli
-> redis.conf
-> String / Hash / TTL

第二阶段:会建模
-> List / Set / ZSet
-> Bitmap / HyperLogLog
-> KEYS vs SCAN
-> 复杂度意识

第三阶段:会落地
-> 分布式锁
-> 限流
-> 排行榜
-> 延时队列
-> Pub/Sub
-> GEO
-> Lua原子脚本

第四阶段:进生产
-> 持久化
-> 主从复制
-> Sentinel
-> Cluster
-> 内存淘汰
-> 慢命令和大Key治理

这篇是Redis基础。后续进阶应重点学习:

  • RDB和AOF持久化;
  • 主从复制、哨兵和集群;
  • 内存编码和对象模型;
  • 过期删除和内存淘汰;
  • 缓存一致性;
  • Redis Cluster槽位和多key限制;
  • Redis Streams和可靠消息模型。

10. 参考资料


Redis基础
https://www.albeltbo.com/posts/fe29e710.html
作者
AlbeltBo
发布于
2026年7月14日
许可协议