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 | |
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. 环境搭建与基石建立
这个阶段的目标是让Redis跑起来,并掌握最基础的K-V交互模型。
2.1 Linux源码编译安装
在高性能Linux服务器上,比如多核EPYC环境,源码编译安装能让你更清楚Redis的运行文件、配置文件和启动方式。
常见流程如下:
1 | |
启动一个前台Redis:
1 | |
指定配置文件启动:
1 | |
客户端连接:
1 | |
如果返回:
1 | |
说明Redis服务已正常响应。
多核服务器上的一个关键认知
Redis的命令执行模型长期以单线程事件循环为核心。Redis 6以后支持I/O线程来改善网络读写开销,但这不等于所有命令会在多个CPU核心上并行执行。
所以在EPYC这类多核机器上,常见优化思路不是简单把一个Redis实例“开满所有核心”,而是:
- 使用更快的CPU核心和更好的内存;
- 对大Key、慢命令、阻塞操作做治理;
- 通过主从、集群、分片、多实例提高整体吞吐;
- 对网络I/O压力较大的场景再考虑I/O线程配置;
- 把复杂计算放到业务层或专门计算服务,不要塞进Redis长时间执行。
2.2 Docker快速部署
开发环境里更推荐用Docker快速拉起Redis。
1 | |
连接:
1 | |
如果需要密码,可以这样启动:
1 | |
连接时:
1 | |
注意:不要把真实密码写进笔记、日志、Git仓库或命令历史里。生产环境更推荐通过环境变量、密钥管理系统或编排平台Secret注入。
2.3 Docker Compose部署
一个最小可用的docker-compose.yml:
1 | |
启动:
1 | |
停止:
1 | |
如果只是本机开发,也可以不挂配置文件,直接使用镜像默认配置。生产环境则应该显式管理配置、密码、持久化、内存上限和网络暴露范围。
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 | |
几个关键提醒:
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 | |
3.2 String做计数器
INCR是原子命令,很适合做计数器。
1 | |
限流场景可以配合过期时间:
1 | |
但这段命令有一个隐患:INCR和EXPIRE是两条命令,如果中间客户端崩溃,可能留下没有过期时间的key。更稳妥的做法是用Lua脚本把“自增 + 设置过期时间”合成一个原子操作。
3.3 String做分布式锁
很多教程会先讲:
1 | |
但这不是最佳实践,因为SETNX和EXPIRE分开执行存在竞态条件。
更推荐使用SET的扩展参数:
1 | |
含义:
NX:只有key不存在时才设置;PX 30000:设置毫秒级过期时间,避免死锁;request-id:锁的持有者标识,释放锁时要校验。
释放锁不能简单执行:
1 | |
因为可能误删别人后来加上的锁。正确思路是用Lua脚本保证“比较持有者 + 删除”原子完成:
1 | |
分布式锁要记住两点:
- Redis锁适合保护短时间、低延迟的临界区;
- 真正强一致的业务约束,最终还要落到数据库唯一索引、状态机、幂等表等机制上。
3.4 Hash
Hash适合存储对象结构。它的结构类似:
1 | |
常用命令:
| 命令 | 作用 | 常见复杂度 |
|---|---|---|
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 | |
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 | |
生命周期管理的核心经验:
- 缓存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 | |
List可以做轻量队列,但要注意:
- 没有Kafka那样的消费位点、重平衡、消息持久化语义;
- 消费者取出后如果崩溃,消息可能丢失;
- 对可靠队列要求高时,应使用专业MQ,或考虑Redis Streams而不是List。
最新动态展示
1 | |
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 | |
SINTER可以找同时具有多个标签的用户。
社交关系:共同好友
1 | |
交集结果就是共同关注或共同好友。
抽奖系统
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 | |
排行榜要注意两个设计点:
- 排名范围要有边界,比如日榜、周榜、赛季榜;
- 分数相同时的排序规则要提前定义,必要时把时间因素编码进score或另存字段。
延时队列
把“执行时间戳”作为score:
1 | |
消费者不断取score小于当前时间的任务:
1 | |
取到后要用Lua脚本或WATCH事务保证“读取 + 删除”不会被多个消费者重复执行。
跳跃表是怎么帮ZSet快速查询的
Redis的ZSet在较大规模时会使用跳跃表等结构来维护有序性。跳跃表可以理解为“多层链表”:
1 | |
查找时不是从头一个个扫,而是从高层开始跳跃,发现跳过头了再下降一层。它用概率换结构简单度,平均查询、插入、删除都能接近O(log N)。
ZSet适合排行榜、时间调度、权重排序,但不适合做复杂多字段排序查询。如果需要“按城市、类型、状态、时间、价格组合筛选再排序”,应该优先考虑数据库索引或搜索引擎。
5.4 Bitmaps
Bitmap不是单独的数据类型,本质上是在String上按位操作。它特别适合表达大量用户的布尔状态。
常用命令:
1 | |
签到场景:
1 | |
千万级用户活跃统计:
1 | |
优点是极度节省内存。缺点是offset必须设计清楚,用户ID如果非常稀疏,会造成空间浪费。比如直接拿很大的数据库ID当offset,可能把Bitmap撑得很大。
5.5 HyperLogLog
HyperLogLog用于基数统计,也就是估算“去重后的数量”。典型场景是网站UV、接口独立访问用户数、设备活跃数。
常用命令:
1 | |
它的核心取舍:
- 优点:非常省内存;
- 缺点:结果是估算值,允许少量误差;
- 适合:大规模UV、独立设备数等统计;
- 不适合:抽奖去重、权限判断、精确计费等必须精确的场景。
5.6 KEYS与SCAN
KEYS pattern可以按模式查找key:
1 | |
但生产环境要极其谨慎,尤其不能随手执行:
1 | |
原因是KEYS会遍历整个keyspace,在key很多时可能阻塞Redis主线程,导致线上请求抖动甚至超时。
生产环境更推荐SCAN:
1 | |
SCAN是游标式迭代:
1 | |
使用SCAN也要注意:
- 它不保证每次返回数量严格等于
COUNT; - 一轮迭代过程中,key如果被新增或删除,结果可能有重复或遗漏;
- 客户端要能处理重复key;
- 它适合渐进式巡检、清理、统计,不适合强一致快照。
6. 高级特性与功能扩展
6.1 Pub/Sub
Pub/Sub是发布订阅模型:发布者把消息发到频道,订阅者监听频道。
常用命令:
1 | |
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 | |
在无人机/车辆轨迹场景中,Redis GEO可以做“附近目标粗筛”:
1 | |
注意:
- Redis GEO主要处理经纬度位置和距离,不处理复杂多边形拓扑;
- 低空业务里还需要高度、时间窗、空域规则,不能只靠二维距离;
- 如果要保存完整轨迹,Redis不应作为唯一事实库,通常还要落PostgreSQL/PostGIS、时序库或对象存储。
6.3 事务
Redis事务相关命令:
| 命令 | 作用 |
|---|---|
MULTI |
开启事务,后续命令进入队列 |
EXEC |
执行队列中的命令 |
DISCARD |
放弃事务队列 |
WATCH key ... |
监听key,实现乐观锁 |
UNWATCH |
取消监听 |
基础示例:
1 | |
在MULTI之后,命令只是进入队列:
1 | |
WATCH用于乐观锁。假设要扣减库存:
1 | |
如果WATCH后的key在EXEC前被其他客户端修改,EXEC会失败,客户端需要重新读取、判断、重试。
Redis事务为什么不支持传统数据库回滚
Redis事务和关系型数据库事务不是一回事。
关系型数据库事务强调ACID,支持复杂回滚。Redis事务更像是“把一组命令排队,然后一次性按顺序执行”,它保证执行期间不会被其他客户端命令插入,但不提供传统意义上的回滚。
例如:
1 | |
如果LPUSH foo bar因为foo是String而失败,前面的INCR foo可能已经执行成功。Redis不会自动把INCR回滚掉。
所以Redis事务适合:
- 少量命令的原子顺序执行;
- 配合
WATCH做乐观锁; - 实现轻量并发控制。
不适合:
- 替代数据库事务;
- 承载复杂业务状态回滚;
- 在失败后自动恢复业务一致性。
6.4 Lua脚本
Lua脚本是Redis里非常重要的原子性工具。它通过EVAL执行:
1 | |
简单示例:
1 | |
Lua脚本的价值不是“能写复杂代码”,而是把多次Redis操作合并成一次服务器端原子执行,减少并发竞态。
限流脚本示例
目标:一个用户60秒内最多访问100次。
1 | |
业务层拿返回值判断:
1 | |
延时队列抢占脚本示例
目标:多个消费者并发时,同一个到期任务只能被一个消费者拿走。
1 | |
Lua脚本实践规则
- 脚本要短,避免长时间阻塞Redis;
- 所有访问的key应通过
KEYS传入,不要在脚本里拼接动态key; - 参数值通过
ARGV传入,不要每次拼出不同脚本文本; - Redis Cluster中,多key脚本通常要求key落在同一个hash slot;
- 高频脚本可考虑
SCRIPT LOAD+EVALSHA,减少脚本传输; - Redis 7.4开始,
EVAL加载的脚本缓存会有LRU淘汰机制,但仍不应滥用动态脚本。
7. 常见业务建模速查
| 场景 | 推荐结构 | 关键命令 | 注意事项 |
|---|---|---|---|
| 缓存对象 | String / Hash | SET、GET、HSET、HGET |
TTL、缓存穿透、击穿、雪崩 |
| 分布式锁 | String | SET NX PX、Lua释放 |
锁值校验、过期时间、幂等兜底 |
| 接口限流 | String | INCR、EXPIRE、Lua |
避免INCR后无TTL |
| 最新动态 | List | LPUSH、LTRIM、LRANGE |
控制长度 |
| 轻量队列 | List | RPUSH、BLPOP |
不适合强可靠消息 |
| 用户标签 | Set | SADD、SINTER |
大集合运算成本 |
| 共同好友 | Set | SINTER |
结果集可能很大 |
| 抽奖 | Set | SPOP、SRANDMEMBER |
是否允许重复中奖 |
| 排行榜 | ZSet | ZINCRBY、ZREVRANGE |
score设计、榜单周期 |
| 延时任务 | ZSet | ZADD、ZRANGEBYSCORE、Lua |
抢占原子性、失败重试 |
| 签到统计 | Bitmap | SETBIT、GETBIT、BITCOUNT |
offset设计 |
| UV统计 | HyperLogLog | PFADD、PFCOUNT |
估算值,不适合精确计费 |
| 附近设备 | GEO | GEOADD、GEODIST、GEOSEARCH |
只做粗筛,复杂GIS另算 |
| 服务广播 | Pub/Sub | PUBLISH、SUBSCRIBE |
无持久化、无ACK |
8. 生产实践清单
8.1 Key命名
推荐用冒号表达层级:
1 | |
命名原则:
- 能从key看出业务含义;
- 避免过长key,key本身也占内存;
- 用统一前缀方便
SCAN MATCH巡检; - 多租户、多环境要有隔离前缀;
- Redis Cluster中需要多key同槽时,可用hash tag,例如
order:{1001}:lock和order:{1001}:items。
8.2 大Key与热Key
大Key是value过大或集合成员过多的key。它会带来:
- 网络传输慢;
- 删除阻塞;
- 持久化压力;
- 主从同步压力;
- 集群迁移压力。
热Key是访问量极高的key。它会带来:
- 单节点CPU或网络瓶颈;
- 缓存击穿;
- 集群分片不均。
治理思路:
- 大对象拆分;
- 限制List/Set/ZSet长度;
- 用
UNLINK异步删除大Key; - 热点缓存加本地缓存;
- 对高频计数做分片计数再汇总;
- 用监控发现慢命令、热Key和内存异常。
8.3 慢命令意识
Redis很快,但不是所有命令都快。以下命令在大数据量下要谨慎:
1 | |
不是说这些命令永远不能用,而是要问:
- 这个key可能有多大?
- 这个命令会不会扫全量?
- 有没有分页、游标或限制数量?
- 有没有在生产高峰期执行?
- 返回结果会不会把客户端或网络打满?
8.4 缓存常见问题
| 问题 | 现象 | 常见处理 |
|---|---|---|
| 缓存穿透 | 查询不存在的数据,每次都打到DB | 空值缓存、布隆过滤器、参数校验 |
| 缓存击穿 | 热点key过期瞬间大量请求打到DB | 互斥锁、逻辑过期、热点预热 |
| 缓存雪崩 | 大量key同时过期或Redis故障 | TTL随机抖动、多级缓存、限流降级 |
| 缓存不一致 | DB和缓存短时间不一致 | 先更新DB再删缓存、消息补偿、延迟双删视场景使用 |
缓存一致性没有银弹。大多数业务要接受“短时间最终一致”,并通过幂等、重试、补偿和过期机制兜底。
9. 学习路线建议
1 | |
这篇是Redis基础。后续进阶应重点学习:
- RDB和AOF持久化;
- 主从复制、哨兵和集群;
- 内存编码和对象模型;
- 过期删除和内存淘汰;
- 缓存一致性;
- Redis Cluster槽位和多key限制;
- Redis Streams和可靠消息模型。