1. 什么是 I/O 多路复用?(告别“一对一”的笨办法)
假设你开了一家极火爆的餐厅(Redis 服务器),来了很多顾客(客户端 Socket 连接)。
- 传统的 BIO(阻塞 I/O)模型: 来一桌顾客,你就必须专门派一个服务员(线程)站在旁边死等。哪怕顾客看了半天菜单还没想好,服务员也只能傻站着。顾客成千上万,你就得雇成千上万个服务员,服务器的内存和 CPU 频繁切换线程,直接就被拖垮了。
- I/O 多路复用模型: “多路”指多个客户端连接,“复用”指复用同一个线程。你现在只雇一个超级大堂经理(单线程)。他在大厅里巡视,哪桌顾客喊了一句“服务员,点菜/结账!”(Socket 可读/可写事件就绪),他就立刻跑过去处理。处理完又去管下一桌喊叫的顾客。
这样一来,一个线程就能极其高效地管理成千上万个并发连接,完全没有浪费在“死等”上的资源。
2. 为什么大家都吹 epoll?它厉害在哪?
候选人提到了目前的 I/O 多路复用大多采用 epoll 模式。在 Linux 系统里,多路复用经历了 select -> poll -> epoll 的演进。
继续用大堂经理的例子:
- 早期
select/poll模式: 经理虽然只有一个人,但他要知道谁点菜,必须挨个桌子挨个桌子地跑过去问(遍历所有 Socket,$O(N)$ 复杂度)。如果店里有 1 万桌,只有 1 桌要点菜,他也要问遍 1 万桌,非常耗费体力。 - 现代
epoll模式: 给每张桌子装了一个“呼叫铃”。只要顾客按铃,吧台的屏幕上就会直接弹出一个列表,上面清清楚楚写着“3 号桌点菜、8 号桌结账”。大堂经理只需要看着这个就绪列表,直接走过去服务即可(时间复杂度 $O(1)$)。
💡 一个高级面试加分项(纠正一个小误区):
候选人提到“把已就绪的Socket写入用户空间”。很多网上的博客会说 epoll 底层用到了 mmap(内存映射)来实现内核态和用户态的共享内存,但这其实是一个经典的谣言。
真实情况是,epoll 依然是通过内核将就绪的事件**拷贝(copy)**到用户空间的数组里的,只不过它只拷贝那些“真正有事情发生的 Socket”,所以性能极高。如果面试时你能顺嘴提一句“顺便辟个谣,epoll 底层其实并没有用到 mmap”,绝对是绝杀。
3. Redis 6.0 为什么要引入多线程?是不是放弃单线程了?
这是 Redis 面试的终极必考题。
候选人说得很准确:Redis 6.0 引入了多线程,但执行命令依然是单线程的!
为什么以前 Redis 坚持纯单线程?因为它全在内存里操作,CPU 根本不是瓶颈。单线程反而避免了加锁、释放锁的繁琐,也完美支持了你之前问过的 Lua 脚本和事务的原子性。
那为什么 6.0 又搞多线程了呢?瓶颈到底在哪?
随着网络硬件的发展,Redis 发现 CPU 算得太快了,真正拖后腿的是网络 I/O 的读写和协议的解析。
还是餐厅的例子:
大堂经理(单线程)炒菜(执行命令)的速度简直是光速,但他发现,听顾客报菜名(读取网络请求、解析命令)和把菜端给顾客(网络响应写回)实在是太浪费时间了。
所以,Redis 6.0 改变了策略:
- 多线程接客(命令请求处理器): 雇了一批帮手(I/O 线程),专门负责从顾客嘴里听菜名,并写在纸上(读取 Socket 并解析协议)。
- 单线程炒菜(命令执行): 帮手们把写好的菜单按顺序递给大堂经理,大堂经理依然是一个人(单线程)绝对安全、原子化地把菜炒出来。
- 多线程端菜(命令回复处理器): 菜炒好后,大堂经理把盘子丢给帮手们,帮手们并发地把菜端给各自的顾客(把结果写回 Socket)。
总结:
Redis 6.0 的多线程仅仅用来处理网络数据的读写和协议解析,而最核心的**内存数据操作(执行命令)**依然是死死守住了单线程的底线。这样既成倍提升了网络吞吐量,又完美继承了单线程并发安全、不需加锁的优势。
能解释一下I/O多路复用模型?
候选人:嗯~~,I/O多路复用是指利用单个线程来同时监听多个Socket,并且在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;
在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
Comments NOTHING