是什么
- memcache是一个自由、源码开放、高性能、分布式的分布式内存对象缓存系统,用于动态Web应用以减轻数据库的负载。它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高了网站访问的速度。
- memcache是一种基于内存的key-value存储,用来存储小块的任意数据(字符串、对象)。这些数据可以是数据库调用、API调用或者是页面渲染的结果。
- memcache简洁而强大。它的简洁设计便于快速开发,减轻开发难度,解决了大数据量缓存的很多问题。它的API兼容大部分流行的开发语言。
许多语言都实现了连接memcached的客户端,其中以Perl、PHP为主。仅仅memcached网站上列出的有:
- Perl
- PHP
- Python
- Ruby
- C#
- C/C++
- Lua
- 等等
本质上,它是一个简洁的key-value存储系统。
memcache和memcached的区别:
- memcache是项目的名称
- memcached是memcache服务器端可以执行文件的名称
特征
memcache作为高速运行的分布式缓存服务器,具有如下特点
- 协议简单
- 基于libevent的事件处理
- 内置内存存储方式
- memcached互不通信的分布式
应用场景
- 访问频繁的字典数据。
- 大量的hot数据。
- 页面缓存。
- 频繁的查询条件和结果。
- 临时处理的数据。
一般的使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。
- memcache可以应对任意多个连接,使用非阻塞的网络IO。由于它的工作机制是在内存中开辟一块空间,然后建立一个HashTable,memcached自管理这些HashTable。
- 使用memcache的网络一般流量都是比较大的,为了缓解数据库的压力,让memcache作为一个缓存区域,把部分数据保存在内存中,在前端能够快速的进行存取。那么一般的焦点就是集中在如何分担数据库压力和进行分布式,毕竟单台memcache的内存容量的有限的。
memcache的操作流程
- 检查客户端的请求数据是否在memcached中,如有,直接把请求数据返回,不再对数据库进行任何操作,路径操作为①②③⑦。
- 如果请求的数据不在memcached中,就去查数据库,把从数据库中获取的数据返回给客户端,同时把数据缓存一份到memcached中(memcached客户端不负责,需要程序明确实现),路径操作为①②④⑤⑦⑥。
- 每次更新数据库的同时更新memcached中的数据,保证一致性。
- 当分配给memcache内存空间用完之后,会使用LRU(Least Recently Used,最近最少使用)策略加上到期失效策略,失效数据首先被替换,然后再替换掉最近未使用的数据
memcache访问模型
特别澄清一个问题,memcache虽然被称为“分布式缓存”,但是memcache本身完全不具备分布式的功能。memcache集群之间不会相互通信(与之形成对比的,比如JBoss Cache,某台服务器有缓存数据更新时,会通知集群中其他机器更新缓存或者清除缓存数据),所谓“分布式”,完全依赖于客户端程序的实现。就像上面这张图的流程一样。
同时基于这张图,我们理一下memcache一次写缓存的流程
- 应用程序输入需要写缓存的数据
- API将Key输入路由算法模块,路由算法根据Key和MemCache集群服务器列表得到一台服务器编号
- 由服务器编号得到MemCache及其的ip地址和端口号
- API调用通信模块和指定编号的服务器通信,将数据写入该服务器,完成一次分布式缓存的写操作
读缓存和写缓存一样,只要使用相同的路由算法和服务器列表,只要应用程序查询的是相同的Key,MemCache客户端总是访问相同的客户端去读取数据,只要服务器中还缓存着该数据,就能保证缓存命中。
这种MemCache集群的方式也是从分区容错性的方面考虑的,假如Node2宕机了,那么Node2上面存储的数据都不可用了,此时由于集群中Node0和Node1还存在,下一次请求Node2中存储的Key值的时候,肯定是没有命中的,这时先从数据库中拿到要缓存的数据,然后路由算法模块根据Key值在Node0和Node1中选取一个节点,把对应的数据放进去,这样下一次就又可以走缓存了,这种集群的做法很好,但是缺点是成本比较大。
一致性hash算法
从上面的图中,可以看出一个很重要的问题,就是对服务器集群的管理,路由算法至关重要,就和负载均衡算法一样,路由算法决定着究竟该如何访问集群中的哪台服务器,先看一个路由算法
余数Hash
比方说,字符串str对应的HashCode是50、服务器的数目是3,取余数得到2,str对应节点Node2,所以路由算法把str路由到Node2服务器上。由于HashCode随机性比较强,所以使用余数Hash路由算法就可以保证缓存数据在整个MemCache服务器集群中有比较均衡的分布。
如果不考虑服务器集群的伸缩性,那么余数Hash算法几乎可以满足绝大多数的缓存路由需求,但是当分布式缓存集群需要扩容的时候,就难办了。
就假设MemCache服务器集群由3台变为4台吧,更改服务器列表,仍然使用余数Hash,50对4的余数是2,对应Node2,但是str原来是存在Node1上的,这就导致了缓存没有命中。如果这么说不够明白,那么不妨举个例子,原来有HashCode为0~19的20个数据,那么:
现在我扩容到4台,加粗标红的表示命中:
如果我扩容到20+的台数,只有前三个HashCode对应的Key是命中的,也就是15%。当然这只是个简单例子,现实情况肯定比这个复杂得多,不过足以说明,使用余数Hash的路由算法,在扩容的时候会造成大量的数据无法正确命中(其实不仅仅是无法命中,那些大量的无法命中的数据还在原缓存中在被移除前占据着内存)。这个结果显然是无法接受的,在网站业务中,大部分的业务数据度操作请求上事实上是通过缓存获取的,只有少量读操作会访问数据库,因此数据库的负载能力是以有缓存为前提而设计的。当大部分被缓存了的数据因为服务器扩容而不能正确读取时,这些数据访问的压力就落在了数据库的身上,这将大大超过数据库的负载能力,严重的可能会导致数据库宕机。
这个问题有解决方案
(1)在网站访问量低谷,通常是深夜,技术团队加班,扩容、重启服务器
(2)通过模拟请求的方式逐渐预热缓存,使缓存服务器中的数据重新分布
一致性Hash算法
一致性Hash算法通过一个叫做一致性Hash环的数据结构实现Key到缓存服务器的Hash映射
具体算法过程为:先构造一个长度为
2
32
2^{32}
232的整数环(这个环叫做一致性hash环),根据节点名称的Hash值(其分布为
[
0
,
2
32
−
1
]
[0, 2^{32}-1]
[0,232−1])将缓存服务器节点放置在这个hash环上,然后根据需要缓存的数据的key值计算得到hash值(其分布也为
[
0
,
2
32
−
1
]
[0, 2^{32}-1]
[0,232−1]),,然后在Hash环上顺时针查找距离这个Key值的Hash值最近的服务器节点,完成Key到服务器的映射查找。
就如同图上所示,三个Node点分别位于Hash环上的三个位置,然后Key值根据其HashCode,在Hash环上有一个固定位置,位置固定下之后,Key就会顺时针去寻找离它最近的一个Node,把数据存储在这个Node的MemCache服务器中。使用Hash环如果加了一个节点会怎么样,看一下:
看到我加了一个Node4节点,只影响到了一个Key值的数据,本来这个Key值应该是在Node1服务器上的,现在要去Node4了。采用一致性Hash算法,的确也会影响到整个集群,但是影响的只是加粗的那一段而已,相比余数Hash算法影响了远超一半的影响率,这种影响要小得多。更重要的是,集群中缓存服务器节点越多,增加节点带来的影响越小,很好理解。换句话说,随着集群规模的增大,继续命中原有缓存数据的概率会越来越大,虽然仍然有小部分数据缓存在服务器中不能被读到,但是这个比例足够小,即使访问数据库,也不会对数据库造成致命的负载压力。
至于具体应用,这个长度为 2 32 2^{32} 232的一致性Hash环通常使用二叉查找树实现