看到这个标题,估计不少人第一反应是"版本列表有什么好整理的,官网看Release Notes不就完了"。但真正接手过老系统的人懂,Redis从2.6到7.0这十年,不只是功能堆叠,而是一套缓存系统的设计思路在迭代。很多你现在用得顺手的东西——混合持久化、IO多线程、ACL,甚至Stream,都是一步一步长出来的。
这篇文章不打算做成Release Notes的翻译版,而是把这些大版本的"关键跃迁点"拆开讲:每个版本解决了什么核心问题、引入它的时候为了什么、以及你从老版本往上爬的时候最容易在哪里翻车。适合正在做版本选型、准备做Redis升级、或者帮人排查诡异缓存故障的开发者参考。
1. 为什么值得把2.6到7.0的老底翻一遍
1.1 版本演进不是"升个级",而是一张决策地图
因为你要选择"用哪个版本"或者"升级途经哪些版本"的时候,真正要评估的其实是一个链条:你的客户端库支持什么协议,你的运维脚本解析哪些INFO字段,你的数据结构编码在哪个版本变了,你的持久化文件在哪个版本换了格式。
这些问题的答案散落在各个版本的Release Notes里,但它们之间存在明确的因果关系。比如说,没有4.0的PSYNC2,就没有后续5.0、6.0在复制稳定性上的改进;没有6.0的多线程IO,就很难支撑高并发读写下网卡吞吐的瓶颈。看版本演进真正看的是这条因果链。
尤其是当你面对一个存量很大的老系统,比如还在Redis 3.2上的业务,你可能以为直接装一个7.0的二进制重启就行。但数据文件格式、主从复制协议、AOF重写机制都已经变了。理解了版本之间为什么这么改,你才敢做升级决策,而不是拿生产环境去赌。
1.2 梳理之前,先想清楚要从版本整理里得到什么
拿到一份版本清单,先分三类去过滤信息:第一类,能力型更新,比如Lua脚本、集群、Stream、函数,这些决定你能做什么新功能;第二类,机制型更新,比如持久化、复制、淘汰策略、线程模型,这些决定架构稳不稳;第三类,兼容性变化,比如ACL、命令语义、INFO字段,这些决定你升级之后会不会被暗坑绊倒。
我自己做版本整理的时候,习惯是把"能力型"和"机制型"的变化做成一张演进图,把"兼容性变化"做成一份排查清单。前者用来做技术规划,后者用来做升级护身符。这篇文章的后面几节,基本就是按这个思路展开的。
2. 逐版本拆解:六个版本各自解锁了什么能力
2.1 Redis 2.6:Lua脚本和客户端命令的第一次扩充
2.6这个版本在Redis历史上最大的贡献是引入了服务端Lua脚本,也就是EVAL和EVALSHA。在那之前,如果我想把"判断-扣减-写回"这一串逻辑做成原子的,只能靠MULTI/EXEC加WATCH,用乐观锁的方式反复重试。Lua脚本一出来,直接把这段逻辑塞进Redis进程里执行,整个操作天然原子,还省了网络往返。
另外一个值得记住的是2.6.12,它给SET命令加了NX、EX、PX这些选项。今天大家都习以为常的"SET lock:key 1 EX 10 NX"这种分布式锁写法,就是这个版本开始变成官方姿势的。在这之前,加锁要SETNX加EXPIRE两条命令,中间隔着窗口期,分布式锁很容易锁死。
还有一个容易忽略的点:2.6把服务端对脚本执行的随机性做了约束——写脚本时不能依赖隐式的随机行为,这是为了后续主从复制和AOF持久化时,脚本在重放时产生一致结果。这个设计思路直接影响到4.0之后的持久化。所以如果有人在做Lua脚本相关的问题排查,2.6的知识到今天依然有用。
2.2 Redis 2.8:Sentinel诞生,复制第一次能"部分重来"
2.8最大的事是Sentinel正式进入Redis发行体系。在运维视角上,这是Redis第一次有了原生的"故障发现+自动切换"能力。Sentinel做的事情说白了就是三件:监控主从状态、自动把从库提升为主库、通知客户端新的主库地址。
跟它一起出现的还有部分重同步(PSYNC)。在主从复制的场景里,断线是家常便饭。老版本的逻辑很粗放:只要主从断开,重连之后就全量同步一把。全量同步意味着要生成RDB快照,传文件,再加载,稍微大一点的实例就会把带宽吃满。PSYNC的思路是让主从都维护一个复制积压缓冲区,断线期间的数据如果还在积压区里,重连之后只需要把缺失的增量补上,不用再全量同步。
另外2.8还带来了SCAN命令。之前想遍历key只能走KEYS,KEYS会把整个Redis卡死在大key匹配上。SCAN用游标方式每次返回一小批key,虽然中间可能有重复或遗漏,但对生产环境友好得多。这个版本算是把"高可用"和"日常运维不踩坑"两个方向都往前推了一大步。
2.3 Redis 3.0:官方集群上线,SDS底层重做
3.0的核心是Redis Cluster。在这之前,业务量大了通常只能搞主从加客户端分片,比如客户端根据hash把数据分散到多组Redis上,路由规则自己维护。集群版本解决了这两个问题:数据通过哈希槽(16384个槽)分散到多个主节点,客户端通过CLUSTER SLOTS拿路由信息,key所属槽被迁移时,Redis会用MOVED重定向告诉客户端去哪个节点访问。
这个版本的槽位分配思路后来被很多分布式系统借鉴。它的好处是简单:key经过CRC16取模,映射到固定16384个槽,槽均匀分到节点上,扩容就迁移槽,缩容就迁走槽。缺点也很明显:不支持多key跨节点事务,本地多key操作只在同槽时可用,所以大量需要复杂关联查询的业务要做"槽位对齐"设计。
另外3.0重做了字符串的底层数据结构SDS。老版本的SDS在append等操作里容易出现分配膨胀的问题,新SDS在内存分配策略和二进制安全上做了不少优化。这些底层的改动不直接在功能层体现,但后来很多大value场景的性能改善都跟它有关。
2.4 Redis 3.2:地理信息命令与quicklist
3.2最醒目的新功能是GEO命令族:GEOADD、GEORADIUS、GEOPOS、GEODIST这一堆。做附近的人、门店点位计算,不需要再拿经纬度到外部引擎算距离了,Redis直接基于地理位置集合算半径内成员。它的底层是把经纬度编码到sorted set的score里,用geohash思想实现的。
3.2还有一个对列表结构的底层重构:quicklist。在它之前,列表对象要么用双向链表,要么用压缩列表,双向链表的每个节点都独立分配内存,压缩列表又对长度很敏感。quicklist的做法是用双向链表把多个压缩列表节点串起来,既能省内存,又不让大列表的操作变成O(n)灾难。这个改动普通开发者平时感知不到,但如果你用Redis存过消息列表、任务队列这类长时间增长的数据,3.2之后内存增长曲线会平缓很多。
还有个值得提的是BITFIELD,它让你把key里的一整块二进制按位段做自增、截断、设置。像简单计数器、在线状态位图、权限位图这类场景,用BITFIELD能省下大量内存。这个命令也是从3.2开始进入稳定API的。
2.5 Redis 4.0:模块系统、混合持久化、PSYNC2
4.0被很多人称为"二次起飞"是有道理的。第一个大事件是模块系统:允许用C语言写扩展,把自定义的数据结构、命令挂进Redis进程里。搜索、布隆过滤器、时间序列这些能力,很多都是靠模块实现的。第二个大事件是混合持久化:开启aof-use-rdb-preamble之后,AOF文件头部直接放一份RDB格式的历史快照,后面再追加增量命令。
RDB的加载速度远快于逐条执行AOF,所以混合持久化让重启恢复时间大幅缩短。这个改动对大数据量实例是质变级别的。之前Redis重启一次,哪怕只是加载1GB的AOF,都要慢慢重放好久,混合之后快很多。
4.0还给了两个日常用起来很爽的命令:UNLINK和FLUSHALL ASYNC。UNLINK就是异步删除,删除大key时不再阻塞主线程。老版本删一个几百万成员的集合,DEL会在执行期间卡住其他命令,听说是"删个key,服务抖几秒"。FLUSHALL ASYNC同理,清库不阻塞。
复制方面,4.0把PSYNC重写为PSYNC2:从库重启后,只要复制积压缓冲区的上下文还在,还能继续部分重同步,不需要全量。之前只要从库重启就很可能触发全量同步,那个滋味,经历过的人都懂。
持久化这里我建议你配一个经验值:如果你目前还在3.x老版本,直接切4.0的混合持久化会看到RDB文件格式变化,旧版本Redis读不了新格式文件。所以一旦决定用4.0以上的版本,回退老版本前必须留好旧格式的备份,别以为redis-check-rdb能解决一切。
2.6 Redis 5.0:Stream进入标准命令集
5.0的C位是Stream数据结构。它是个日志消息队列,既能做消息的追加和读取,也支持消费组(consumer group)、ACK确认、pending队列。跟PUB/SUB相比,Stream的消息不会丢,消费者挂了可以重新读取;跟外部MQ相比,它又轻量得多。
Stream的id是"时间戳-序号"结构,所以天然按时间排序,你可以通过范围查询只取某个时间段的消息。XADD、XREAD、XGROUP、XACK都是这波出来的。如果你的业务对消息可靠性有要求,又不想额外引入一堆中间件,5.0之后确实可以认真考虑用Stream顶一阵。
5.0还加强了集群运维能力,比如CLUSTER FAILOVER支持在从库上主动发起切换而不用连到主库操作,以及从节点可以做只读扩展。开发侧也补了ZPOPMIN、ZPOPMAX这些有序集合的弹出命令,原本要"ZREVRANGE取一个再ZREM删一个"的两步操作终于能一步完成了。
2.7 Redis 6.0:多线程IO、RESP3、ACL、TLS
6.0这版信息量非常大,我按四个维度拆。
第一,多线程IO。注意这里的多线程只管网络读写,命令的实际执行仍然是单线程。这么做的原因很现实:Redis的性能瓶颈往往不在CPU执行命令,而在网络收发数据的系统调用。引入IO线程之后,读写socket可以并行处理,吞吐量明显提升。配置参数是io-threads,默认不开启,需要按机器核数手动打开。别一次开太多,官方都建议别超过8个,多了反而上下文切换开销更大。
第二,RESP3协议。新版协议支持推送消息、流式聚合响应,最典型的是HELLO命令做协议版本切换。但默认还是RESP2,老客户端完全不受影响。
第三,ACL访问控制列表。在6.0之前,权限控制基本就是requirepass一把密码。ACL允许你给不同用户配置独立的密码、可执行的命令白名单、可访问的key范围。对多业务共用一套Redis的场景,它的价值是质变级的。
第四,TLS支持。从这版开始,官方支持加密传输,虽然配置起来麻烦,但在安全合规要求高的环境里,这是唯一不需要靠外部代理就能搞定加密的路线。
6.0后面还有一个小版本6.2,补充了很多命令语法糖:GETDEL、COPY、SMISMEMBER、ZRANGESTORE,LPOP/RPOP也能指定count了。所以你现在从5.x升到6.2,到手的是一堆"原本要几步操作现在一条命令搞定"的快乐。
2.8 Redis 7.0:ACL V2、函数命令、AOF多部分文件
7.0延续了6.0的安全和持久化思路,但做了更多底层重构。
ACL升级成V2,支持更细粒度的key权限(比如只允许读写某个前缀的key)、频道权限(控制PUB/SUB订阅范围)、以及命令选择器。这套东西让"同一个Redis里安全隔离多个租户"变可行了。
Redis Functions是7.0对Lua脚本的一次收编。之前用EVAL分发脚本,脚本散落在客户端代码里,管理成本高。Functions允许你把函数以注册方式装进Redis服务端,调用时只传函数名和参数。它还能像存储过程一样设置可写策略,错误处理也更规范。
持久化这边,7.0把AOF改成了多部分文件:一个基础文件(base)加多个增量文件(incr),通过manifest清单管理。这解释了为什么升级到7.0之后,你可能会看到appendonly目录里不再是单个appendonly.aof,而是一组文件。备份脚本如果用固定的aof文件名,升级后立刻就会踩坑。
调优层面,7.0继续推动listpack替代部分场景的ziplist,hash和zset在数据量较小时会选用更紧凑的编码格式。跟4.0的PSYNC2一样,这些底层细节不体现在功能列表里,但直接影响大规模场景下的内存和延迟表现。
3. 三条横跨版本的主线:线程模型、持久化、高可用
3.1 线程模型:从单线程执行到IO多线程,核心执行逻辑没有变
Redis"单线程"这个标签被记了很多年,但这个话题在不同版本里指的东西不一样。2.6时代,网络读写、命令执行、数据持久化全在同一个主线程里转,好处是简单,没有竞态条件,坏处是任何一个慢操作都会拖住全局。
到了6.0,官方引入了IO多线程,让"读socket、解析缓冲区、写回socket"这些IO相关工作分流到多个IO线程。但命令执行本身还是那个单线程,所以不需要加锁,数据一致性模型没变,开发者写Lua脚本或者使用pipeline的思维方式也没变。这点理解错了很容易误判性能瓶颈。
从2.6到7.0还有一个隐藏的优化:内存分配和多线程后台任务。4.0开始逐步把删除、flush这类重操作变成后台异步执行;7.0继续细化异步线程池。你能看到的直观收益是,删除超大集合时不再"卡命令",某些fork快照引起的延迟毛刺也在逐年改善。
3.2 持久化:RDB、AOF、混合,再到7.0的多部分
持久化这条线是最需要认真研究的。最早的RDB是定期全量快照,恢复快但数据丢失窗口大。AOF是追加命令日志,能精确恢复但文件大、加载慢。Redis很长时间是让你两选一,或者同时开,但各有各的痛。
4.0的混合持久化把两者的优点做了折中:AOF文件的开头是RDB预加载数据,之后才是增量命令。加载时先秒级载入快照再执行少量命令,速度和完整性都有。7.0的多部分AOF则是对AOF重写机制的重构:基础文件保存重写时刻的完整数据,增量文件持续追加,后台触发重写时只替换基础文件而不再阻塞文件操作。
这条演进告诉我们一件事:持久化方案没有银弹,官方一直在做的是"降低恢复成本、减少丢失窗口、控制重写开销"三者的平衡。选型的时候,你要算清楚自己到底更怕丢数据还是更怕恢复慢。
3.3 高可用:Sentinel、Cluster和复制协议的演进
高可用这条线从2.8的Sentinel开始,走向官方运维框架。Sentinel解决的是主从自动切换,Cluster解决的是数据分片和集群路由。它们解决的问题尺度不同,不是替代关系,不少人把这两个搞混,聊到架构时就会出岔子。
复制协议从2.8的PSYNC到4.0的PSYNC2,再到7.0对积压缓冲区的调整,核心都是同一个目标:尽量别全量同步。全量同步是资源黑洞,越是大数据量越需要PSYNC保住积累的复制上下文。另外,7.0还优化了主从切换时的故障转移流程,配合Sentinel和Cluster使用时,脑裂的窗口有所缩短。
看版本演进时如果把这三条主线抓在手里,你会发现所有新功能都不过是这三条线上某个痛点的解法。理解了这点,你读更新日志的思路会清晰很多。
4. 从老版本升级到新版本,最容易出事的兼容性清单
4.1 命令与返回内容的变化,监控脚本最容易挂
升级后最先哭的往往是监控脚本。INFO命令的输出字段,不同版本差异非常大:4.0去掉了部分碎片率字段的表达方式,5.0改进了replication区块,7.0对内存、复制、命令统计都有重构。你如果有一个shell或者Python脚本grep固定的INFO行,升级之后第一件事就是把它跑一遍,该改的地方马上改。
命令本身也有变化:新增命令不会冲突,但旧命令的选项和语义可能变。比如某些版本的命令对异常参数的返回错误消息措辞不一样,或者对最大参数长度的限制不同。再比如6.0引入ACL后,默认default用户虽然能直接用,但一旦你配置了ACL并误删了default权限,很多老客户端会突然连接被拒。
4.2 默认配置和行为差异:过期、淘汰、认证、协议
升级时不会报错、但行为悄悄变化的东西最坑。4.0以后AOF默认就带RDB前导(aof-use-rdb-preamble默认yes),你如果升级前不知道,看到AOF文件的大小和格式变了会吓一跳。7.0以后AOF变成多文件加manifest,备份目录多出一堆文件,不通用的备份清理脚本会误删增量文件,直接导致重启时AOF丢失。
7.0对淘汰策略的默认处理也和旧版略有不同:虽然策略名没变,但LFU和LRU的近似算法细节在不同版本之间有改进。新版淘汰行为通常更合理,但如果你的系统对淘汰顺序有强依赖,就需要重新压测验证。
认证这块,6.0如果只设置了requirepass,老逻辑仍然有效。但如果你为了用ACL新建了用户,记得老客户端用的用户名和密码组合可能需要同步调整。协议方面,默认还是RESP2,只有客户端主动HELLO到RESP3才会切换,所以一般不会出问题。
4.3 客户端库和阈值参数的隐形门槛
很多人升级Redis版本,却忽略了客户端库也要对齐。老版本客户端不认新命令、不支持RESP3,或者在新协议下处理类型的逻辑有bug,都是常见事故。我的建议是:升级前先确认客户端库的版本支持目标Redis的大版本,至少不要低于目标版本发布前后的维护版本。
内存编码参数也有门槛。比如hash-max-ziplist-entries、hash-max-ziplist-value这种,在4.0前后存在配置名的调整,在7.0中部分新编码参数替换了旧的ziplist配置。你用旧版redis.conf直接跑7.0时,某些参数会变成unknown config,Redis的响应可能是启动失败或者静默忽略,取决于具体配置项的处理策略。稳妥做法是升级前用访问目标版本的redis.conf对照改一遍,别拿旧配置文件硬凑。
5. 一条稳一点的升级路线:检查、灰度、回滚
5.1 先盘点存量,再决定跨几个版本
跨版本升级最忌讳"一步登天"。从2.8直接跳到7.0,中间跳过太多行为变化和数据格式变迁,出了问题你连归因都困难。建议的路径是按主版本爬坡:2.6或2.8先到3.x,再4.0,再5.0,再6.2,最后7.0。每个大版本之间消化一下兼容问题,风险是可控的。
升级前先回答三个问题:所有command都还在吗;INFO输出和配置项有哪些对不上;有没有用GEO、Stream、模块这些特定版本的新特性。这三个问题的答案就是你的改造清单,缺一不可。
老实话讲,现实里很多团队不会真的逐级升级,他们会直接在新环境起新版本做好数据校验。但无论如何,"盘点存量命令+扫描配置项+梳理客户端依赖"这三步不该省。
5.2 灰度升级时,重点盯这几个指标
如果条件允许,先搭一套新版本的从库,让它从当前主库做全量同步。观察两个东西:第一,主从复制积压的负载变化;第二,从库加载RDB和AOF的耗时。同步稳定后,切一部分读流量过去,对比命中率、延迟、OOM kill和fork耗时。
这里有一个容易被忽视的指标:fork耗时。RDB持久化依赖fork,而fork时间在内存大的实例上会随着版本和操作系统变化。某些场景下,新版本因为引入了新的内存分配策略,fork时间反而更久。你得实际测,不要拿文档结论当结果。
等读流量验证没问题,再在低峰期做一次主从切换演练,确认Sentinel或Cluster模式下切换后新主能正常写、旧主不会脑裂抢写。这一步通过在预发布环境模拟就够了,但一定要真的做过,不能只在文档上推演。
5.3 回滚方案不是"保留旧包",而是保留旧状态
很多人自以为有回滚方案:把旧版本二进制留着,出了事停新版拉旧版。但版本这玩意儿一旦新版本启动过,RDB文件格式可能已经升级,AOF也可能已经重写,旧版本读不回去。所以真正的回滚方案是:确定最后一个兼容旧版本的状态点,比如升级前的RDB备份和AOF备份完整保留,或者旧实例的磁盘快照保留。
更好一点的做法是保留一个旧版本从库,让它继续跟着旧主库复制。升级失败时,这个旧从库还能随时提升为新主,避免回退到"没有数据副本"的尴尬状态。这个习惯我一直在用,虽然会浪费一台机器,但心里踏实。
6. 不同场景下该怎么选版本
6.1 老项目改造:4.0是一个比较稳的基线
如果你的业务有一套跑了好几年的Redis,用了很多自定义脚本、老旧的客户端,也不太想大规模改造,那4.0作为升级目标是性价比最高的。它有混合持久化、异步删除、PSYNC2、模块系统,这几项都是实打实的稳定性提升,而且命令API和3.x基本兼容,改造面小。
4.0唯一的问题是它已经比较老了,后续版本里好用的新命令(GETDEL、Stream、ACL)你都没法用。所以这个建议只适合"先稳为主、功能不追新"的场景。
6.2 新项目开发:7.0,但要做好配置功课
新项目不用犹豫,直接上7.0。Stream做消息队列、ACL做权限隔离、Functions管理脚本、多线程IO应付高吞吐,这些能力都是新需求里高频踩到的点。但新版本带来的配置复杂度也高,尤其是ACL、IO线程调优、AOF多文件管理,需要你提前做实验。
新项目的好处是数据模型自己设计,从一开始就可以把key前缀规划和ACL权限规划配合起来,比如让团队A只能访问order前缀,团队B只能访问user前缀。这套东西在老项目里很难坐实,新项目则可以在一开始就定好规矩。
6.3 云服务与自建的选择,版本跟进周期也不太一样
用云厂商的托管Redis时,版本推进是可控但滞后的。托管的优势是底层监控、参数模板、高可用都由平台兜底,你省心不少,但万一你正好需要某个新版本的功能而平台还没上线,就非常难受。自建Redis虽然自由度大,但升级、压测、回滚全得自己扛,责任和成本也会实打实压过来。
不管哪种方式,我建议你都把"目标主版本+对应客户端版本+配置文件基线"记录在案,至少让团队里任何人都能在十分钟内回答出"我们现在的Redis是哪个版本、客户端是什么版本、配置基线在哪"。这比任何高深的调优都重要。
说实话,把2.6到7.0这段历史翻完之后,我最大的感受是:Redis能成为基础设施,靠的不是某一个惊艳版本,而是每一代都在"保兼容、换机制、加能力"之间的耐心平衡。这里没有银弹版本,只有适合你当前状态的版本。选型时别只看特性列表,先把你自己的运维脚本、客户端版本、数据恢复需求列清楚,答案自然会浮现。