关于 Redis 与 Memcached 的几点澄清
原文由 Salvatore Sanfilippo 于 发布,订阅该博客
如果你了解我,就知道我不是那种把竞品视为坏事的人。我其实很乐见用户拥有选择的余地,所以我很少会去做 Redis 与其他技术对比这类事。
但同样不可否认的是,用户要做出正确的选择,就必须获得准确的信息。
写这篇文章的起因,是我读到了 Mike Perham 发表的一篇博文。你可能知道,他是热门库 Sidekiq 的作者,而 Sidekiq 恰好以 Redis 作为后端。所以我完全不认为 Mike 是那种“反对”Redis 的人。然而在那篇博文中(地址为 http://www.mikeperham.com/2015/09/24/storing-data-with-redis/),他却表示,就缓存而言,“你或许应该用 Memcached 而不是 [Redis]”。可见 Mike 是真心认为 Redis 不适合做缓存,他是这样论证自己观点的:
- Memcached 专为缓存而设计。
- 它完全不产生磁盘 I/O。
- 它是多线程的,能够通过多核扩展来处理数十万请求。
下面我会逐一回应上述观点,之后还会补充一些上述论述未涵盖、但在我看来对大多数缓存用户和场景更为重要的信息。
Memcached 专为缓存而设计:这一点我就不多说了,因为它算不上什么论据。我同样可以说“Redis 专为缓存而设计”。所以在这方面,两者完全一样,我们直接看下一条。
它完全不产生磁盘 I/O:在 Redis 中,如果你愿意,也可以完全禁用磁盘 I/O,从而获得纯内存的体验。不同的是,如果你确实有需要,还可以在即将重启时才对数据库做一次持久化,比如使用“SHUTDOWN SAVE”命令。归根结底,就算你完全不用持久化,Redis 的持久化能力也是一种额外价值。
它是多线程的:这倒是事实,我的目标之一也是让 Redis 实现 I/O 多线程化(就像 memcached 那样,其实数据访问本身也并非多线程)。不过 Redis,尤其是配合流水线(pipelining)使用时,单线程每秒就能处理相当惊人的请求量(在流水线强度很高的情况下,每秒五十万次是很常见的数字;不使用流水线时,也能达到每秒约 10 万次操作)。在最典型的缓存场景下——每个 Redis 实例都相同、都作为主节点运行、禁用磁盘操作、分片由客户端负责,就像“memcached 分片模型”那样——在单台机器上启动多个 Redis 进程并不是什么糟糕的做法。这样做之后,你得到的其实是一个无共享的多线程架构,此时关键就在于单线程能处理多少操作。上次我测试时,Redis 在单线程上的速度至少不逊于 memcached。随着实现不断变化,今天孰优孰劣或许会有不同,但我相信两者性能会非常接近,因为它们都在尽力压榨可用资源。Memcached 的多线程仍然是一个优势,因为它让使用和管理变得更简单,但我认为这并非决定性因素。
还有一点。Mike 在谈每秒操作数时,却没有提到操作的*质量*。事实上,在 Redis 和 Memcached 这类系统中,命令分发和 I/O 的开销远比实际操作内存数据结构要大。所以基本上,在 Redis 里执行一个简单的 GET、一个 SET,或是一个像 ZRANK 这样的复杂操作,开销是差不多的。但从应用层面来看,一个复杂操作能完成的工作要多得多。也许你不必分五次去取五个缓存值,只需发送一小段 Lua 脚本就能搞定。因此,两套系统真正的“可扩展性”是有多重维度的,而你能用一次操作完成多少事,就是其中之一。
在我看来,Mike 的担忧中唯一站得住脚的就是多线程问题,而如果我们把 Redis 仅仅当作 memcached 的替代品来考虑,这个问题可以通过运行多个进程来解决,甚至只运行一个进程也足够了——因为要让单线程在执行类似 memcached 的操作时达到饱和,是非常非常困难的。
真正的差异
现在来谈谈两套系统之间*真正*的差异。
内存效率
这是 Memcached 过去优于 Redis 的地方。在一个被设计为纯粹的字符串到字符串字典的系统中,要更高效地利用内存相对简单。这一差距并不悬殊,而且我大概有五年没再去对比过了,但过去还是比较明显的。
不过,如果我们考虑的是长期运行进程的内存效率,情况就有些不同了。请看下一节。
但话说回来,要真正评估内存效率,你还得把 Redis 中经过特殊编码的小聚合值极高的内存效率考虑进去。例如,由小整数组成的集合在内部会被表示为 8 位、16 位、32 位或 64 位整数的数组,并且由于它们是有序的,在查询某个元素是否存在时可以通过二分查找以对数时间完成访问。
当你使用哈希来表示对象而不是使用 JSON 时,情况也是如此。因此,真正的内存效率必须结合具体的使用场景来评估。
Redis LRU 与 Slab 分配器
从内存利用率的角度看,Memcached 并非完美。如果你的应用中缓存值的大小会随时间发生剧烈变化,就很可能会产生严重的内存碎片,而唯一的解决办法就是重启。相比之下,Redis 在这方面要可预测得多。
此外,Redis 的 LRU 近期得到了大幅改进,现在已经非常接近真正的 LRU。更多信息可以在这里找到:http://redis.io/topics/lru-cache。据我理解,memcached 的 LRU 仍然依据其 slab 分配器来淘汰数据,因此有时行为会与真正的 LRU 相去甚远,不过我很想听听专家对此的看法。如果你想测试 Redis 的 LRU,现在可以使用新版 Redis 中 redis-cli 提供的 LRU 测试模式。
智能缓存
如果你想用 Redis 做缓存,却像使用 memcached 那样去用它,那你就真的错失了很多东西。在我看来,这是 Mike 那篇博文中最大的误区。越来越多的人转向 Redis,正是因为他们发现可以用更有用的方式来表示缓存数据。想保留某类数据的最新 N 条?可以用带上限的列表。想维护一个缓存的热度排行榜?可以用有序集合,诸如此类。
持久化与复制
如果你需要这些功能,它们就是非常重要的优势。例如,利用这种模式来支撑海量读请求就非常简单。带持久化的重启、能够随时间对缓存做快照等等,也是如此。当然,也完全存在两者都无关紧要的使用场景。我在这里想说的是,即便在“纯缓存”的用例中,持久化和复制有时也很重要。
可观测性
Redis 的可观测性非常非常强。它会针对大量内部指标提供详细的报告,你可以 SCAN 整个数据集、观察对象的过期情况、调整 LRU 算法、为客户端命名并在 CLIENT LIST 中查看、使用“MONITOR”来调试应用,以及其他许多高级功能。我认为这是一大优势。
Lua 脚本
我认为 Lua 脚本在许多缓存用例中都能提供极大的帮助。例如,如果你缓存了一个 JSON 对象,通过一条 Lua 命令就可以只提取其中某个字段并返回给客户端,而不必传输整个对象(从概念上讲,直接使用 Redis 哈希来表示对象也能达到同样的效果)。
结论
Memcached 是一款非常出色的软件,我多次阅读过它的源代码,它曾是行业内的一场革命,你应该好好评估一下对你而言它是否比 Redis 更合适。不过,事物应当就其本身来评判,归根结底,这些年来读到 Mike 这篇以及许多类似的报告,让我感到有些困扰,所以我决定在此阐述我的观点。如果你发现其中有任何事实性错误,请告诉我,我会以“EDIT”章节的形式更新这篇博文。
随机一篇博客
评论
登录后参与讨论