Redis 的单线程与速度
为什么 Redis 是单线程,但仍然能跑出十万级 QPS。
~/posts/redis-single-thread $ cat post.md
基本数据结构
Redis 的存储结构包括 String、List、Set、Sorted Set(即 ZSet)和 Hash。除了 ZSet 之外,其它结构的插入、查找、读取都是 O(1);ZSet 的读取也是 O(1)——这给了 Redis 在其它方面踏实优化速度的空间。
最知名的设计是 纯内存模式:没有启用持久化的 Redis 是一个纯内存缓存数据库,这让它节省了大量 IO 操作的时间和复杂度,可以专注优化数据结构本身的速度。
量级与单线程
单连接情况下,Redis 实例可以跑出十万级 QPS。测试机性能在这个量级上并不是瓶颈——内存和 CPU 都比需要的要快,真正的瓶颈是网络请求。在大量连接需要同时处理时其它硬件才会受影响,比如 40,000 个独立连接时 Redis QPS 会降到六万左右。
这个速度对绝大多数单实例的使用场景都足够。
Redis 处理网络问题的一种做法是自己实现了一套 VM 机制,用来应对内存被完全占满的情况。和一般数据库不同,Redis 倾向于把全部冷数据落到硬盘,但保留全部 Key 在内存里——这样冷数据的缓存命中能力更强。它还实现了自己独立的网络协议,比通用的 client-to-client 协议更强调速度,少经几次系统调用。总之,瓶颈基本和 CPU 无关。
写到这里其实有点矛盾——这个话题没什么好说的,自己跑一个 Redis 就能发现:CPU 根本不会跑满,网络已经先忙着排队了。这种情况下多线程的意义有限——既减不了本就没什么压力的 CPU 负担,反而要承担加锁解锁的额外开销。
多路 IO 复用
这是 Redis 值得讲清楚的点。多路 IO 复用利用 Event Loop 同时检查多个 IO 流的状态:Redis 优先轮询所有流,一次性处理就绪的流,避免无谓的轮空操作。“多路”指多个网络连接,“复用”指共用同一个线程。
它的作用是在处理多个连接请求时把网络 IO 的时间消耗压到最低。在其它硬件不成为瓶颈的前提下,这种做法能有效提高 Redis 处理瓶颈环节(网络)的效率。
单线程不等于只有一个线程
Node.js 也是单线程的,但我们不会真的在双核机器上只跑一个 Node 实例。
类似地,即使开一个 Redis 服务,它内部会把 IO 处理(包括网络和硬盘 IO)交给自己创建的子线程。也可以配置 Redis 启用 master-slave 结构,把一些耗时的读取 / 搜索操作分给子线程处理。即便应用本身是单线程,我们也可以通过配置自己来均衡 CPU 核心的使用——因为我们对自己的应用比操作系统的负载均衡更了解。