由于实验室在一个项目中用到了Memcached分布式缓存,自己这段时间也对分布式缓存深入学习了一下,本文就总结一下自己的收获,还是从Memcached是什么谈起吧。
Memcached是什么
Memcached是一款高性能的分布式内存对象缓存系统,使用它可以减少应用系统对数据库的直接访问,减轻了数据库负载,并且提升了应用程序的响应速度。可以将Memcached比作一个巨大的、存储了很多
LRU算法
操作系统的页面置换算法中也包括该算法,这种算法非常适合于缓存系统。它基于一种假设:过去一段时间使用频率最低的数据,将来也会很少使用。下面是我自己实现的一个基于LRU算法的缓存,它实现了基本的缓存功能,不过需要改进的一点是该缓存应该是单例的。
package colin.cache;
/**
*
* @author Colin Wang
* Created on Apr 18, 2015
*/
public class LRUCache {
private static final int DEFAULT_MAX_SIZE = 10;
private Object[] elements;
private int size;
public LRUCache(int maxSize) {
elements = new Object[maxSize];
}
public LRUCache() {
this(DEFAULT_MAX_SIZE);
}
public boolean isEmpty() {
return size == 0;
}
public int size() {
return size;
}
public boolean isFull() {
return size == elements.length;
}
/**
* 返回元素在数组中的位置
*
* @param element
* @return
*/
public int indexOf(Object element) {
for (int i = 0; i < size; i++) {
if (element.equals(elements[i])) {
return i;
}
}
return -1;
}
public Object push(Object element) {
int index = -1;
if (!isFull() && indexOf(element) == -1) {
// 缓存未满,并且其中不含有待插入的元素
elements[size++] = element;
} else if (isFull() && indexOf(element) == -1) {
// 缓存已满,并且其中不含有待插入的元素
for (int i = 0; i < size - 1; i++) {
elements[i] = elements[i + 1];
}
elements[size - 1] = element;
} else {
index = indexOf(element);
for (int i = index; i < size - 1; i++) {
elements[i] = elements[i + 1];
}
elements[size - 1] = element;
}
return index == -1 ? null : elements[index];
}
public void show() {
for (int i = 0; i < size; i++) {
System.out.print(elements[i].toString() + " ");
}
}
}
一致性哈希算法
该算法在分布式缓存系统中应用十分广泛,它的关键之处在于使用环形的哈希空间。首先我们先考虑一下JDK中HashMap的实现,我们知道,HashMap有一个初始容量和装载因子,当HashMap中元素的数量达到了初始容量*装载因子
这么多时,HashMap就会进行扩容(变为原来的2倍)然后再哈希,当元素很多时,这种再哈希操作实际上是很消耗资源的。尽管HashMap使用了一种比较优化的方式,比如HashMap的大小总是为2的n次幂(默认大小为16),这样既减少了哈希碰撞的几率(因为2的n次幂-1
转化为二进制时所有位均为1,这样就避免了某些位不能发挥作用),又增加了HashMap在定位元素时的效率,比如方法:
static int indexFor(int h, int length) {
return h & (length-1);
}
当length总是2的n次方时,h & (length - 1)
运算等价于对length取模。但是这些仍然不能完全弥补所有元素必须全部重新哈希的弱点。比如我们拥有N台缓存服务器,所有的热数据已经通过Hash算法映射到了这N台服务器上,这时候当其中一台缓存服务器挂掉,或者我们又新添了几台缓存服务器进来时,所有已经映射好的数据都将失效,而我们的后端数据库将会承受这一切!所以,一致性Hash算法就派上用场了,上面已经提到,一致性哈希算法使用的是环形的哈希空间,如下图所示:
常规的Hash算法为将某个key映射为一个32位整数,我们将这0至2^32-1的数值空间首位相接便形成了一个环。然后把我们的缓存服务器通过散列(对IP或机器名等进行散列)映射到这个环上,这样,当一个对象被散列到某个位置后,我们就把该对象缓存到顺时针方向离它最近的缓存服务器结点上。使用这种方式,当其中一个缓存服务器结点失效时,不会导致所有的缓存数据都失效,也仅仅需要把原来被缓存到该结点上的数据继续缓存到下一个顺时针方向离它最近的结点上即可。当新增一个结点时,同样也只需要对很少的一部分数据进行再哈希并映射到新的结点上即可。为了避免出现数据分配不均匀的情况,比如绝大多数的数据都被缓存到了一个结点上,而其他的结点则缓存很少的数据,一致性哈希算法还提出了虚拟结点的概念。如下图所示:
A1和A2为结点A的虚拟结点,虚拟结点的hash值计算可以采用对应结点的IP地址加数字后缀的方式,如“192.168.1.1#1”。这样就可以将被散列到A1和A2的数据都缓存进结点A中,从而避免了数据分布不平衡的现象。