我最早接触 Redis,其实是因为一次同事半夜发来的求助——线上某个接口突然变慢,数据库连接池被打满,最后定位到是因为一个热点数据没有加缓存,高峰期几千个请求全都怼到了 MySQL 上。那次之后我才认真把 Redis 从“听说过”到了解、使用、再到踩坑排查,一点点吃透。
作为入门教程的“第一章”,这篇文章我不打算写成官方文档的翻译版,而是从一个使用者的角度,把 Redis 的来龙去脉、安装方式、核心数据结构、持久化机制、高可用方案,以及日常开发中一定会遇到的坑都串一遍。内容会保持“能用、能落地”的节奏,每个部分都尽量把“为什么这样做”讲明白,而不是只告诉你“命令是什么”。
如果你正准备学 Redis,或者已经在项目里用了但有些底层逻辑还模模糊糊,这篇值得你花点时间读完。
1. Redis 是什么,以及它凭什么这么流行
1.1 先理解它的本质:一个跑在内存里的键值对数据库
Redis 的全称是 Remote Dictionary Server,直译过来就是“远程字典服务器”。你看这名字就知道,它本质上就是一个基于字典结构(key-value)的存储系统。和 MySQL、PostgreSQL 这些关系型数据库最大的区别在于,Redis 的数据主要存储在内存里,读写速度非常快,单实例的 QPS 可以轻松达到十万级别,而且它支持的数据结构比普通 KV 存储要丰富得多。
我习惯把它比作“放在内存里的多功能工具箱”。同样是存一个用户信息,你用 MySQL 可能要建表、写 SQL、做索引;用 Redis 的话,你可以用 Hash 类型直接存一个对象,用 List 存操作日志,用 ZSet 做排行榜,用 Set 做去重和交集运算。甚至很多临时性的标记,比如“这个用户是否已签到”,也可以直接用 String 类型配合位图来搞定。
这里有一个很容易被忽略的点:Redis 虽然是单线程模型,但它的性能并不低。你可能会问,单线程会不会浪费多核 CPU?其实 Redis 的瓶颈从来不在 CPU,而在网络 IO 和内存大小。单线程的好处是避免了多线程竞争带来的上下文切换和锁开销,同时也让所有操作都是原子性的——这个特性在实现分布式锁、计数器等场景时特别好用。
1.2 为什么是 Redis,而不是 Memcached
早年间一说到缓存,很多人第一反应是 Memcached。现在 Redis 成了默认选择,原因是多方面的。我简单列一个对比,你就明白了:
| 对比维度 | Redis | Memcached |
|---|---|---|
| 数据结构 | 支持 String、Hash、List、Set、ZSet 等 | 仅支持 String |
| 持久化 | 支持 RDB / AOF 持久化 | 不支持 |
| 分布式支持 | 主从、哨兵、Cluster 集群 | 客户端分片 |
| 功能扩展 | 支持 Lua 脚本、事务、发布订阅、Stream | 较弱 |
| 使用难度 | 中等,功能丰富但概念也多 | 简单,开箱即用 |
| 适用场景 | 缓存、队列、排行榜、分布式锁、实时计算等 | 单纯缓存 |
Memcached 在多线程模型下处理纯 KV 场景确实也很快,但一旦你需要在缓存里做聚合操作,比如“取两个集合的交集”“对某个 key 做自增”,Memcached 就无能为力了。而 Redis 这些操作都是原生支持的,直接省掉了一大堆业务代码。这也是它被称为“中间件”而不是单纯“缓存”的原因。
1.3 学了 Redis 能用在哪些真实场景
说实话,Redis 的应用面比我一开始想的宽得多。往大了说有:
- 缓存加速:把热点数据、接口结果、页面片段缓存起来,降低数据库压力。
- 分布式会话共享:多台服务器部署时,登录状态统一存 Redis,不需要做 sticky session。
- 排行榜与计数:比如积分榜、点赞数、在线人数,用 ZSet 和 String 自增配合实现。
- 分布式锁:多个服务实例抢同一份资源时,用 SETNX 或者 Redisson 实现互斥。
- 消息队列:List 和 Stream 可以做轻量级消息队列,适合中小型项目。
- 秒杀与限流:配合 Lua 脚本,可以在 Redis 里完成库存扣减和令牌桶限流。
- 实时数据统计:比如 UV 去重统计,用 Set 或 HyperLogLog 都能搞定。
所以不管你是后端开发、架构师还是运维,Redis 都是绕不开的一项技能。这篇“第一章”我先把地基打牢,后面如果有机会再展开讲分布式锁、缓存穿透这些进阶主题。
2. 环境准备:五分钟装出一个能跑的 Redis
2.1 Windows 下最简单的安装方式
很多人第一次接触 Redis 是在 Windows 上。说实话,Redis 官方并不原生支持 Windows,但微软的团队长期维护着一个 Windows 移植版本,而且现在 GitHub 上也有不少编译好的 Release 包。你可以去 Redis 官网或者 GitHub 的 microsoftarchive/redis 仓库下找到 zip 包,解压之后就有 redis-server.exe 和 redis-cli.exe 两个关键文件。
使用步骤很简单:
# 打开命令行,进入解压目录 redis-server.exe # 另开一个命令行窗口,连接服务 redis-cli.exe默认情况下,Redis 监听在 6379 端口,没有密码。你可以在 redis-cli 里执行PING,如果返回PONG,说明服务已经正常跑起来了。
不过我这里要提醒一句:如果你只是为了本地学习,用 Windows 版没问题;如果是用于生产环境,还是建议用 Linux,因为官方对 Linux 的版本迭代、性能优化和 bug 修复都更及时。生产环境用 Windows 版 Redis 我踩过内存管理上的几个小坑,不展开说了,建议直接避开。
2.2 Linux 下用源码编译安装
生产环境最常见的方式还是从源码编译安装,尤其是你希望指定 Redis 版本、用上最新的特性时。我以 Redis 7.0 为例,步骤如下:
# 1. 下载源码包 wget https://download.redis.io/releases/redis-7.0.12.tar.gz # 2. 解压 tar xzf redis-7.0.12.tar.gz # 3. 进入目录并编译 cd redis-7.0.12 make # 4. 可选:安装到 /usr/local/bin make install编译完成后,在 src 目录下会生成 redis-server、redis-cli、redis-sentinel 等可执行文件。用make install安装到系统目录后,就可以直接用命令启动了:
# 前台启动,适合调试 redis-server # 后台启动 redis-server --daemonize yes # 指定配置文件启动 redis-server /etc/redis/redis.conf这里有一个新手容易忽略的点:编译前最好确认系统已经安装了 gcc 编译器和 make 工具。如果缺少,编译过程中会报错,提示找不到 cc 或 make 命令。Ubuntu 上可以执行:
apt-get install -y build-essentialCentOS 上是:
yum install -y gcc make2.3 用 Docker 跑一个 Redis
如果你不想污染宿主机环境,或者需要快速起多个实例做实验,Docker 是最方便的方式。我之前用 Docker 搭主从集群就花了几分钟时间,比在虚拟机里一个个装省事太多。
最简单的启动命令:
docker run -d --name redis \ -p 6379:6379 \ -v /data/redis:/data \ redis:7.0 --appendonly yes我来解释一下这些参数:-d表示后台运行;--name redis给容器起名字;-p 6379:6379把宿主机的 6379 端口映射到容器的 6379 端口;-v /data/redis:/data是把容器内的数据目录挂载到宿主机,防止容器删除后数据丢失;最后的--appendonly yes是传给 Redis 的启动参数,开启 AOF 持久化。
如果你会用 docker-compose,还可以写成这样:
services: redis: image: redis:7.0 container_name: redis restart: always ports: - "6379:6379" volumes: - /data/redis:/data command: redis-server --appendonly yes --requirepass 123456这段配置里我加了一个--requirepass 123456,也就是设置访问密码。这只是演示,生产环境请使用强度更高的密码,别拿 123456 开玩笑。
2.4 可视化客户端选哪个
命令行再强大,日常开发中你总会想在图形界面里看看 key 的情况。我这几年前后用过不少 Redis 可视化客户端,这里做个对比:
| 工具名称 | 平台支持 | 特点 | 适用人群 |
|---|---|---|---|
| Redis Desktop Manager | Windows / Linux / macOS | 老牌工具,界面简洁,但新版部分功能收费 | 大多数后端开发 |
| Another Redis Desktop Manager | 全平台 | 免费开源,支持多标签、内存分析、慢日志查看 | 日常开发首选 |
| RedisInsight | 全平台 | Redis 官方出品,自带可视化 CLI、内存分析、集群管理 | 需要深入排查性能问题时 |
| Tiny RDM | 全平台 | 免费、颜值高、支持多种连接方式 | 喜欢尝试新工具的同学 |
我个人目前主力用的是 Another Redis Desktop Manager,因为它在免费的前提下功能足够完整,支持连接哨兵、集群,还能直接查看 key 的 TTL 和内存占用,日常排查问题完全够用。RedisInsight 我也会装一份,主要用于看内存碎片率和执行分析命令,官方工具对内部数据的可视化确实做得更细。
2.5 安装完之后先做这几件事
启动好了不要急着写业务代码,先花两分钟确认基础状态:
# 检查能否连通 redis-cli ping # 如果设置了密码 redis-cli -a 你的密码 ping # 查看服务信息,重点关注内存、连接数、命中率 redis-cli info # 查看当前所有 key 的数量(生产环境慎用,会影响性能) redis-cli dbsizeINFO命令的输出信息量很大,我一般习惯用redis-cli INFO Memory看内存情况,用redis-cli INFO Stats看命中率,用redis-cli INFO Replication看主从状态。这些命令在刚安装完做体检时尤其有用,后面排障也会频繁用到。
3. 五种基础数据类型:学会用场景反推命令
3.1 String 字符串:不只是缓存
String 是 Redis 里最基础的数据类型,它可以是字符串、整数或者浮点数。很多人以为它就是“缓存一个字符串”,其实它的应用远不止如此。
最常见的用法当然是缓存:
SET user:10001 '{"name":"张三","age":18}' GET user:10001但 String 还有一个高频用法——计数器。因为 Redis 是单线程执行命令,INCR操作天然是原子性的,所以我们可以用它来实现点赞数、访问量、库存扣减等功能:
INCR page:view:20250101 INCRBY page:view:20250101 10 DECR stock:12345这里有个细节值得说:如果你用 RedisTemplate 做increment()操作报错,一定要第一时间检查 key 里存的到底是不是整数。我见过很多次是因为之前往同一个 key 里放了 JSON 字符串,再调用自增方法时就抛了异常。关于这个问题,我在后面第六部分会专门展开讲。
另外,String 还可以配合SETEX、PSETEX设置带过期时间的值,以及配合SETNX来实现分布式锁的底层逻辑。
3.2 Hash 哈希:存对象的第一选择
Hash 在 Redis 中是一个 string 类型的 field 和 value 的映射表,特别适合用来存储对象。比如用户信息,用 String 存储的话只能整体读写,用 Hash 就可以单独读写某个字段:
HSET user:10001 name "张三" age 18 city "杭州" HGET user:10001 name HGETALL user:10001 HINCRBY user:10001 age 1这种结构在业务上非常实用。比如在线购物车的实现,每个用户的购物车就是一个 Hash,field 是商品 ID,value 是商品数量:
HSET cart:user10001 item:iPhone 1 HSET cart:user10001 item:MacBook 2你可以想一下,如果用 String 存购物车,每次修改都要把整个对象读出来再写回去,并发情况下很容易丢数据。而 Hash 天然支持对单个字段的修改和自增,这才是它存在的核心意义。
3.3 List 列表:队列和时间线的天然载体
List 是一个双向链表,可以从头部或尾部插入、弹出元素。这个概念学过数据结构的都不会陌生,它最常见的应用就是消息队列:
# 生产者:生产任务 LPUSH task:queue task_001 LPUSH task:queue task_002 # 消费者:消费任务 RPOP task:queue需要注意的是,用 List 做消息队列有一个小问题:如果消费者循环去RPOP,队列为空时会一直空转,浪费 CPU。这时可以用BRPOP这个阻塞式命令,队列为空时消费者会休眠,直到有数据进来再返回:
BRPOP task:queue 0最后一个参数 0 表示永不超时,这样子客户端就能一直挂在那里等消息。
List 的另一个经典场景是“最新动态/时间线”。比如微博的时间线列表,用LPUSH把新动态放在最前面,再配合LTRIM截断长度,就能实现一个轻量级的“只看最近 100 条”的功能:
LPUSH user:10001:timeline feed_3001 LTRIM user:10001:timeline 0 993.4 Set 集合:去重和集合运算的利器
Set 是无序、唯一的字符串集合。它最直接的应用就是去重。比如一个用户点赞过的所有帖子:
SADD post:10086:liked user_101 SADD post:10086:liked user_102 SADD post:10086:liked user_101 # 查看哪些用户点了赞 SMEMBERS post:10086:liked因为 Set 本身就保证元素不重复,所以重复执行SADD不会产生脏数据。
更强大的是集合运算。比如在社交场景里,“我关注的人有哪些”、“他关注的人有哪些”,用两个 Set 存好之后,取交集就能得到共同关注:
SINTER user:10001:following user:10002:following还有一个很好玩的场景——抽奖。把参与用户全部SADD进 Set,然后用SRANDMEMBER或者SPOP随机取出中奖者,轻轻松松:
SPOP lottery:20250101 33.5 ZSet 有序集合:排行榜的标准答案
ZSet 和 Set 一样是唯一元素的集合,区别在于每个元素都关联了一个 double 类型的 score,Redis 会按照 score 从小到大排序。这个特性让它成为排行榜功能的不二之选。
ZADD rank:game 100 "player_1" ZADD rank:game 200 "player_2" ZADD rank:game 150 "player_3" # 获取前 10 名,分数从高到低 ZREVRANGE rank:game 0 9 WITHSCORES # 获取某个玩家的排名 ZRANK rank:game player_2 # 给某个玩家加分 ZINCRBY rank:game 50 player_2你在应用商店看到的游戏热度榜、直播间人气榜,底层九成都是这个套路。ZSet 还能做限流、延时队列等高级玩法,比如把任务到期时间作为 score,用ZRANGEBYSCORE去取出到期的任务。
到这里你可能会发现,五大数据结构虽然名字听起来简单,但每种都对应着一批非常具体的业务场景。学习的时候最好反过来记:先知道业务上要解决什么问题,再看看哪种类型最合适。这样才是“用场景反推命令”,而不是死记命令。
4. 持久化:Redis 重启后数据还在吗
4.1 RDB 快照:速度快,但丢数据有风险
Redis 默认的持久化方案是 RDB,它会在指定时间间隔内把内存数据生成一份二进制快照,保存到磁盘上的 dump.rdb 文件里。
触发 RDB 的方式有几种:配置文件中定义快照条件、手动执行SAVE或BGSAVE命令、主从复制时自动触发等。配置文件里默认的触发条件类似这样:
save 900 1 save 300 10 save 60 10000这三行的意思是:如果 900 秒内有至少 1 个 key 发生变化,就执行一次快照;或者 300 秒内有 10 个 key 发生变化;或者 60 秒内有 10000 个 key 发生变化。用大白话讲,就是数据改动越频繁,备份点就越密。
RDB 的优点很明显:文件紧凑,恢复速度快,适合做冷备份和定期归档。但它的缺点也不能忽视——因为快照是“间隔性”的,如果你配置了 900 秒存一次,那中途宕机时,最近十几分钟的数据可能就丢了。对于像支付、库存这类不能接受丢数据的业务,RDB 不适合单独使用。
4.2 AOF 日志:数据更安全,但要控制体积
AOF 的全称是 Append Only File,它的机制是把每一条写操作命令以日志形式追加到文件末尾。听起来有点类似 MySQL 的 binlog。恢复的时候,把所有命令重新执行一遍,数据就回来了。
AOF 有三个刷盘策略,配置文件里用appendfsync指定:
| 策略 | 行为 | 安全性 | 性能影响 |
|---|---|---|---|
| always | 每执行一条写命令就同步写盘 | 最安全,最多丢一条命令 | 最慢 |
| everysec | 每秒批量写盘一次 | 最多丢 1 秒数据 | 性能较好 |
| no | 由操作系统决定何时写盘 | 不可控,可能丢较多数据 | 最快 |
生产环境我一般建议使用everysec,这是安全性和性能之间最平衡的折中。always虽然安全,但在写密集型场景下会有明显的性能损耗。
AOF 有个天然问题:随着运行时间增长,日志文件会越来越大。Redis 提供了 AOF 重写机制,也就是把内存中的数据重新整理成最小化的写命令集合,再用这个新日志替换旧日志。可以手动BGREWRITEAOF触发,也可以配置自动触发条件:
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb意思是:当 AOF 文件比上次重写后的体积翻了一倍,且超过 64MB 时,自动触发重写。
4.3 混合持久化:两种方案我都要
Redis 4.0 之后引入了混合持久化。简单说,RDB 快照做全量备份,AOF 日志记录快照之后的增量数据。这样重启恢复的时候,加载 RDB 文件的速度很快,日志部分补的增量又很少,恢复时间大幅缩短。
开启方式很简单,在配置文件里加一行:
aof-use-rdb-preamble yes开启混合持久化后,生成的 AOF 文件前半部分是 RDB 格式的二进制快照,后半部分才是追加的 AOF 命令。如果你用编辑器直接打开混合持久化文件,会发现前面是乱码,后面才是可读的命令文本,这不是文件损坏,是正常现象。
我个人在实际项目中,非极端场景的推荐配置是:RDB 用于定期备份,AOF 开启 everysec 用于故障恢复,且开启混合持久化。这套组合既能保证数据相对安全,又不会让重启恢复时间过长。
5. 从单机到集群:主从、哨兵、Cluster
5.1 主从复制:读写分离的起步方案
单机 Redis 一旦宕机,整个系统可能就要跟着不可用。一个最基础的容灾手段就是配置从节点,让数据在主节点上写入,异步复制到从节点,读请求可以分流到从节点上。
配置从节点非常容易。在从节点的配置文件里加一行:
replicaof 192.168.1.101 6379或者直接用命令动态设置:
redis-server --port 6380 --replicaof 192.168.1.101 6379设置完成后,主从之间会进行全量同步:主节点把内存数据生成 RDB 快照发给从节点,从节点清空自己旧数据再加载新快照,整个过程是异步的,对主节点影响很小。为了避免全量同步过于频繁,从节点重启后如果能部分同步,会走增量复制。
用 Docker 起一主一从也很快:
# 主节点 docker run -d --name redis-master -p 6379:6379 redis:7.0 # 从节点 docker run -d --name redis-slave -p 6380:6379 redis:7.0 \ redis-server --replicaof 宿主机IP 6379主从复制解决了读扩展和基础容灾问题,但它有一个致命缺陷:主节点挂掉后,从节点不会自动提升为主节点,整个系统需要人工干预才能恢复写能力。这就轮到哨兵出场了。
5.2 哨兵:给 Redis 装一个自动故障转移开关
Sentinel(哨兵)是 Redis 提供的高可用解决方案。它的核心职责有三个:监控、通知、自动故障转移。
你可以把哨兵想象成一个 7x24 小时的专职监控员。它不停地发 PING 给主节点和从节点,一旦发现主节点不可用,就发起投票,从候选从节点里挑一个升级为新的主节点,再通知其他从节点切换复制目标。整个过程对外部应用是透明的,前提是你的客户端也配置了哨兵地址。
一个最精简的哨兵配置如下:
port 26379 sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000sentinel monitor后面的2表示:至少 2 个哨兵节点都认为主节点挂了,才启动故障转移。这是为了防止单个哨兵误判导致整个集群震荡。
可千万别小看这个数字。我在测试环境玩坏过一次,就是因为只有一个哨兵节点,网络抖动一下,主节点被误判下线,自动完成了切换,结果本来健康的服务反而断了几分钟。生产环境哨兵节点至少部署 3 个,且分散在不同机器上。
5.3 Cluster 集群:数据量大到一台装不下时
当数据量达到几十 GB 甚至上百 GB,单机内存扛不住,或者你希望写性能也能水平扩展时,就需要 Redis Cluster 出场了。
Cluster 架构和主从、哨兵的最大区别是:数据会分片。整个集群的 key 空间被划分为 16384 个哈希槽,每个节点负责一部分槽。执行命令时,Redis 会根据 key 计算出它属于哪个槽,再定位到对应节点。客户端可以直接连接任意一个节点,如果 key 不在该节点上,会返回一个 MOVED 重定向指令,客户端再转到正确节点执行。
搭建 Cluster 最少需要 3 个主节点,每个主节点还可以带一个从节点。用官方提供的redis-cli --cluster create命令可以快速搭建:
redis-cli --cluster create \ 192.168.1.1:6379 \ 192.168.1.2:6379 \ 192.168.1.3:6379 \ --cluster-replicas 1--cluster-replicas 1表示每个主节点挂一个从节点,也就是至少 6 个实例。对于小规模集群来说,这个配置已经够用。
Cluster 虽然强大,但引入的复杂度也不小。比如多 key 操作不再无条件支持,除非所有 key 都在同一个哈希槽里;再比如数据迁移过程中可能遇到CROSSSLOT错误。我的建议是:数据量真的达到单机瓶颈,再考虑 Cluster,而不是一开始就上。
5.4 生产部署方案怎么选
很多新手会纠结到底该用哪种方案,其实判断标准没那么复杂:
- 只有一台服务器,想避免重启丢数据:持久化配好就够了,不需要集群。
- 服务有读多写少的特征,希望缓解读压力:主从就够了,读走从节点。
- 要求自动切换,不想半夜爬起来手动切换主节点:上哨兵。
- 单机内存不够,或者写压力大,需要水平扩展:上 Cluster。
当然实际生产中,主从和哨兵往往是一起部署的:主从负责数据复制和读写分离,哨兵负责自动故障转移。这套组合是中小团队用得最多的高可用方案。
6. 实战避坑:这些问题我大概率都遇到过
6.1 RedisTemplate 的 increment() 报错,90% 是序列化问题
用 Spring Boot 开发的同学,十有八九会在做计数器时遇到过这个异常:
java.lang.IllegalArgumentException: value is not an integer or out of range
我一开始以为是 Redis 版本问题,后来定位到原因其实很俗:RedisTemplate 默认使用 JdkSerializationRedisSerializer 做序列化,往同一个 key 里写入的时候,字符串和数字会被序列化成完全不同的二进制结构。如果你之前用set("key", "100")存了一个字符串,后面再用increment("key")操作时,Redis 拿到的底层根本不是整数,自然就报这个错。
解决方案有两种。
第一种:统一使用 StringRedisTemplate,所有 key 和 value 都以字符串形式存储。increment()依然可以用,因为 Redis 自己会在内存里把字符串数字转换成整数运算:
@Autowired private StringRedisTemplate stringRedisTemplate; stringRedisTemplate.opsForValue().increment("count:10001");第二种:自己配置 RedisTemplate 的序列化器,把 key 和 value 都改成 StringRedisSerializer,并让 Hash 结构也使用同样的序列化方式:
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer = new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(stringSerializer); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; }我的经验是:如果项目里的缓存值基本都是 JSON 字符串或可序列化对象,直接采用第二种方式,后面踩的坑会少很多。如果你只想快速解决计数问题,第一种最直接。
6.2 缓存穿透、击穿、雪崩,治理思路别搞混
这三个词看起来像,但成因和应对方法完全不同。我在面试时也经常用这三个场景考察候选人,很多人会把它们混为一谈。
- 缓存穿透:查询一个完全不存在的数据,缓存和数据库都没有,请求直接打到数据库。解决办法:缓存空值并设短过期时间,或者用布隆过滤器拦截明显不存在的 key。
- 缓存击穿:某个热点 key 到了过期时间,恰好大量请求同时来,缓存失效后所有请求打到数据库。解决办法:热点数据不设置过期时间,或者用互斥锁保证只有一个请求去重建缓存。
- 缓存雪崩:大量 key 在同一时间集中过期,或者 Redis 整个宕机,导致数据库压力瞬间飙升。解决办法:给过期时间加随机抖动,用高可用方案保障 Redis 不宕机。
这里我特别想强调第 2 点,缓存击穿的核心是“并发重建缓存要加锁”。当年我们做活动页时,一个爆款商品的详情接口在零点的瞬时 QPS 很高,缓存 30 分钟过期,每次过期的那一瞬间都会打慢数据库一次。后来我改用:线程内先从缓存拿,拿不到就尝试获取分布式锁,拿到锁的线程重新查库回填缓存,其他线程短暂自旋后再从缓存读。这个方案上线后,数据库连接池的波峰直接平缓了很多。
6.3 分布式锁:只靠 SETNX 是不够的
用 Redis 做分布式锁是开发中的常见需求,最原始的写法是:
SETNX lock_key unique_value如果返回 1,说明拿到锁;如果返回 0,说明锁已被别人持有。但这有一个经典问题:如果拿到锁的线程在执行过程中崩溃了,锁永远不会释放,造成死锁。所以正确的姿势是给锁设置过期时间:
SET lock_key unique_value NX PX 30000这条命令的意思是:只有当 lock_key 不存在时才设置它,同时设置 30 秒过期时间。只要锁的持有者挂了,30 秒后锁自动释放。
但还有一个隐患:如果业务执行时间超过 30 秒,锁自动过期了,其他线程拿到了锁,而此时第一个线程还没执行完,就会发生并发安全问题。解决办法是使用 Redisson 的看门狗机制,它会自动续期;或者干脆在释放锁之前校验 value 是不是自己设置的唯一标识。
释放锁也不是简单的DEL,更安全的做法是用 Lua 脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end这段脚本的含义是:先判断当前锁的 value 是不是自己设置的,如果是才删除,避免误删别人的锁。我在生产环境第一次实现分布式锁的时候就只用了 SETNX 加 DEL,结果在高并发下一个慢查询把锁的时间拖超了,恰好被另一个线程删掉了锁,直接导致资源竞争。后来换成带唯一值和 Lua 脚本的写法,就再没出过事。
6.4 序列化方案选错,排查成本很高
使用 RedisTemplate 时最容易忽略的是序列化方式。很多人默认用 JdkSerializationRedisSerializer,结果往 Redis 里一存,key 就变成了一长串带序号的二进制,肉眼根本看不懂,用可视化工具看到的 key 也不是项目里定义的字符串。一旦出问题,想从 Redis 侧去查数据几乎没法弄。
所以从项目第一天开始,就建议统一配置序列化:
- key 序列化器用 StringRedisSerializer,保证 key 可读。
- value 序列化器用 Jackson 或 Fastjson,保证存入的是 JSON 字符串。
- Hash 的 key 和 value 也要分别设置序列化器。
这里不是说 JDK 序列化完全不行,它有个好处是保留了完整的 Java 类型信息,反序列化时不容易出类型转换问题;但它可读性差、体积大、跨语言不友好,这在现在这种多团队协作的环境里非常吃亏。我的建议是:除非项目里完全没有跨语言需求、也不打算排查 Redis 里的数据,否则别用 JDK 序列化。
6.5 从日志里找故障线索,慢日志和运行日志都要看
Redis 本身提供了比较丰富的日志体系。第一个值得关注的是慢查询日志。默认情况下,执行时间超过 10 毫秒的命令会被记录,但很多生产环境建议把这个阈值调低,比如 2 毫秒:
slowlog-log-slower-than 2000 slowlog-max-len 128排查的时候,用SLOWLOG GET 10就能看到最近最慢的 10 条命令。我遇到过一种情况:某个大集合的SMEMBERS操作耗时很长,把 CPU 打满,整个 Redis 实例的响应都变慢了。查慢日志才发现罪魁祸首是这条命令,后来果断改成了SSCAN分批次遍历,问题才消失。
第二个是运行日志。Redis 的运行日志默认输出在配置的 logfile 里,常见信息包括启动参数、连接变化、主从切换、持久化状态等。哨兵模式下,故障转移的每一步都会记录在哨兵的日志里,排查“为什么切换了主节点”这类问题非常有用。
我之前排查过一次诡异的主从切换,最后就是靠看哨兵日志发现是某台机器的时钟发生了跳变,导致哨兵误判主节点主观下线。日志里白纸黑字写着+sdown、+odown、+switch-master这几个状态变化,一步步看下来,比瞎猜高效太多。
6.6 常见问题速查表
我整理了一份高频问题的快速对照表,当你遇到类似场景时可以翻出来看看:
| 问题现象 | 可能原因 | 排查/解决方向 |
|---|---|---|
| 连接超时或拒绝连接 | 服务未启动、防火墙拦截、bind 配置限制 | 先redis-cli ping,再检查配置文件 bind 和端口 |
increment()报 not an integer | 序列化类型不对或 key 中存的不是整数 | 换 StringRedisTemplate,或改为字符串数字 |
| key 前缀乱码 | 序列化器没有配置好,用了 JDK 默认 | 配置 StringRedisSerializer 和 JSON value 序列化器 |
| 内存突然暴涨 | 大 key 或过期策略没生效 | 用redis-cli --bigkeys扫描大 key,检查 maxmemory 配置 |
| 缓存雪崩 | 多个 key 同时过期 | 过期时间加随机值,热度高的 key 不设过期 |
| 主从切换后数据没同步 | 哨兵配置问题或网络分区 | 检查哨兵日志,确认新的主从关系是否建立 |
| 阻塞命令导致卡顿 | KEYS、SMEMBERS、HGETALL 等操作大集合 | 改用 SCAN、SSCAN、HSCAN分批遍历 |
这份表里的大部分问题,我都实际排查过不止一次。你会发现,大部分坑归根结底就三个原因:序列化配置不对、参数细节没注意、把单机玩法带进了集群环境。
如果你刚开始接触 Redis,我的建议是先别急着背命令,而是把 String、Hash、List、Set、ZSet 分别对应到“计数器、对象存储、队列、去重集合、排行榜”这五个典型场景里,用场景去理解数据结构。再花点时间把持久化和主从复制的配置亲手搭一遍,因为这两个部分是生产环境最有价值、也最容易出问题的环节。
最后一个我在实际项目中特别有感触的体会:Redis 用得好不好,往往不取决于你会多少冷门命令,而在于你对“数据应该存哪里、怎么存才不会丢、高并发下怎么保证一致性”这些基础问题的理解深度。基础的东西反而要反复琢磨,它会在你后面写业务代码、排故障、做架构设计时,一遍遍派上用场。