Redis核心知识点深度剖析:原理、机制与应用
title: Redis源码与实战剖析小结date: 2026-03-24 23:38:51permalink: /pages/327659/categories:数据库Redistags:author:name: sharkchililink: https://github.com/shkctl写在文章开头Redis 作为面试和实战中的高频技术栈,涵盖数据结构、持久化、内存管理、网络模型等多个维度。本文梳理了 Redis 核心知识点,适合准备面试或快速回顾知识体系。本文覆盖内容:基础原理:单线程设计、IO 多路复用、Pipeline数据结构:SDS、Zset、GEO、Stream、布隆过滤器可靠性:持久化机制、内存淘汰策略、原子性保证实战应用:分布式锁、消息队列、限流实现你好,我是SharkChili,Java Guide 核心维护者之一,对 Redis、Nightingale 等知名开源项目有深度源码研究经验。熟悉 Java、Go、C 等多语言技术栈,现任某知名黑厂高级开发工程师,专注于高并发系统架构设计与性能优化。🌟 开源项目贡献mini-redis:教学级 Redis 精简实现,助力分布式缓存原理学习🔗 https://github.com/shark-ctrl/mini-redis(欢迎 Star Contribute)📚 公众号价值分享企业级架构设计、性能优化、源码解析等核心技术干货,涵盖分布式系统、微服务治理、大数据处理等实战领域,并探索面向AI的vibe coding等现代开发范式。👥 加入技术社群关注公众号,回复【加群】获取联系方式,与众多技术爱好者交流分布式架构、微服务等前沿技术!详解redis基础知识点为什么Redis被设计成是单线程的Redis 采用单线程设计,主要基于以下原因:内存操作,CPU 不是瓶颈:Redis 数据完全存于内存,内存操作耗时约100 纳秒(ns),属于极轻量级操作避免锁竞争:如果引入多线程并发操作,必须加锁保护,而锁的引入会导致线程阻塞等待,反而降低吞吐量简化实现:单线程模型避免了复杂的并发控制,代码更简洁,bug 更少核心结论:内存操作极快,多线程带来的计算能力提升对 Redis 意义不大,反而徒增复杂度更多关于源码的解释可以参考笔者写的这篇文章:https://mp.weixin.qq.com/s?__biz=MzkwODYyNTM2MQ==mid=2247485163idx=1sn=e31811b653af3b999e19372dc913e0e6chksm=c0c65255f7b1db4348ed1fbf511509470b718ae15a21403c7a0a3ea6b9b096d4e71dc5cf2040#rd为什么 Redis 单线程也能这么快IO 多路复用:通过 epoll/select/kqueue 等机制,单线程可同时处理海量并发连接,避免为每个连接创建线程的开销数据结构极致优化:SDS:O(1) 获取长度,二进制安全ziplist / skiplist:按数据量自动切换编码,平衡内存与性能整数集合、压缩列表等紧凑编码,减少内存开销纯内存操作:数据存于内存,耗时约 100ns,无磁盘 I/O 阻塞无锁模型:单线程串行执行命令,无需加锁,无死锁风险,无同步原语开销补充:Redis 6.0 引入多线程后,网络 I/O 和协议解析也由多线程处理,进一步释放了单线程的数据处理能力更多关于源码的解释可以参考笔者写的这两篇文章:详解redis单线程设计思路: https://mp.weixin.qq.com/s/v8LJqvtPMukv2g6CHr948Q如何理解redis是单线程的:https://mp.weixin.qq.com/s/8BHPnoPAbx2eIAOeZCzPJwRedis 管道 PipelinePipeline 是redis客户端内置的一个优化设计,只需通过换行符分隔键入多指令,即可批量发送多条指令,减少网络往返次数(RTT)。当我们键入回车批量发送指令后,其指令流程为:客户端将多条命令打包,一次性发送到服务端服务端依次执行,缓存结果最后一次性返回所有执行结果对应性能优势为:优化点无 Pipeline有 PipelineRTTN 条命令 = N 次 RTTN 条命令 = 1 次 RTT系统调用N × 2 次(读+写)1 × 2 次注意事项:Pipeline不保证原子性,如果需要原子性,应使用 Lua 脚本与事务(MULTI/EXEC)的区别:事务保证原子性(但不自动回滚),Pipeline 只优化网络对于pipeline实现感兴趣的读者,可以查看redis-cli.c中的相关代码:/* Pipeline 核心流程 */while(repeat--){// 1. 命令追加到客户端缓冲区(此时无网络 I/O)redisAppendCommandArgv(context,argc,(constchar**)argv,argvlen);// 2. 读取响应时自动发送所有缓冲命令if(cliReadReply(output_raw)!=REDIS_OK){returnREDIS_ERR;}}关键点:redisAppendCommandArgv():仅将命令追加到context-obuf(sds 动态字符串),不触发网络发送cliReadReply()→redisGetReply():首次调用时通过redisBufferWrite()一次性发送所有缓冲命令多个命令共享一次网络往返,大幅减少 RTTRedis 如何保证命令原子性Redis 保证原子性的方式主要有三种:1. 原子命令Redis 提供了一系列单指令原子操作,将 RMW(Read-Modify-Write)合并为一条命令,无需额外加锁。例如:INCR/DECR:原子递增/递减SETNX:仅当 key 不存在时设置(相当于"检查再写入")这是最简单也是最安全的原子性保证方式。2. 单线程串行执行由于 Redis 采用单线程模型处理所有客户端请求,每个命令都是串行执行的,不存在并发竞争。这意味着:当客户端 A 执行某个命令时,客户端 B 的请求必须等待从结果上看,命令执行是互斥的,天然保证了原子性3. 分布式锁当业务逻辑无法用单条原子命令完成时,需要借助分布式锁保护临界资源。基本流程是:先用 SETNX 尝试获取锁,成功后执行业务逻辑,最后释放锁。代码示例如下:// 获取锁(使用唯一值作为 value,便于后续校验)StringuniqueValue=UUID.randomUUID().toString();StringlockKey="distribute:lock:order";Stringresult=jedis.set(lockKey,uniqueValue,"NX","PX",30000);if("OK".equals(result)){try{// 业务逻辑doSomething();}finally{// 释放锁(必须校验 value,避免误删他人的锁)Stringscript="if redis.call('get', KEYS[1]) == ARGV[1] then "+" return redis.call('del', KEYS[1]) "+"else "+" return 0 "+"end";jedis.eval(script,1,lockKey,uniqueValue);}}需要特别注意的是,分布式锁必须解决两个问题:第一,死锁问题:如果持有锁的进程崩溃或网络中断,锁将永远无法释放。因此必须给锁设置超时时间(TTL),让锁自动过期。第二,误删问题:假设客户端 A 持有锁,客户端 B 因为超时或网络原因无法完成业务,此时锁自动释放了,客户端 C 获取了锁。但客户端 A 业务完成后仍然会执行 DEL,可能误删客户端 C 的锁。解决方案是:在释放锁时先校验 value,只有值匹配才执行删除。4. Lua 脚本如果业务需要执行多条 Redis 命令,可以将它们写在 Lua 脚本中。Redis 会把整个 Lua 脚本作为原子操作执行,执行过程中不会被其他命令打断:localcurrent=redis.call("incr",KEYS[1])iftonumber(current)==1thenredis.call("expire",KEYS[1],60)endreturncurrent生产环境中推荐使用 Redisson 等成熟库。以 Redisson 为例,它内部封装了完整的分布式锁实现:Lua 脚本:获取锁、释放锁等核心操作都通过 Lua 脚本保证原子性,无需手写看门狗机制:如果业务执行时间超过锁 TTL,Redisson 会启动定时任务自动续期,避免锁提前过期导致业务失败唯一值校验:每次加锁都会设置唯一标识,释放锁时自动校验,避免误删他人持有的锁相比手写分布式锁,使用 Redisson 等成熟库更安全、更可靠。Redis 使用什么协议进行通信Redis 采用 RESP(REdis Serialization Protocol)协议进行通信,这是 Redis 自己设计的一种文本协议,用于客户端与服务端之间的请求响应。RESP 协议具有以下特点:简单:协议基于 ASCII 编码,易于阅读和调试,例如用 telnet 就能直接与 Redis 交互。高效:协议设计简洁,解析速度快,序列化/反序列化开销低。易于解析:客户端只需按规则读取首字节判断数据类型即可。二进制安全:使用长度前缀而非特殊字符(如\0)标识边界,可以处理包含任意字节的数据。RESP2 支持的数据类型包括:类型标识说明示例+简单字符串+OK\r\n-错误信息-ERR unknown command\r\n:整数:1000\r\n$批量字符串$5\r\nhello\r\n*数组*3\r\n:1\r\n:2\r\n:3\r\n实际使用中,客户端库(如 Jedis、Lettuce)会封装这些细节,开发者通常无需直接处理协议解析。Redis 支持哪几种数据类型Redis 的数据类型可以分为两大类:基本数据类型(5 种):String(字符串):最基础的数据类型,可存储字符串、整数或浮点数List(列表):按插入顺序存储的字符串列表,支持从两端操作Set(集合):无序且不重复的字符串集合,支持交集、并集等集合运算ZSet(有序集合):每个元素关联一个分数,按分数排序,适用于排行榜场景Hash(哈希):键值对集合,适合存储对象特殊数据类型(4 种):Bitmap(位图):按位操作,适合做签到、用户在线状态统计Geo(地理位置):存储经纬度坐标,支持距离计算、范围查询HyperLogLog:用于基数统计,占用空间极小,适合 UV 统计Stream(流):5.0 引入的消息队列,支持持久化和消费组Redis 为什么要自己定义 SDSSDS(Simple Dynamic Strings)是 Redis 自定义的字符串数据结构,相比 C 字符串有以下优势:1. 二进制安全C 字符串以\0结尾,如果存储二进制数据(如序列化对象、图片),中间可能包含\0,strlen()等函数会在第一个\0处截断。SDS 使用长度字段而非\0判断边界,可以存储任意字节。2. O(1) 获取长度C 字符串获取长度需要遍历到\0,时间复杂度 O(n)。SDS 在结构体中维护len字段,读取长度为 O(1)。以 SDS 8 为例,结构体定义如下:struct__attribute__((__packed__))sdshdr8{uint8_tlen;/* 已使用的字节数,读取长度直接返回此字段 → O(1) */...charbuf[];/* 柔性数组,存放实际字符串内容 */};可以看到,len字段紧邻buf[],获取字符串长度只需读取len字段,无需遍历,strlen()的调用被替换为直接读取结构体中的len字段。3. 空间预分配与惰性释放C 字符串拼接时,如果目标缓冲区空间不足,会溢出。SDS 会先检查空间,不够时自动扩容,安全可靠。更进一步,SDS 在扩容时会多分配一些空间(空间预分配),避免每次拼接都扩容;对于缩短操作,不会立即释放多余空间(惰性释放),方便下次复用,从而提升字符串处理效率。以sdscatlen为例,可以看到扩容的核心逻辑:sdssdscatlen(sds s,constvoid*t,size_tlen){size_tcurlen=sdslen(s);/* 关键:空间不够时自动扩容 ⭐ */s=sdsMakeRoomFor(s,len);if(s==NULL)returnNULL;memcpy(s+curlen,t,len);/* 复制数据 */sdssetlen(s,curlen+len);/* 更新长度 */s[curlen+len]='\0';/* 添加结束符 */returns;}再看惰性释放的实现,sdsclear只是将len置为 0,不释放内存:voidsdsclear(sds s){sdssetlen(s,0);/* 只重置长度,不释放内存! */s[0]='\0';/* 字符串结束符 */}这样设计的好处是:下次sdscatlen追加数据时,可以直接复用已分配的缓冲区,避免频繁malloc/free。Redis 中的 Zset 是怎么实现的Zset(有序集合)的底层实现根据数据量大小分为两种编码:小数据量:使用 listpack(3.2+)或 ziplist(3.2 之前),紧凑存储,节省内存。大数据量:使用 skiplist + dict 组合结构:dict(哈希表):存储 member → score 映射,用于 O(1) 查找某成员的分数skiplist(跳表):按分数有序存储 member,用于 O(logN) 的范围查询和排序为什么要同时用 dict 和 skiplist?因为:单独用 skiplist:按分数查 member 高效,但按 member 查分数需要 O(n)单独用 dict:按 member 查分数高效,但无法保证有序