「Redis跑在一个线程上。现代服务器有64、96甚至128个核心。Redis只用其中一个。这是严重的架构限制。」——这话你一定听过,通常来自刚买了64核服务器、发现63个核心在摸鱼的工程师。
但抱怨归抱怨,结论错了。Redis的单线程不是技术债,是Salvatore Sanfilippo在2009年西西里岛家里写下的第一行代码时就定下的死规矩。理解这个选择,等于理解高性能系统设计的核心矛盾:你以为的瓶颈,往往不是真的瓶颈。
一、起源:一个创业公司逼出来的内存数据库
2009年,Sanfilippo的创业项目LLOOGG需要实时展示网站访客数据。当时Google Analytics还没有实时功能(要到2011年才上线)。技术卡点很具体:MySQL扛不住每秒几百次的列表推入弹出,每次还要毫秒级响应。
他在家先用Tcl写了个原型,300行,叫LMDB(LLOOGG Memory Database)。能跑。重写为C语言,取名Redis(Remote Dictionary Server)。
关键决策就在这时做出:单线程,事件循环驱动。不是没能力写多线程,是故意不写。
二、单线程的真正代价:不是算力浪费,是协调成本
现代服务器的核心数确实在暴涨。但Redis的操作粒度是微秒级——一次GET或SET在内存里完成的速度,比线程切换、锁竞争、上下文保存恢复的开销快一个数量级。
多线程的隐性账单:
• 锁:保护共享状态需要互斥,获取释放锁本身就是操作
• 缓存失效:多核之间的缓存一致性协议拖慢访问
• 上下文切换:操作系统调度线程的固定开销
• 复杂度:死锁、竞态条件、调试地狱
Redis的策略是结构性消除这些成本。没有锁,因为没有并行执行。没有竞态条件,因为命令严格串行。事件循环(kqueue/epoll/select)用单个线程处理所有网络I/O,没有「一个连接一个线程」的内存膨胀。
Sanfilippo的原话:「简单就是性能。」
三、诚实的代价:慢命令会阻塞一切
设计没有免费午餐。单线程的代价是:一个慢命令,全站停摆。
典型案例:KEYS命令扫描全库。生产环境百万键的数据库上执行,服务器在扫描期间完全无响应。不是延迟变高,是彻底卡住。凌晨3点的事故单,通常来自没读文档的工程师用了KEYS而不是SCAN。
这是设计层面的取舍,不是bug。文档明确警告,但人总会犯错。Redis Cluster和后来的多线程I/O(6.0版本)是部分回应,但核心命令执行保持单线程——因为换多线程的代价高于收益。
四、为什么这个选择至今成立
十五年过去,硬件天翻地覆,Redis的单线程核心没变。原因:
• 内存带宽才是瓶颈:现代CPU算力过剩,但内存子系统的并行度有限
• 网络延迟掩盖单线程:大多数Redis部署的延迟来自网络往返,而非命令执行
• 水平扩展更简单:多实例分片比多线程共享状态更容易预测和调试
• 代码可维护性:核心代码量小,bug少,新特性添加快
6.0版本引入的多线程I/O只处理网络读写,命令执行仍单线程。这是务实的修补,不是架构转向。
五、给系统设计的启示
Redis的案例戳破了一个迷思:核心数不用满就是浪费。真正的浪费是为用满核心而支付的协调成本。当操作粒度足够细、共享状态足够多时,串行执行可能比并行更快。
判断标准:如果锁竞争和上下文切换的时间接近或超过实际计算时间,多线程就是负优化。这个阈值在内存数据库里来得特别早。
实用建议:
• 监控慢查询日志,KEYS/FLUSHALL/SORT等命令设告警
• 大键值拆分,避免单个操作耗时过长
• 考虑Redis Cluster或代理层分片,而非单实例硬扛
• 评估是否需要Redis:读多写少且能容忍秒级延迟的场景,也许S3+CloudFront更便宜
单线程不是 Redis 的临时补丁,是贯穿其设计哲学的核心选择。它牺牲了垂直扩展的极限,换取了可预测的性能和极简的并发模型。当你的系统需要处理百万级键的亚毫秒响应时,这种取舍的价值才会真正显现。
热门跟贴