
文章目录一、Redis 到底是单线程还是多线程1. 线程模型的架构划分2. I/O 多路复用工作流3. 为什么 Redis 能坚持核心单线程二、Redis 如何保证多操作的原子执行1. 复合指令2. Redis 事务 (Transactions)核心机制排队而非回滚事务异常处理与回滚真相3. Pipeline (管道机制)注意点4. Lua 脚本基础语法与调用实战分布式安全限流/扣减库存脚本注意5. Redis Function加载与调用自定义 Function三、 Redis 持久化底层原理1. RDB (Redis Database) 内存快照持久化触发方式核心底层Copy-on-Write (写时复制技术)2. AOF (Append-Only File) 追加日志核心配置redis.confAOF Rewrite (重写机制)一、Redis 到底是单线程还是多线程核心结论客户端多线程服务端核心网络 I/O 与键值读写单线程。1. 线程模型的架构划分Redis 的整体架构可以分为“对外连接”与“对内执行”两个部分对外通信与连接管理多线程Redis 为了支持海量客户端的并发连接在网络层利用多线程来维护 Socket 连接。在redis.conf中通过maxclients 10000参数来控制最大并发连接数。自 Redis 6.x 起引入了Threaded I/O多线程 I/O允许并行处理 Socket 的读取和写入。# redis.conf 核心 I/O 线程配置io-threads4# 建议在 4 核以上的机器开启留出一个空闲核。4核配2~3线程8核配6线程。核心事件处理器单线程Redis 响应网络请求中的“键值读写指令”时 report 给主线程串行执行。它基于操作系统的I/O 多路复用技术如 Linux 的 epoll构建了一个事件分发器Dispatcher。2. I/O 多路复用工作流单线程命令处理器事件分发器I/O多路复用器 (Epoll)单线程命令处理器事件分发器I/O多路复用器 (Epoll)轮询判断I/O事件检查软硬件资源是否满足严格串行执行无锁无线程切换开销Client 1Client 2发送 c1-connect / c2-get1发送 c3-hset2将就绪事件分派3串行化分发命令4Client 1Client 23. 为什么 Redis 能坚持核心单线程CPU 不是瓶颈现代 Redis 的性能瓶颈通常在于内存容量和网络带宽而非 CPU 计算能力。免去并发开销由于核心指令在主线程中串行执行Redis 完美避开了多线程高并发下的死锁、资源竞争以及线程上下文切换带来的资源损耗。在 Redis 层面你完全不用担心类似 MySQL 的脏读、幻读和不可重复读等并发问题。版本演进的谨慎妥协Redis 4.x 以前是纯单线程5.x 重构了核心代码6.x/7.x 引入了慢 I/O 异步线程。现在诸如FLUSHALL ASYNC、UNLINK异步删除以及 RDB/AOF 文件的生成都是由后台辅助线程独立完成的以此确保主线程的绝对流畅。二、Redis 如何保证多操作的原子执行虽然单个 Redis 命令是原子的但如果客户端连续发送多个命令在并发环境下仍可能被其他客户端的指令“加塞”打断。为了保证一组操作的原子性Redis 提供了以下 5 种进阶方案1. 复合指令Redis 内部原生提供了一些将多个动作合二为一的复合命令它们天然具备原子性。SETNX(Set if Not Exists)SETEX(Set with Expiration)GETSET(获取旧值并设置新值)MSET/HMSET(批量写入)2. Redis 事务 (Transactions)Redis 通过MULTI、EXEC、DISCARD、WATCH等命令支持事务机制。然而Redis 的事务与传统关系型数据库MySQL有本质区别。核心机制排队而非回滚当你开启事务MULTI后输入的后续命令并不会立即执行而是返回QUEUED状态进入一个 FIFO 的命令队列。只有收到EXEC时Redis 才会一股脑将队列中的命令串行执行完毕期间拒绝任何外来指令加塞。事务异常处理与回滚真相Redis 的事务不支持传统意义上的事务回滚Rollback数据。它处理错误的方式分为以下两种场景错误类型发生阶段表现形式最终结果语法/入队错误(Dirty)EXEC执行前(如命令敲错、参数不对)立即报错Redis 会为该事务打上Dirty标记全部不执行执行EXEC时会直接返回EXECABORT丢弃整个事务。运行时错误(Runtime Error)EXEC执行后(如对 String 执行 LPOP)命令在执行时报错输出WRONGTYPE错误不回滚报错的命令执行失败但之前和之后正确的命令依然会正常生效并持久化。# 运行时错误示例一条报错其他照样执行成功127.0.0.1:6379MULTI OK127.0.0.1:6379(TX)setk22QUEUED127.0.0.1:6379(TX)lpop k2 QUEUED127.0.0.1:6379(TX)incr k2 QUEUED127.0.0.1:6379(TX)exec1)OK2)(error)WRONGTYPE Operation against a key holding the wrong kind of value3)(integer)3极端宕机风险与 AOF 修复当EXEC被调用后事务命令会先写入 AOF 文件再执行。若在执行过程中服务器被kill -9或崩溃可能导致 AOF 文件记录了事务但数据未落盘。此时 Redis 重启会报错。修复方案运行redis-check-aof --fix工具剔除 AOF 文件中不完整的事务记录即可正常引导启动。3. Pipeline (管道机制)Pipeline 是一种优化网络 RTTRound-Trip Time往返时间的技术。当客户端需要执行大批量非依赖性指令时单条发送会产生巨大的网络延迟损耗。Pipeline 将多条命令打包一次性发送给服务端再统一接收响应。# Linux 终端下利用 Pipeline 批量高效写入示例catcommand.txt|redis-cli-a123qweasd--pipe注意点不具备原子性Pipeline 只是网络层面的打包命令传到服务端后依然有可能被其他客户端的离散指令插队加塞。内存占用风险Pipeline 期间会阻塞当前客户端。组装的指令不宜过多否则不仅客户端等待时间长服务端为了暂存这个繁忙客户端的批量回复Reply Buffer也会吃掉大量内存。4. Lua 脚本由于 Lua 语言的执行引擎在 Redis 内部是单线程运行的一条 Lua 脚本在 Redis 内部的执行是绝对原子性的。它弥补了事务和 Pipeline 无法编写复杂业务逻辑如条件判断、循环的断层。基础语法与调用# 语法结构EVAL script numkeys[key[key...]][arg[arg...]]# 示例通过全局变量 KEYS 和 ARGV 获取传参127.0.0.1:6379evalreturn {KEYS,KEYS,ARGV,ARGV}2key1 key2 first second1)key12)key23)first4)second实战分布式安全限流/扣减库存脚本在 Lua 脚本中可以通过redis.call()函数直接调用 Redis 指令-- 逻辑获取当前商品库存若大于等于预期值则扣减/设置否则放弃localinitcountredis.call(get,KEYS)localatonumber(initcount)localbtonumber(ARGV)ifabthenredis.call(set,KEYS,b)return1endreturn0注意严禁死循环与耗时运算若 Lua 脚本运行超时默认lua-time-limit为 5 秒Redis 会向其他所有客户端抛出BUSY异常此时只能使用SCRIPT KILL仅限无写操作的脚本或SHUTDOWN NOSAVE强行终止。Redis 7 新特性EVAL_RO只读脚本对于不修改数据集的只读脚本打上只读标记并使用EVAL_RO执行。这类脚本可以在 Replica 复制节点上运行大幅分摊主节点压力。5. Redis Function如果你觉得在代码里拼装 Lua 脚本不便管理且无法复用Redis 7.x 带来了全新的Redis Function。Function 允许管理员将一系列复杂的逻辑直接声明为一个“类库函数”并常驻缓存到 Redis 服务端。客户端只需像调用本地函数一样通过FCALL触发。加载与调用自定义 Function在服务器上编写mylib.lua注意第一行命名空间声明绝不可少#!lua namemyliblocalfunctionmy_hset(keys,args)localhashkeyslocaltimeredis.call(TIME)-- 获取Redis服务端时间returnredis.call(HSET,hash,_last_modified_,time,unpack(args))end-- 注册函数redis.register_function(my_hset,my_hset)通过客户端加载该函数库到 Rediscatmylib.lua|redis-cli-a123qweasd-xFUNCTION LOAD REPLACE其他客户端直接调用该函数127.0.0.1:6379FCALL my_hset1myhash myfieldsome value(integer)3三、 Redis 持久化底层原理作为一个内存数据库为了不让数据随着服务器断电而烟消云散Redis 提供了 RDB 和 AOF 两种不同的持久化硬核技术。1. RDB (Redis Database) 内存快照RDB 是在指定的时间间隔内将内存中当前时间点的数据生成一份全量快照Snapshot并保存到磁盘的dump.rdb文件中。持久化触发方式SAVE同步阻塞主线程亲自去写磁盘在写完之前Redis无法响应任何外部读写命令。线上严禁使用。BGSAVE异步非阻塞主线程会调用操作系统的fork()机制开辟一个子线程Sub-thread去完成磁盘 I/O。核心底层Copy-on-Write (写时复制技术)在执行BGSAVE的fork()瞬间子线程与主线程共享同一块物理内存数据。如果此时客户端发来全是读请求主、子线程互不影响如果客户端发来写请求修改数据操作系统会把该块内存区域复制Copy出一份副本主线程在副本上进行修改而子线程则继续将原本的老数据写入.rdb文件。主线程 (Main Thread) ───► 收到写指令 ───► 触发 Copy-on-Write ───► 复制单块内存页修改 ▲ 子线程 (Forked Thread) ─────────────────────────────────────────────┘ 保持原内存快照写磁盘 I/O优点文件紧凑恢复大数集时的速度远快于 AOF。缺点由于fork()成本较高通常只能隔一段时间做一次。如果中途宕机会丢失最后一次快照之后的全部数据。2. AOF (Append-Only File) 追加日志AOF 机制会将客户端发送的每一条写命令通过 RESP 协议追加到日志文件appendonly.aof的末尾。核心配置redis.confappendonlyyes# 开启 AOF# 三种同步策略# appendfsync always # 每次有写操作都强制刷盘。绝对安全但严重拖慢吞吐量。appendfsync everysec# 每秒刷盘一次。官方推荐折中方案最多丢失1秒数据。# appendfsync no # 由操作系统决定何时刷盘。AOF Rewrite (重写机制)随着运行时间增长AOF 文件会变得极其庞大。Redis 提供了自动重写机制当文件达到阈值时子线程会直接读取当前内存中的最新状态逆向生成最精简的落地命令去覆盖旧的 AOF例如连续 100 次INCR count会被直接重写合并为一条SET count 100。auto-aof-rewrite-percentage100auto-aof-rewrite-min-size 64mb# 当AOF文件至少达到64MB且相比上一次重写后大小增长了100%时触发重写优点极度安全默认每秒刷盘容灾恢复好。缺点相同数据集下文件体积远大于 RDB在高并发写操作下性能劣于 RDB。注意线上环境务必将 RDB 和 AOF 混合开启。RDB 用于大粒度快速容灾恢复AOF 用于保证不丢失小粒度实时数据。