在上一篇文章中,我们聊了 Redis 高性能的基石------I/O 多路复用。我们把它比作**"海底捞的沙发等位模式"**:通过一个大屏幕(Epoll)让唯一的服务员(主线程)能监控无数个在沙发上等待的客人,从而避免了"死排队"。
但是,随着互联网的发展,只有"大屏幕叫号"还不够了。于是 Redis 6.0 引入了多线程。
很多人听到"多线程"三个字就开始慌:Redis 变复杂了吗?需要加锁了吗? 别急,今天我们继续用餐厅的逻辑,把这三个核心问题一次性讲清楚。
一、 Redis 6.0 为什么要引入多线程?
首先要澄清一个概念:Redis 6.0 引入多线程,并不是说它变成了一个多线程数据库。 它操作内存数据的核心逻辑(也就是"Get/Set"这种内存操作),依然是单线程的。
那多线程用在哪儿了?为什么要用?
1. 之前的瓶颈:大厨太累了
我们回到海底捞的场景。 在 Redis 6.0 之前,虽然有"大屏幕"负责叫号,客人不需要傻站着排队,但是当号叫到了之后,流程是这样的:
-
场景(单线程模式):
-
大屏幕叫号:5 号桌客人入座。
-
点单(IO Read - 慢) :大厨(主线程) 必须亲自跑到 5 号桌,拿着小本本听客人报菜名。客人说话慢,大厨就得在那儿等着记录。
-
做菜(Execute - 快):大厨拿到单子,跑回后厨,瞬间炒好。
-
上菜(IO Write - 慢):大厨又得亲自端着菜跑回 5 号桌。
-
发现问题了吗? 现在的 CPU(大厨的厨艺)太快了,纳秒级的。真正拖后腿的是**"跑到桌边听客人说话"和"端菜过去"**这两个环节(即网络 I/O 读写)。 大厨的时间全浪费在跑堂上了,灶台经常是空的。
2. Redis 6.0 的变革:雇佣"传菜员"
简单来说:前面通过IO多路复用去避免BIO阻塞的思路还是不变的,只不过针对传递数据的过程,多引入了几个线程来操作,降低了主线程的压力。
为了解放大厨,Redis 6.0 引入了多线程 I/O。 这里的多线程,相当于给大厨配了几个**"传菜员"**。
-
场景(6.0 多线程模式):
-
大屏幕叫号:5、6、7 号桌客人同时入座。(这一步没变,依然是主线程负责监控)。
-
点单(多线程并发 - 变了!): 大厨大手一挥:"传菜员 A、B、C,你们分别去听 5、6、7 号桌的点单,把单子记下来给我!"
- 效果 :原来大厨要跑 3 趟,现在 3 个传菜员同时跑。大厨站在灶台边,一步都没动。
-
做菜(单线程执行 - 没变!) : 传菜员把 3 张单子交给大厨。 注意:大厨依然是一个人、挨个儿炒这 3 份菜。 因为不需要别人插手核心的炒菜环节,所以不需要复杂的锁,依然极速且安全。
-
上菜(多线程并发 - 变了!): 菜炒好了,大厨让传菜员 A、B、C 分别端走。
-
3. 结论
Redis 6.0 引入多线程,是为了解决网络 I/O 的读写瓶颈。 它把"听"和"说"这两个费嘴皮子的事儿外包给了子线程,让主线程更专注于"动脑子"(操作内存)。
二、 架构取舍:Redis 是 AP 还是 CP?
聊完了微观的线程,我们看看宏观的架构。 在分布式系统的 CAP 定理中,Redis 是坚定的 AP 派(允许容错)。
-
C (Consistency 一致性):所有节点在同一时间的数据完全一致。
-
A (Availability 可用性):服务一直可用,哪怕读到的数据是旧的。
-
P (Partition Tolerance 分区容错性):网线断了系统也能工作。
1. 为什么是 AP?
想象一下,Redis 的主节点在北京,从节点在上海。突然由于施工,中间的光缆断了(分区 P 发生)。 此时,有一个用户向北京节点写入数据。
-
如果是 CP(如 Zookeeper、银行系统) : 北京节点会说:"我不确定上海那边能不能收到,为了防止数据不一致,我拒绝写入,暂停服务 。" ------ 保了数据,丢了服务。
-
如果是 AP(Redis) : 北京节点会说:"管他呢,能不能连上上海以后再说。我先收下你的数据,保证你现在能用 。" ------ 保了服务,丢了一致性。
2. 代价:异步复制的数据丢失
因为 Redis 选择了"快"和"可用",它默认采用了异步复制:
-
主节点写入成功。
-
主节点立刻返回 OK 给客户端。
-
主节点在后台通过异步线程,慢慢把数据同步给从节点。
风险 :如果在第 2 步和第 3 步之间,主节点突然断电,而数据还没来得及发给从节点。那么当从节点变成新的主节点时,这条数据就永远丢失了。
这就是 Redis 的性格:活着比完美更重要。
三、 事务哲学:Redis 支持事务回滚吗?
很多用惯了 MySQL 的同学,在用 Redis 事务时会觉得它是个"渣男"。
1. Redis 的事务是什么?
Redis 的事务(MULTI / EXEC)本质上是一个**"不插队的批处理队列"**。
-
你输入
MULTI,Redis 就给你发一个空篮子(队列)。 -
你往里放命令,Redis 不执行,只告诉你
QUEUED(入队成功)。 -
你喊
EXEC,Redis 就锁住大厨,一口气把篮子里的命令全做完,中间不允许任何人插队。
2. 为什么不支持回滚?
在 MySQL 中,如果事务执行一半报错了,数据库会像暖男一样,通过 Undo Log 帮你回滚,仿佛一切都没发生过。 但在 Redis 里,如果中间有一条命令报错(比如对 String 类型求 List 长度):
-
报错的那条命令失败。
-
其他的命令依然照常执行!
-
Redis 拒不认错,拒不回滚。
3. Redis 为什么要这么做?
这体现了 Redis 极简主义的设计哲学:
-
性能至上 :支持回滚需要记录复杂的日志(Undo Log),还要管理版本控制,这太慢了。Redis 是跑在内存里的超跑,不能背这么重的包袱。
-
都是你的错 :Redis 官方认为,事务失败通常是因为语法错误 或类型错误 (比如把字符串当数组用)。这是程序员在写代码时逻辑不严谨造成的 Bug,应该在开发阶段修好,而不是让数据库在生产环境牺牲性能来给你兜底。
所以,Redis 的态度是:"我很快,但我很冷酷。你自己写代码小心点。"
四、 总结
这篇博客我们站在上一篇 IO 模型的基础上,进一步拆解了 Redis 的三个核心特性:
-
线程模型 :从"大厨跑堂"进化为"大厨+传菜员",Redis 6.0 的多线程只用来处理网络读写,核心计算依然是单线程。
-
架构模式 :Redis 是 AP 系统。为了高可用和低延迟,它接受短暂的数据不一致,甚至接受极端情况下的数据丢失。
-
事务特性 :Redis 支持批量执行的原子性,但不支持回滚。它要求开发者对自己写的代码负责,以换取极致的性能。
到此为止,Redis篇完结。