Redis学了很久,也踩了不少坑,趁这次做技术复盘,把从安装部署到集群高可用、再到面试高频点的一些实战经验和踩坑记录整理出来。这篇东西目标很明确:让你看完之后能真正把Redis用起来,而不是停留在背命令、看过教程就忘的层面。不管是刚接触Redis的新手,还是已经用过但想系统梳理一遍的开发者,内容都会对你有帮助。
Redis从入门到实战:安装、数据类型、分布式锁与高可用集群全解析
我最早接触Redis,是因为一个让他印象深刻的线上问题:数据库连接被打满,接口响应超过10秒。当时的系统每天几百万请求量,缓存方案用的是本地Map加定时刷新,一到高峰期就各种穿透、击穿。后来把Redis引入作为缓存层,问题才真正解决。也是从那时候开始,Redis从一个“键值数据库”变成了我技术栈里离不开的工具。
Redis是什么?一句话概括:一个基于内存的、支持多种数据结构的、可以用来做缓存、做分布式锁、做消息队列、做排行榜的键值存储系统。它解决的核心问题就两个字——速度。内存读写是微秒级别,比数据库的毫秒到秒级快几个数量级。它能做的事远不止缓存:计数、限流、分布式锁、延迟队列、排行榜、社交关系、地理位置搜索,只要是高频读写的场景,Redis几乎都能插一脚。
这篇笔记会从安装开始,把数据类型、持久化、主从哨兵集群、分布式锁、常见面试题这些全部串一遍。每一块都会结合我实际操作中遇到的问题来讲,不是抄官方文档那种冷冰冰的翻译。
1. Redis到底解决了什么问题:核心价值与适用边界
1.1 为什么大家都在用Redis:从一次接口慢查询说起
先看一个非常典型的场景。某个订单查询接口,SQL本身不复杂,但表数据量到了两千万行,每次查询都要走好几个索引、关联四五张表。高峰期QPS稍微上来一点,数据库CPU直接飙到90%以上,接口响应时间从50ms恶化到2秒。用户投诉不断,DBA半夜被电话叫起来。
没有Redis的时候,你能做的优化无非是加索引、优化SQL、读写分离、分库分表。但这些都是治标不治本,因为数据库的本质瓶颈在于磁盘IO,再优化的SQL也扛不住每秒几千次的随机磁盘读取。
引入Redis之后,逻辑就很清晰了:第一次请求查完数据库,把结果按一定结构写到Redis;后续请求直接走内存,200万次请求里可能只有几百次真实打到数据库。数据库压力降下去,接口响应从秒级回到毫秒级。
这就是Redis作为缓存层的价值——用内存换IO,用读写速度换数据库压力。几乎所有大流量的系统,架构里都有这样一层缓存。
1.2 哪些场景不适合用Redis
不过说实话,Redis也不是万能的。我见过不少团队把Redis当万能药,什么都往里塞,最后踩坑踩得很惨。
第一个不适合的场景是低频访问但数据量极大的数据。比如用户的历史订单流水,一个用户一年可能下几百单,全国几千万用户就是上亿条数据。全放进Redis,内存根本扛不住,成本远超收益。这种数据的正确归宿是冷存储(如对象存储、归档数据库),缓存只放最近一个月的热数据。
第二个是强事务场景。Redis的事务(MULTI/EXEC)只保证原子性,不保证隔离性——它不像MySQL那样有一致性视图,事务执行过程中其他客户端的写入不会被阻塞。如果业务对数据的强一致性有硬性要求,比如转账、库存扣减,不能只用Redis。
第三个是复杂查询场景。Redis本身是个KV存储,不支持复杂的条件查询、聚合分析。你要是想“查所有上周注册且购买超过3次的用户”,Redis做不了,那是SQL的活。
选型的时候想清楚一个问题:这个数据是高频读、单键访问、能容忍短暂不一致吗?三个条件都满足,Redis是绝配;有一条不满足,就要慎重了。
2. 把Redis跑起来:Windows、Linux与Docker三种安装姿势
Redis虽然官方不支持Windows(微软自己维护过一个移植版,后来也停更了),但实际开发中Windows环境依然很常用。这里把三种常用安装方式的实际操作、注意点和踩过的坑都写一遍。
2.1 Linux下编译安装:最稳的方式
生产环境几乎都是Linux,推荐用源码编译安装的方式,原因有两个:一是可以指定安装路径,方便管理;二是编译参数可控,比如可以关闭某些用不到的特性来减小体积。
编译安装的完整流程:
# 下载(用稳定版,不要用 alpha/beta) wget https://download.redis.io/releases/redis-7.2.4.tar.gz tar -zxvf redis-7.2.4.tar.gz cd redis-7.2.4 # 编译并安装到指定目录 make && make install PREFIX=/usr/local/redis # 拷贝配置文件,默认在源码目录下的 redis.conf cp redis.conf /usr/local/redis/注意几个只有踩过坑才知道的细节:
- GCC版本不能太低。Redis 6.0以上对编译器版本有要求,老版本CentOS自带的GCC 4.8编译Redis 7.x会直接报错。提前用
gcc --version确认版本,不行就先升级一下。 - make test可以跑一遍。
make test会执行完整的测试用例,虽然耗点时间,但能确认当前环境是否兼容。网上不少“编译成功但启动直接崩”的案例,跳过测试是主要原因之一。 - 配置文件路径别搞错。Redis启动时不会像MySQL那样自动加载某个目录下的配置文件,必须通过
redis-server /path/redis.conf显式指定,否则会用默认配置启动——默认配置在安全上完全是裸奔状态,没有密码、没有持久化。
启动与验证:
/usr/local/redis/bin/redis-server /usr/local/redis/redis.conf /usr/local/redis/bin/redis-cli ping # 返回PONG即启动成功启动完别急着高兴,先看几件事:日志有没有报错、端口是否正常监听(默认6379,如果不能用netstat -tlnp | grep 6379确认)、配置文件里的daemonize是否设置成了yes(不然前台运行,窗口一关进程就没了)。
2.2 Windows下安装的常见坑
Windows环境不推荐用官方源码编译,用社区维护的Windows移植版或者直接用Docker更方便。网上有很多Redis Windows版下载资源,比如tpenguinltg维护的版本(对应7.x),下载后解压就能用。
Windows版解压后,目录里有redis-server.exe和redis-cli.exe,双击或者命令行启动:
redis-server.exe redis.windows.conf这里最常见的两个坑:
坑一:启动瞬间闪退。绝大多数原因是配置文件里的日志路径或持久化目录在Windows下不存在。打开redis.windows.conf,搜dir和logfile这两个参数,把路径改成实际存在的目录,比如D:\redis\data。
坑二:每次都要手动启动。Windows服务器重启后,Redis进程不会自动拉起。要解决,给Redis注册成Windows服务:
# 以管理员身份运行 redis-server --service-install redis.windows.conf --service-name Redis6379 redis-server --service-start --service-name Redis6379之后Redis就会作为系统服务随Windows开机自启了。
2.3 用Docker快速跑Redis主从
Docker是现在最省事的部署方式,尤其是本地开发环境。一条命令起一个实例:
# 这里有坑,记得加 -v 挂载数据目录 docker run -d --name redis-single -p 6379:6379 -v /data/redis:/data redis:7.2 redis-server --appendonly yes注意--appendonly yes这个参数,它的作用是开启AOF持久化。不加的话容器一删、数据全没了,这在开发环境还能忍,在测试环境有时候也会造成不小的麻烦。数据卷挂载相当于是Redis的数据和容器解耦了,容器随便删,数据还在。
用Docker跑主从更省事。先跑一个主节点,再跑一个从节点用--replicaof指定主节点地址:
# 主节点 docker run -d --name redis-master -p 6379:6379 -v /data/redis-master:/data redis:7.2 redis-server --appendonly yes # 从节点,replicaof 指定主节点IP和端口 docker run -d --name redis-slave -p 6380:6379 --link redis-master -v /data/redis-slave:/data redis:7.2 redis-server --appendonly yes --replicaof redis-master 6379然后进从节点确认主从状态:
docker exec -it redis-slave redis-cli info replication输出里role:slave、master_link_status:up就说明主从正常。写数据到主节点,从节点会自动同步,这就是主从复制的最基本形态。
我个人的建议是:本地开发用Docker最方便(尤其Windows用户),生产环境还是走Linux物理机或云主机的编译安装,性能和可维护性都更好。不管是哪种方式,密码必须设置,哪怕只是内网环境。
# 配置文件里设置 requirepass yourstrongpassword设置密码后,客户端连接要带密码:
redis-cli -a yourstrongpassword2.4 可视化客户端怎么选
常年用命令行操作Redis的开发者可能觉得命令行就够了,但真正排查数据、查看key分布的时候,有个可视化客户端效率会高很多。讲一下我用过几款工具的实际体验。
热门的有官方的Redis Insight(前身是Redis Desktop Manager)、Another Redis Desktop Manager(简称ARDM)、以及TablePlus等。
Redis Insight:Redis官方出品,功能最全,支持数据浏览、命令行、慢日志分析、内存分析,甚至可以直接查键的内存占用。界面比老版RDM现代很多。缺点是有点重,启动慢,有些老电脑跑起来不太流畅。
Another Redis Desktop Manager(ARDM):Github开源免费,轻量、跨平台,连接和管理多个Redis实例很方便。我自己日常用这个,够用且不卡。支持树状key浏览、命令行、多标签,还有深色模式。
Redis Desktop Manager(RDM):老牌工具,曾经很流行,但现在官方版本开始收费了。如果你在网上找的是开源旧版,功能相对落后,不推荐新用户再入坑。一个需要注意的地方是,网上很多所谓“破解版”RDM来源不明,安全上不建议安装。
实际选型建议:
- 日常开发调试、连Linux服务器上的Redis:ARDM就够了,免费轻量。
- 需要做内存分析、慢日志排查、生产环境运维:Redis Insight,可视化维度更全。
- 只想快速看看key值:真的,命令行
redis-cli GET key可能比打开一个GUI工具更快。
3. 五种核心数据类型:每个命令背后都是一个真实需求
Redis为什么叫数据结构服务器(Data Structure Server),就是因为它不只是简单的key-value,每种数据类型背后都对应着一类常见业务场景。逐个拆解一下,重点讲每个数据类型怎么用、用在什么场景、有什么坑。
3.1 String:最简单的结构,最容易被用错
String是Redis里最基础的数据类型,value最大能存512MB。平时说的SET key value、GET key就是操作它。
常用命令:
SET key value # 设置值 GET key # 获取值 SETNX key value # 只有key不存在时才设置,常用于分布式锁 INCR key # 自增1,原子操作,常用于计数器 INCRBY key increment # 增加指定值 DECR key # 自减1 SETEX key seconds value # 设置值并指定过期时间实际业务里用得最多的场景:
计数器。比如文章的浏览量、商品的库存。直接用INCR就是原子的,在高并发下不会出错。我之前有个项目,用户签到计数就是用Redis里的INCR实现的,单机QPS支撑到每秒上万完全无压力。
但INCR有一个陷阱:如果value不是数字,它会报错ERR value is not an integer or out of range。如果在写入的时候没做好数据校验,脏数据进到Redis里,计数器就可能直接抛异常。所以在我用INCR之前,一定会先确认key的value是合法的整数。
另一个容易踩的坑是String的序列化。Java后端最常见的写法是把一个对象直接用JSON序列化成字符串存进去:
SET user:1001 {"name":"张三","age":30,"city":"北京"}这种用法本身没毛病,但要注意:对象的某个字段更新时,很多人会直接把整个对象重新SET一遍。如果这个对象有几个大字段(比如头像地址、历史记录),每次只改一个字段也要重写整串,浪费带宽和内存。这种场景就应该用下面的Hash类型。
3.2 Hash:存储对象的正确姿势
Hash是一种field-value映射表,适合存储对象。上面的用户信息案例,用Hash来表示:
HSET user:1001 name "张三" age 30 city "北京" # 修改某个字段时不需要重写整个对象 HSET user:1001 age 31 HGETALL user:1001常用命令:
HSET key field value # 设置单个字段 HMSET key f1 v1 f2 v2 # 批量设置字段 HGET key field # 获取单个字段 HGETALL key # 获取所有字段 HDEL key field # 删除字段 HINCRBY key field n # 对字段做自增Hash的优势在于字段级别的操作:当对象字段很多、更新又频繁时,Hash能把网络传输和内存占用控制在很小的范围。如果用户对象的字段数量不多(3-5个),String和Hash差别不大;字段多、更新频繁,Hash是明显更好的选择。
不过Hash也有需要注意的地方:HGETALL会把所有字段一次性全部取出来。如果字段特别多(上百个),每次HGETALL的网络传输成本就不小了。这时候可以拆分成多个小Hash,或者用HGET只取需要的字段。设计的时候就要对字段规模心里有数。
3.3 List:队列、栈、消息列表
List是一个双向链表,可以从头部或尾部插入、弹出数据。常用命令:
LPUSH key value # 从头部插入 RPUSH key value # 从尾部插入 LPOP key # 从头部弹出 RPOP key # 从尾部弹出 LRANGE key start stop # 获取指定范围元素 LLEN key # 获取长度用List实现的最典型场景是消息队列。生产者RPUSH到列表尾部,消费者LPOP从头部取:
# 生产者 RPUSH task:queue "job-1" # 消费者 LPOP task:queue但是经典的问题来了:如果用LPOP,消费者取完数据处理失败,消息就丢了。改进方案是用BRPOPLPUSH,从主队列弹出元素的同时,备份一份到另一个队列,处理成功后再从备份队列删除:
BRPOPLPUSH task:queue task:queue-backup 0这算是使用List做消息队列的进阶技巧。不过说实话,如果消息量很大、对可靠性要求高,还是建议用专业的消息队列(Kafka/RabbitMQ),Redis的List队列更适合轻量级、允许偶尔丢消息的场景。
还有一个常用场景是最新动态/最新列表,比如新闻资讯列表,用LPUSH把新内容插到头部,再用LRANGE key 0 9取出前10条,性能和实现都很简单。
3.4 Set与ZSet:去重、标签、排行榜
Set是无序去重的字符串集合,ZSet(有序集合)则额外给每个元素关联了一个score(分数),元素按照score从小到大排序。
Set常用命令:
SADD key value # 添加元素 SMEMBERS key # 获取所有元素 SISMEMBER key value # 判断元素是否存在 SINTER key1 key2 # 求交集 SUNION key1 key2 # 求并集 SDIFF key1 key2 # 求差集Set最典型的应用是标签体系。给用户打标签,给文章打标签,然后做交集、并集运算——比如“所有既喜欢篮球又喜欢足球的用户”可以直接用SINTER得到:
SADD user:1001:tags basketball football swimming SADD user:1002:tags basketball running SINTER user:1001:tags user:1002:tags # 得到 basketballSet的另一个经典场景是去重。比如统计一个活动的独立访客数,每个用户访问的时候SADD activity:uv userId,最终用SCARD得到集合大小,天然去重。
ZSet是在Set基础上多了一个score字段,常用命令:
ZADD key score member # 添加元素并指定分数 ZRANGE key start stop # 按排名取元素(升序) ZREVRANGE key start stop # 按排名取元素(降序,从高到低) ZINCRBY key increment member # 增加某元素的分数 ZSCORE key member # 获取某元素的分数ZSet最没争议的场景是排行榜。游戏里的战力排行、商城里的销量排行,都是它的主场。比如“直接排名前100”:
ZADD leaderboard 10000 "player_A" ZADD leaderboard 8000 "player_B" ZREVRANGE leaderboard 0 99 WITHSCORES还有一种用法容易被忽略:范围查询。ZSet的score是有序的,可以用ZRANGEBYSCORE key min max查询分数在某个区间的元素,比如查“最近一周活跃度在80到100分的用户”。比用Set遍历全量数据再在内存里过滤效率高太多了。
3.5 Redis序列化那些事
这个话题在热搜词里出现频率很高。之前已经提到了String里直接存JSON,这是最直观的序列化方式。但在实际项目(尤其是Spring Boot整合Redis)里,开发者经常遇到的序列化问题分为两类:
第一类:Redis服务端保存的格式问题。如果你用redis-cli直接GET一个用Java序列化(JdkSerializationRedisSerializer)写入的key,很可能看到的是类似以下这样的乱码:
"\xac\xed\x00\x05t\x00\x04name"这不是Redis出了问题,而是客户端写入时使用了Java对象序列化。Java序列化有两大问题:体积大(比JSON大很多)、跨语言读取不友好。除非系统只有Java一个语言,否则建议用JSON序列化器(GenericJackson2JsonRedisSerializer)或自定义的序列化方案。
第二类:key和value要不要都设置序列化器。Spring Data Redis里,如果只用默认配置,key会被加上奇怪的序列化前缀,导致用命令行找不到这个key。实操建议是:
- key用StringRedisSerializer,保证人类可读,方便排查。
- value用Jackson序列化器,存JSON,兼顾跨语言和可调试性。
我在项目里踩过一个具体坑:团队的微服务是Java和Go混用的,Java写入Redis时用了JdkSerializationRedisSerializer,Go这边读出来全是乱码,排查了很久才发现是序列化方案不一致。后来统一改成JSON,问题立刻消失。跨语言场景下,序列化方案必须在架构设计阶段就统一。
4. 三个高频难点:分布式锁、持久化、缓存穿透击穿雪崩
4.1 分布式锁的正确姿势:从SET NX到Redisson
分布式锁是Redis面试里的常客,也是业务系统里最常见的需求之一。为什么需要分布式锁?举个简单例子:多个实例同时对一个共享资源做操作,比如同一个订单的库存扣减,如果不加锁就会出现超卖。单机时代用synchronized就够了,分布式环境下多个进程之间需要一把公共的锁,Redis能提供这个能力。
早期的实现方式是用SETNX(SET if Not eXists):
SETNX lock:order:1001 1 # 返回1表示拿到锁,返回0表示锁已被别人持有用完锁后删除:
DEL lock:order:1001但这里有两个致命问题:
问题一:锁没有过期时间。如果一个线程拿到锁后崩溃了,忘记DEL,这把锁就永远不释放,所有其他线程都会卡死。解决办法是加EXPIRE,即设置过期时间(也叫TTL,Time To Live)。
问题二:SETNX和EXPIRE不是原子的。如果SETNX成功但EXPIRE还没来得及执行,进程就崩溃了,锁同样永远不释放。好在后来Redis官方推出了一个原子命令来解决这个问题:
SET lock:order:1001 1 NX EX 30NX表示不存在时才设置,EX 30表示30秒过期。这一条命令同时完成了加锁和设置过期时间,是分布式锁最基础也是最关键的改进。
但即便是这样,还有一个经典问题:线程A拿到锁后执行时间超过了30秒,锁自动过期被线程B拿到,此时线程A执行完了再去DEL,把B的锁删掉了。这个问题被称为“误删锁”。解决办法是:删除锁之前先检查value是否是自己写入的,也就是给锁加上一个唯一标识:
# 加锁 SET lock:order:1001 uuid-xxx NX EX 30 # 解锁(先比较value再删除,注意这两条要保证原子性) if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end用Lua脚本让“比较+删除”变成一个原子操作,避免误删。
再进一步,30秒过期时间本身也是个痛点:业务执行时间不可控,锁过期了怎么办?成熟的方案是看门狗机制,也就是Redisson中实现的:加锁后启动一个定时任务,在锁快过期时自动续期。这样即使业务执行时间很长,锁也能一直持有直到业务结束。
如果需要快速落地,直接引入Redisson(Java生态)是最省心的方式:
RLock lock = redissonClient.getLock("order:" + orderId); try { // 尝试加锁,最多等待10秒,锁自动过期30秒 if (lock.tryLock(10, 30, TimeUnit.SECONDS)) { // 业务逻辑 } } finally { lock.unlock(); }Redisson封装了上述所有细节,包括原子加锁、看门狗续期、Lua脚本来解锁,是我在实际项目里最推荐的方式。在C++/Go/Python等语言中,也可以用SET NX EX加锁、Lua脚本解锁来自己实现,但一定要把原子性这一整套逻辑都做完整。
4.2 RDB与AOF:持久化怎么选才不丢数据
Redis是内存数据库,一旦进程退出、机器重启,内存里的数据就全没了。要防止数据丢失,就得靠持久化把数据从内存写到磁盘。Redis提供了两种持久化方式:RDB(快照)和AOF(追加日志)。搞清楚这两种方式的原理和取舍,是Redis运维的基本功。
RDB(Redis DataBase):把内存中的全部数据生成一份快照写入磁盘。默认会生成dump.rdb文件。RDB的触发方式有两种:手动执行SAVE或BGSAVE,以及通过配置文件自动触发:
save 900 1 # 900秒内至少有1个key变化,就执行一次快照 save 300 10 # 300秒内至少有10个key变化 save 60 10000 # 60秒内至少有10000个key变化RDB的优势是文件小、恢复快,适合做备份和灾难恢复。缺点是会丢数据:假设你每900秒执行一次快照,在这900秒内写入的数据如果Redis崩溃,这部分就丢了。
AOF(Append Only File):把每一次写操作以日志的形式追加到文件中。重启时只要重放一遍日志,数据就能完整恢复。AOF的配置:
appendonly yes appendfsync everysec # 每秒刷一次磁盘,默认值 # 还有一个选项是 always(每次写都立即刷盘,最安全但最慢)AOF的优势是丢数据少(最多丢1秒的数据),缺点是文件越来越大,恢复速度比RDB慢。但好在Redis支持AOF重写(BGREWRITEAOF),可以定期把AOF文件合并压缩。
在实际生产环境,我的建议是两者都开:
- RDB做日常备份,快速恢复。
- AOF保证数据不丢。
如果是对数据丢失零容忍的场景(比如金融交易),AOF的刷盘策略要设为always,但代价是性能明显下降,每笔写操作都要刷盘。这个取舍得结合业务情况来判断。
还有一个细节:Redis 4.0之后支持混合持久化(默认开启),AOF重写时会同时包含RDB快照内容和增量日志,结合了两者的优点,恢复速度和数据完整性都更好。
4.3 缓存三大坑:穿透、击穿、雪崩及治理方案
这三个是Redis面试几乎必考的概念,同时也是线上事故的高发原因。
缓存穿透:查询一个根本不存在的数据。比如查用户ID为-1的记录,缓存里没有,数据库里也没有,每次请求都打到数据库,数据库压力瞬间飙升。攻击者可以故意构造大量不存在的key来打垮你的数据库。
解决方法:
- 缓存空值:查不到数据时,在Redis里缓存一个空值并设置较短的过期时间(比如60秒),下次同样请求直接返回空。
- 布隆过滤器:把所有合法ID提前加到布隆过滤器里,查询前先过一遍过滤器,不存在的直接拦截掉,根本不会查数据库。布隆过滤器可能会误判(说某个key存在其实不存在),但不会漏判(不存在的key一定不会说存在),在大多数场景下误判是可以接受的。
缓存击穿:某个key在缓存过期的瞬间,有大量并发请求同时访问该key。比如一个热点新闻的详情数据在Redis里过期了,同一秒内有10万个请求过来,缓存没命中,全部打到数据库。数据库瞬间被击穿。
解决方法是互斥锁:当缓存过期时,只允许一个线程去重建缓存(查数据库),其他线程阻塞等待,等缓存重建好了再来读。
流程大致是:
请求来了 -> 查缓存 -> 未命中 -> 尝试获取分布式锁 -> 拿到锁的线程查数据库、写缓存、释放锁 -> 没拿到锁的线程等待一段时间后重新查缓存(此时大概率已重建完成)缓存雪崩:大量key在同一时间集体过期,或者Redis服务本身宕机,导致所有请求全部打到数据库。和缓存击穿的区别在于:击穿是单个热点key,雪崩是一大批key。
解决思路:
- 过期时间加随机值:不要让大量key的过期时间都精确到同一秒,可以在基础过期时间上加上一个随机数(比如3-5分钟)。
- Redis高可用:部署主从+哨兵或Redis Cluster,避免单点故障。
- 服务降级:Redis不可用时,接口返回本地缓存的旧数据或直接限流。
从我实际经历来看,缓存击穿和雪崩是线上最常见的事故场景,尤其是做电商促销活动的时候——热点数据集中、过期时间又容易设置成同一个整点,零点一过,缓存集体失效,数据库就直接被冲垮了。这是绝对要提前预防的。
5. 从单机到集群:主从复制、哨兵与Cluster
单机Redis再快也有上限,内存和CPU都是瓶颈,更重要的是单点故障问题:机器挂了,整个缓存层就瘫痪了。解决方式是集群化部署,而集群化部署有三种演进形态:主从复制、哨兵、Cluster集群。
5.1 主从复制原理与断线重连
主从复制是Redis高可用最基础的一层。一个主节点(master)负责写,一个或多个从节点(slave)负责读,从节点通过复制主节点的数据来保持同步。
核心原理可以归纳为三个步骤:
- 建立连接:从节点向主节点发起同步请求(
PSYNC)。 - 全量复制:如果是第一次同步,主节点执行
BGSAVE生成RDB快照发给从节点,从节点加载这份快照。 - 增量复制:在第一次同步之后,主节点把后续的写操作通过命令传播发送给从节点,从节点执行同样的命令来保持数据一致。
主从复制提供了两个核心价值:
- 读写分离:读操作打到从节点,减轻主节点压力。
- 数据备份:从节点作为主节点数据的冗余备份。
有一个经典问题需要考虑:主从复制的数据是异步的。主节点写入成功就返回给客户端,但从节点复制是异步进行的。如果主节点写完马上宕机,从节点可能还没来得及同步最新数据,这部分数据就丢了。对这个问题的容忍度,决定了你选哪种部署方案。
断线重连:如果主从之间的网络发生抖动,从节点断开了一段时间又恢复了,Redis不会重新全量复制,而是通过PSYNC带上自己的复制偏移量(offset),主节点只需要把断线期间的增量命令发给从节点就行。这比每次断线都全量复制高效得多。
但有个坑要注意:如果断线时间过长,主节点的复制积压缓冲区(backlog)不够大,导致offset对应的命令已经被覆盖,主节点就只能退化成全量复制。解决方式是调大repl-backlog-size,默认1MB,在高写入量场景建议调到64MB或更高。
5.2 哨兵机制:自动故障转移怎么实现
主从复制解决了读压力和备份问题,但有一个隐患:如果主节点挂了,从节点不会自动变成主节点,整个写服务就停了。手动切换主从虽然可行,但需要人工介入,而且停机时间不可控。哨兵(Sentinel)就是来解决这个问题的。
哨兵是一个独立的Redis进程,它负责监控所有Redis节点的状态。配置一套哨兵集群(通常3个以上,避免哨兵自身成为单点),当主节点不可达时,哨兵会自动做如下操作:
- 检测主节点下线(主观下线 → 客观下线,需要多个哨兵达成一致)。
- 从所有从节点中选出一个,执行
SLAVEOF NO ONE,把它提升为新的主节点。 - 把其他从节点的主节点地址改成新的主节点。
- 通知客户端新的主节点地址。
哨兵配置的核心项:
sentinel monitor mymaster 127.0.0.1 6379 2 # 2表示至少2个哨兵同意主节点下线,才判定为主观下线后升级为客观下线 sentinel down-after-milliseconds mymaster 5000 # 5秒内无法连接主节点,则判定为下线 sentinel failover-timeout mymaster 15000 # 15秒内完成故障转移三个哨兵的部署方式:在三个不同机器上各启动一个哨兵进程,监控同一个主节点。这样即使一个哨兵挂了,剩下的哨兵仍能达成共识,不会出现脑裂。
我对哨兵的实际体验是:整体方案已经足够成熟,但要注意客户端对故障转移的感知。故障转移完成后,客户端如果还连着旧的主节点地址,就写不进去了。所以客户端要配置哨兵地址列表,而不是直接配主节点地址。在Java里用Spring Data Redis或Redisson时,配置sentinel masterId和readFrom等参数,就能自动感知主机变化。
5.3 集群模式:数据分片与节点通信
当单机内存不足以支撑数据量,或者写请求并发度超过单节点能力时,可以使用Redis Cluster做水平扩展。
Cluster的核心设计是数据分片:整个keyspace被划分为16384个哈希槽(slot),每个节点负责一部分槽。写入时对key做CRC16校验,再对16384取模,得到对应的槽,然后找到负责该槽的节点:
CRC16(key) % 16384 = 槽号假设三个节点:节点A负责0-5460槽,节点B负责5461-10922,节点C负责10923-16383。每个key只会落在其中一个节点上。
这样设计带来两个优势:
- 数据均匀分散:不同key分布在不同的节点上,单节点压力下降。
- 横向扩展方便:增加节点时,只需要把部分槽迁移到新节点即可。
集群模式下,客户端(比如Jedis、Lettuce)不用自己算槽号,它会向任意节点发送命令,如果key不在该节点,节点会返回MOVED错误,客户端跟进这个错误重定向到正确的节点。这也是为什么集群模式要求客户端库必须要支持重定向机制。
Cluster的坑有几个,我逐个提一下:
坑一:不支持多key操作。因为不同的key可能在不同的节点上,MSET、MGET、交集并集这些操作跨节点执行不了。Redis Cluster提供了hash tag机制来解决:如果两个key里包含相同的{...}部分,比如{user:1001}.name和{user:1001}.age,它们会被分配到同一个槽里,就可以执行多key操作了。
坑二:从节点不提供写服务。Cluster模式下,默认只有主节点提供读写。从节点只是作为备份节点,可以在主节点故障时通过CLUSTER FAILOVER提升为主节点。如果你的读写比例较高想要做读写分离,需要在客户端配READONLY命令,但整体配置复杂度会上升。
坑三:故障转移的粒度是槽而不是节点。某个主节点挂了,它负责的槽会迁移到从节点。如果在故障转移过程中客户端访问这些槽的key,可能短暂返回CLUSTERDOWN错误,需要在业务层做重试。
还有一个常见的疑问:Redis Cluster和哨兵该选哪个?我的判断是:
- 数据量不大(几十GB以内)、主要解决高可用问题:哨兵模式足够。
- 数据量大(数百GB以上)、需要水平扩展:Cluster更合适。
如果从零开始搭新项目,我倾向于直接上Cluster,虽然配置和维护成本高一些,但不用在数据量增长到瓶颈时再迁移,一次到位。
6. 那些面试里绕不开的Redis题
每次面试Redis相关岗位,问来问去基本就是那几个点。这些题目看似简单,但想答得出彩,关键不是背答案,而是理解背后的原理。
6.1 为什么Redis这么快
这个问题几乎必问。回答应该从几个维度展开:
- 纯内存操作:Redis的数据都存在内存里,读写内存的速度是纳秒~微秒级别,而磁盘随机读写是毫秒级别,差了至少一个数量级。
- 单线程模型:Redis的IO读写和命令执行是单线程的,避免了多线程的上下文切换和竞争问题。单线程的好处是简单、无锁,在内存场景下性能反而更高。
- IO多路复用:Redis使用epoll(Linux下的事件驱动机制)同时监听大量客户端连接,当某个连接有数据可读时才去处理,而不是每个连接分配一个线程阻塞等待。
- 高效的数据结构:Redis的每种数据类型都有专门的高效实现,比如哈希表的扩容、跳表的查找,都是经过优化的。
不过要注意:Redis 6.0之后引入了一些多线程IO能力(用于网络读写),但命令执行仍然是单线程的。这个细节答出来能显得你关注版本演进。
6.2 Redis是单线程还是多线程
这是一个特别容易答错或者答一半的题目。
准确说法是:命令执行是单线程的,但Redis整体不只一个线程。Redis内部还有其他线程负责文件事件处理、AOF持久化、后台RDB快照、异步删除等。Redis 6.0之后,网络IO也支持多线程处理(默认关闭),可以显著提升多核CPU下的吞吐量。
单线程执行命令的核心原因是为了绝对的原子性——所有命令在Redis内部都是原子执行的,不存在数据竞争问题。这也是Redis能做分布式锁的基础之一。
如果面试官问“为什么单线程还能跑那么快”,回到6.1的答案:因为瓶颈不在CPU,而在内存和网络。
6.3 常见面试问答速查表
整理了一些常被问到的问题和简答,方便快速复习:
| 问题 | 核心答案要点 |
|---|---|
| 缓存穿透、击穿、雪崩是什么 | 见4.3,记住三者的区别:穿透查不存在数据,击穿是热点key过期,雪崩是大量key同时过期 |
| Redis过期key是怎么删除的 | 惰性删除(访问时发现过期就删)+ 定期删除(每100ms抽查部分key) |
| Redis内存淘汰策略有哪些 | noeviction(默认,不淘汰直接报错)、allkeys-lru、volatile-lru、allkeys-random等,具体看场景选 |
| Redis为什么快 | 内存操作 + 单线程无竞争 + IO多路复用 + 高效数据结构 |
| 主从复制和哨兵的区别 | 主从做冗余备份和读写分离,哨兵在主从基础上增加自动故障转移 |
| Redis Cluster的槽是怎么分布的 | 16384个槽,CRC16(key)取模分配 |
| 持久化RDB和AOF怎么选 | RDB恢复快丢数据多,AOF丢数据少文件大恢复慢,生产环境两者都开 |
6.4 Lua脚本与原子性
Lua脚本在Redis里是一个很重要的进阶话题,因为它能解决很多“多条命令需要原子执行”的问题。
上一篇讲分布式锁时提到的“先比较再删除”就是用Lua实现的。Lua脚本在这里的核心价值是:Redis会把整个Lua脚本作为一个整体原子执行,执行过程中不会插入其他命令。
也就是说,一批命令如果通过Lua脚本提交,Redis保证这批命令要么全部执行,要么在遇到错误时全部不执行,不会有中间状态被其他客户端看到。这在业务中非常有用。
举一个实际场景:库存扣减。需求是“库存充足时扣减,不足时返回失败”。用原生Redis命令,你得先GET库存,再判断,再DECR,中间如果并发进来一个请求,就可能出现超卖。用Lua脚本就安全了:
-- KEYS[1] 是库存key -- ARGV[1] 是扣减数量 local stock = tonumber(redis.call('GET', KEYS[1])) local num = tonumber(ARGV[1]) if stock >= num then return redis.call('DECRBY', KEYS[1], num) else return -1 end这段脚本从获取库存、判断、扣减都在Redis服务端一次完成,天然是原子的,不存在竞态条件。用Java的RedisTemplate执行非常简单:
DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class); Long result = redisTemplate.execute(script, Collections.singletonList("stock:1001"), 1);Lua脚本还可以用于:
- 批量设置多个key并检查所有key是否存在。
- 限流(固定窗口或令牌桶算法)。
- 分布式锁的续期操作。
但注意,Lua脚本也不能滥用。如果一个脚本特别长(比如超过几百行),会阻塞Redis主线程,其他命令全部排队等待。脚本执行时间过长,会显著影响Redis的吞吐量。线上必须监控慢脚本,一旦发现就优化拆分。
7. 踩坑记录与线上排查技巧
7.1 日志怎么看:从日志中发现异常
Redis自身日志的位置由配置文件里的logfile参数控制,默认输出到stdout,如果配了日志路径,就在指定文件里。排查问题时,日志是最直接的信息来源。
常见日志条目和含义:
# 主从断连 Connection with replica lost. # 意味着主从网络发生波动,需要检查网络、调整repl-timeout # RDB快照失败 Failed opening the RDB file for saving: Permission denied # 磁盘权限问题,检查Redis进程对数据目录的写权限 # 客户端连接过多 Max number of clients reached # 超过maxclients限制了,可能客户端连接泄漏,也可能是maxclients设置太小 # 内存接近上限 Memory used is close to the configured maxmemory # 即将触发内存淘汰策略,需要关注内存增长趋势实操排查思路可以这样走:
- 先看
INFO memory,确认内存使用量是否接近maxmemory。 - 再看
INFO clients,确认连接数是否异常。 - 然后看
INFO persistence,确认最近一次RDB/BGSAVE是否成功。 - 最后打开日志文件,搜索
error、fail、lost等关键字。
一条我自己的习惯:每天定时跑一次redis-cli --bigkeys,扫一下有哪些key占用的内存特别大,未雨绸缪比出事了再排查要轻松得多。
7.2 线上排查三件套:INFO、SLOWLOG、MONITOR
排查Redis线上问题,有三个命令是标配工具:
INFO:一站式查看Redis的运行状态。
INFO server # 服务端信息 INFO clients # 客户端连接情况 INFO memory # 内存使用详情 INFO stats # 统计信息,包括命令执行次数、命中率等 INFO replication # 主从复制状态其中INFO stats里的keyspace_hits和keyspace_misses能算出缓存命中率。命中率低,说明很多查询走到了数据库,缓存策略可能有问题。这个数值在性能优化的时候非常有参考价值。
SLOWLOG:查询慢命令日志。Redis默认会把执行时间超过10毫秒的命令记录为慢命令:
SLOWLOG GET 10实际线上问题里,慢命令通常集中在以下几种情况:
KEYS *这种全量扫描命令,会阻塞主线程。HGETALL一个特别大的Hash。ZRANGE一个数据量极大的ZSet。- 复杂的Lua脚本。
如果慢日志频繁出现,优先排查是不是业务上用了这些命令。KEYS *在线上绝对不能出现,要用SCAN命令做分批扫描替代。
MONITOR:实时打印所有命令(调试用,生产别开太久)。
MONITOR它会把所有命令实时输出,能看到每个客户端在执行什么操作。调试连接泄漏、定位某个key被谁修改等问题时很有用。但生产环境开MONITOR会降低Redis性能,谨慎使用。
7.3 基于热搜词补充:缓存治理的落地清单
“缓存治理”这个词这几年在运维圈里越来越常见。所谓的缓存治理,不是单纯把数据塞进Redis,而是围绕Redis做一整套规范和管理。做个清单供参考:
- 容量管理:为每个Redis实例设定合理的
maxmemory,并选择合适的淘汰策略。常用的是allkeys-lru(对所有key做LRU淘汰),适合缓存场景。 - key规范:统一key的命名格式,推荐用
业务:模块:唯一标识的方式,比如order:detail:1001。命令空间清晰,排查问题才快。 - 过期时间规划:缓存数据必须设置合理的TTL。比如用户详情缓存30分钟,排行榜缓存5分钟。无TTL的key是内存泄漏的温床。
- 热点数据治理:对访问量极大的热点key,可以考虑做本地缓存(Caffeine)加Redis分级缓存,减少Redis集中压力。
- 异常监控:通过哨兵、Prometheus等工具监控Redis的CPU、内存、连接数、慢日志、命中率等指标,做到异常提前发现。
这套清单不一定全盘照搬,但核心思想就是:Redis不是把数据丢进去就完事,后续的容量、性能、安全都要管理起来,这才是“缓存治理”的真正含义。
8. 最后分享一点个人经验
写到这里,再分享一点个人经验吧。Redis这个技术栈,从入门到真正熟练,我经历了好几个阶段:最开始只会SET、GET,后来学会了数据类型和命令,再后来开始处理持久化、主从、哨兵、集群,最后才是分布式锁、Lua、缓存治理这些进阶玩法。每一步都踩过坑,也都在解决这些坑之后对它的理解变得更深入。
如果让我给刚开始学Redis的朋友一个建议,那就是:一定要带场景去学,而不是背命令。看到一个数据类型,先想想它能解决什么问题;看到一个方案,先想想它的使用边界在哪里。比如主从复制能解决读压力的瓶颈,但数据是最终一致的,如果你在业务里读取刚刚写入的数据(比如下单后立刻查库存),就要考虑读不到最新数据的情况,这时候要么读主节点,要么接受短暂不一致。
Redis本身的命令并不复杂,难的是在真实工程结构中做出正确的选型和取舍。希望这篇内容能帮你少走一些弯路。如果你在实操中遇到了意外情况,比如主从同步卡住、Cluster迁移失败、AOF文件损坏恢复不了,欢迎带着现场状态来交流,真实问题的排查过程比任何教程都更能让人成长。