为了账号安全,请及时绑定邮箱和手机立即绑定

脚踏两只船的困惑 - Memcached与Redis

标签:
Java 设计 产品
序言

很多人喜欢把Memcached与Redis进行比较,基于这个论点,笔者希望可以大家呈现一些笔者自己的见解与大家分享。
首先,讨论一件事情之前,我们必须要对他们有一个基本的了解,才能给我们提供一个比较好的理论依据:

Memcached:一款完全开源、高性能的、分布式的内存系统;

Redis:一个开源的、Key-Value型、基于内存运行并支持持久化的NoSQL数据库;

从概念上来讲,不难看出来,其实Memcached和Redis并非是一对 ”孪生兄弟“ , 能将Redis和Memcached之间联系起来的纽带只有一个,就是 ”内存“。
从诞生之初,两者的目标是有差异的,Memcached追求的高性能的内存服务;而Redis追求的不仅仅是内存运行,还有数据持久化的需求;从这一点来讲,Memcached更专一高效,而Redis更追求 ”德智体全面发展“。

下面,我们可以来看一个图,让我们来更直观的认识他们:
图片描述

看起来是不是有点复杂,没关系,抛弃MySQL和MongoDB以后,再来一遍:
图片描述
相信通过上表的内容,大家应该对Redis和Memcached之间有一个比较直观的印象了,那么我们继续下一站,各方面优劣势对比。

网络IO之间的对比

Memcached是多线程,非阻塞IO复用的网络模型,分为监听主线程和worker子线程,监听线程监听网络连接,接受请求后,将连接描述字pipe传递给worker线程,进行读写IO,网络层使用libevent封装的事件库,多线程模型可以发挥多核作用,但是引入了cache coherency和锁的问题,比如:memcached最常用的stats命令,实际memcached所有操作都要对这个全局变量加锁,进行技术等工作,带来了性能损耗。

Redis使用单线程的IO复用模型,自己封装了一个简单的AeEvent事件处理框架,主要实现了epoll, kqueue和select,对于单存只有IO操作来说,单线程可以将速度优势发挥到最大,但是redis也提供了一些简单的计算功能,比如排序、聚合等,对于这些操作,单线程模型施加会严重影响整体吞吐量,CPU计算过程中,整个IO调度都是被阻塞的。

基于以上的内容大致可以总结如下:
在高并发场景的压力下,多线程非阻塞式IO的Memcached表现会更加优异。

内存管理机制

Memcached与Redis都是C语言实现,但是不约而同的都抛弃了传统C语言中的malloc/free函数,而选择了自主实现内存模型。
Memcached的内存模式,官方定义为 ”Slab Allocation“,大致流程图如下:

图片描述

上图为我们描述了如下几点:
1、chunk是Memcached用来存储数据的最小单位,就好像一个盒子,每个盒子里装的是我们的午饭一样。Memcache这样设计的初衷是为了尽量减少内存碎片的问题,熟练掌握内存变成的童鞋,相信对内存碎片都有 ”刻骨铭心“ 的感觉,这里就不赘述了
2、slab和page是更大一些的盒子,用于承装不同尺寸的Chunk,Chunk的大小是通过Factor【自增长因子决定】
3、不同尺寸的Chunk最终会交给一个”目录“进行管理,以便于访问,而这个目录叫slab_class.
数据访问流程: 客户端会现在slab_class里找到尺寸合适的Slab,并且通过一定的方式找到Chunk,最终保证数据会进入一个更合适的”盒子“,从而减少内存的浪费。

Redis在这一方面的处理相对简单,大致的形式如下:
图片描述
Redis每一个数据块都是根据数据类型和大小进行分配的,这一块数据的元数据(比如数据块大小)会存入内存块的头部,real_ptr是redis调用malloc后返回的指针。redis将内存块的大小size存入头部,size所占据的内存大小是已知的,为size_t类型的长度,然后返回ret_ptr。当需要释放内存的时候,ret_ptr被传给内存管理程序。通过ret_ptr,程序可以很容易的算出real_ptr的值,然后将real_ptr传给free释放内存。

总的来讲:
Memcached使用预分配的内存池的方式,使用slab和大小不同的chunk来管理内存,Item根据大小选择合适的chunk存储,内存池的方式可以省去申请/释放内存的开销,并且能减小内存碎片产生,但这种方式也会带来一定程度上的空间浪费

Redis使用现场申请内存的方式来存储数据,并且很少使用free-list等方式来优化内存分配,会在一定程度上存在内存碎片,Redis跟据存储命令参数,会把带过期时间的数据单独存放在一起,并把它们称为临时数据,非临时数据是永远不会被剔除的,即便物理内存不够,导致swap也不会剔除任何非临时数据(但会尝试剔除部分临时数据),这点上Redis更适合作为存储而不是cache。

基于以上两方面可以总结如下:
1、Redis内存空间的利用比Memcahced更精细,引入Memcached是用一个“盒子”对数据进行承载,哪怕这个盒子的尺寸再合适,也不可避免的会有空置;
2、Memcached完美的解决了内存碎片的问题;
3、Memcached内部还存在一个slot的机制,对内存的使用优先使用废弃内存,在内存的重复利用上也具有一定的优势;
4、Redis并不是将所有内存数据都存放在内存中,只会将所有的key存放在内存,在读取的时候会有一定几率存在一次IO操作,在这一点上,Redis是使用时间换取了空间的策略;

数据一致性保障

Redis提供了一个“事务”的概念,虽然这是一个假的事务,由于Redis是单进程操作,所以Redis的事务仅仅只是将一组操作按顺序进行操作,在这之间不会插入任何其他命令,从而保证数据的一致性,但是这种方式很容易造成操作阻塞。
Memcached提供了类似于乐观锁一样的cas操作,会快速的返回处理成功或失败,不会对其他数据操作产生影响。
在这一点上,Memcached的速度要比Redis更快也更安全。

集群形式

Memcached本身并不支持集群,所有的集群形式都是通过客户端实现,这就是大名鼎鼎的Memcached的两段Hash,大致如下图:
图片描述
如图所示:【从下往上看哈】
1、应用程序首先调用Memcached客户端(比如著名的Java客户端XMemcached),由Memcached客户端通过Hash算法选择Memcached节点。
2、对用户来讲,所有的分布式节点都是透明不可见的,就好像操作一个节点一样操作整个集群。

相比与Memcached,Redis显得更加“正常”一些,它没有中心节点,具有线性可伸缩的功能。Redis Cluster的分布式存储架构,节点与节点之间通过二进制协议进行通信,节点与客户端之间通过ascii协议进行通信。在数据的放置策略上,Redis Cluster将整个key的数值域分成4096个哈希槽,每个节点上可以存储一个或多个插槽。

有几点需要注意的内容:
1、单纯从集群角度而言,好像Redis更好,因为Memcached的分布式是客户端分布,不同客户端可能会产生完全不同的结果,比如一个C语言的客户端,一个Java语言的客户端。
2、Redis的集群,也存在一个很大的问题,就是插槽转移,如果Redis存在损坏节点,需要将数据插槽迁移出来会是一个灾难级别的存在。
BUT, 哈哈,万事万物中最可怕的BUT,在现有的分布式解决方案中,提供了一个Twemproxy的中间件,很好的解决了以上两部分内容,所以在这一点上,Redis与Memcached平分秋色。

总结

所有不希望看长篇大论的童鞋和所有已经看到这里的童鞋,这一部分才是我们的核心关键点,请重点阅读:
1、如果大家对持久化要求不高,更希望快速高效,建议大家优先考虑Memcached,常见的应用场景如下:

Tomcat集群部署:

第一个优势Tomcat集群的目标是统一Session访问,而这一部分Session在绝大多数场景下持久化的要求不高,所以不妨牺牲持久化来换取性能的提升;
另外一个优势,由于用户Session大部分时候都是长度都是在一个区间范围内,那么我们可以通过修改factor自增长因子来控制Chunk大小,最大程度的减少内存浪费;
如果对这一部分内容感兴趣,可以参考:https://coding.imooc.com/class/186.html

实时配置和无持久化要求的数据

在实际工作场景中,我们经常会有这样一种需求,就是预热服务器配置,并且希望可以通过在线修改配置信息来达到不同的效果,比如:一些功能开关【Hystrix的熔断开关、热卖商品的限时开关等等】、 软件系统在测试环境/预生产环境/生产环境之间的零配置切换等等内容,这些需要实时读取,但是没有持久化要求的内容,都比较适合Memcached。

存储JSON字符串

近一两年,很多童鞋喜欢将一个对象转换为JSON字符串存储,而这一部分内容的存储Memcached的效率要高于Redis。或者这样讲,Redis的String类型其实就是一个缩小版的Memcached,所以使用Redis的String类型做的事,Memcached都可以做的更出色。


2、追求多类型支撑,持久化要求相对比较高的情况下,优先使用Redis
Redis有Memcahced所无法比拟的多数据类型支撑,而这些类型在很多时候都可以化腐朽为神奇,大量减少业务的冗余代码。
第二,作为NoSQL数据库,Redis提供了两种持久化机制,从而保障了业务的延续性,在这一段上,Redis完爆Memcached,
第三:Redis从3.1版本开始支持Lua脚本这样的编程式读写数据,也极大的提高了Redis的延展性。
以下是Redis的常见应用场景:

***- 取最新N个数据的操作,如:可以将最新的50条评论的ID放在List集合

  • 排行榜类的应用,取TOP N操作,前面操作以时间为权重,这个是以某个条件为权重,比如按顶的次数排序计数器应用
  • 存储关系:比如社交关系,比如Tag等
  • 获取某段时间所有数据排重值,使用set,比如某段时间访问的用户ID,或者是客户端IP
  • 构建队列系统,List可以构建栈和队列,使用zset可以构建优先级队列***

最后最后,如果大家觉得上面的例子还是太复杂的话,就给大家留下一个终极秘籍:
以往你们使用Redis的String类型做的事,都可以用Memcached替换,以此换取更好的性能提升; 除此以外,优先考虑Redis;

点击查看更多内容
44人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
1.5万
获赞与收藏
624

关注作者,订阅最新文章

阅读免费教程

感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消