分布式缓存设计:一致性Hash算法
缓存作为数据库前的一道屏障,它的可用性与缓存命中率都会直接影响到数据库,所以除了配置主从保证高可用之外还需要设计分布式缓存来扩充缓存的容量,将数据分布在多台机器上如果有一台不可用了对整体影响也比较小。不多说,你懂我意思的!
缓存的分布式集群和应用服务器不同,缓存的分布式是每台机器上的缓存数据是不一样的,不像应用服务器是无状态的,每台机器上的服务是一致的。缓存访问请求不可以在缓存服务器集群中的任意一台处理,必须找到缓存有需要数据的服务器。所以保证在新上线机器和删除机器的时候整个集群中的已经缓存的数据尽可能地还被访问得到是分布式缓存系统设计的终极目录(这句话有点长)。
如何选择到正确的服务器需要使用好的路由算法,下面以简单的余数Hash算法为例来选择服务器:
首先用缓存数据的key的hash值除以服务器的数目,得到的余数就是集群服务器列表的下标。这样就可以定位到正确的服务节点。比如有3台服务器,node1,node2,node3对应在服务器列表编号为1,2,3。key为“Hello”的缓存查询请求来了之后,计算Hello的Hash值,假如为23432113,除以3余数假如是1,那就说明数据在node1上。
这种算法能否保证各服务节点保存的数据量大致相同完全依赖于key的hash值是否够分散。所以通常不会直接使用java对象的hashCode(),这个方法计算出来的值不够分散,而是先计算出对象的MD5值然后再调用hashCode()。
问题来了,假如新增一台机器,3台扩展到4台,这时候的除数变为4了,之前机器计算出来的余数都和之前不一致了,也就是说再也不能正确命中到缓存服务器,请求就直接到达数据库,大大增加了数据库的负载能力。针对这个问题一种解决办法是在网站访问量最少的时候扩容服务器,然后通过模拟请求的方法对缓存进行预热,使得数据在缓存服务器中重新分布。当然这种方案有点low,另外一种方法是改进路由算法,使得新加入的服务器不影响大部分缓存数据的正确命中。目前比较流行的就是一致性Hash算法。
一致性Hash算法大致过程:
1,先构造一个长度为2^32的整数环(称为一致性Hash环,分布范围为[0,2^32-1])
2,根据节点名称(例如IP或机器名)的Hash值放置在这个Hash环上。
3,然后根据需要缓存的数据的key值计算出Hash值,同样也分布在Hash环上。
4,当取缓存时,根据key算出Hash,然后顺时针查找离这个Hash值最近的服务节点,这个缓存值就保存在这个节点中。
一致性Hash算法可以使用二叉查找树来实现,Hash查找的过程实际上就是在二叉树中查找不小于查找数的最小数值,二叉树最右边叶子节点和最左边的叶子节点相连接,就可以造成环状的数据结构。在Java中就可以利用TreeMap轻松实现。
二叉查找树算法参考:http://blog.csdn.net/lcore/article/details/8889176#
如上图所示,Hash(object1)=key1;Hash(object2)=key2;Hash(object3)=key3;Hash(object4)=key4;分别分布在Hash环上。如果有三台机器,Hash(NODE1) = KEY1;Hash(NODE2) = KEY2;Hash(NODE3) = KEY3;所以就如下图所示,key1找到的是KEY1也就是NODE1;key2找到的是KEY3也就是NODE3;key3找到的是KEY2也就是NODE2;key4找到的是KEY3也就是NODE3。
这时如果新增了一台NODE4,其Hash为KEY4,假如在Hash环上分布如下,那么key2不再找KEY3了,找的是KEY4。
新增机器后,key1,key3,key4找的机器相比之前没有变化。也就是说这种算法能保证之前缓存的大部分数据还能继续被命中,优于取余数的Hash算法。但这个算法还是有一个问题,就是key2被命中到KEY4上了,NODE3有效数据被减少了,如果缓存数据量相当大的时候,会造成集群上的各个服务器之间的不平衡。
解决这种不平衡的方式是引入虚拟节点,虚拟节点是根据每一台真实节点而来的,假如NODE1的ip是10.0.0.1,我们不再使用这个IP直接计算Hash值放在Hash环上了。而是使用多个10.0.0.1#1、10.0.0.1#2 ......10.0.0.1#N计算多个Hash值放在Hash环上。这样就相当于给真实节点扩充了N个虚拟节点。这时再加一台机器就相当于要往Hash环上放N个节点,这N个节点会均衡分布在环上。这会给之前每个key值的指向都会产生变化,也就是相当于把变化的指向扩散给环上的多个节点去承担了。这样就保证了算法的平衡性。
不说了,看代码(Java实现的一致性Hash算法,利用TreeMap):
- public class ConsistentHash<T> {
- //自定义hash函数 Md5加密后再取hashcode
- private final HashFunction hashFunction;
- //虚拟节点个数
- private final int numberOfReplicas;
- //Hash环
- private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();
- //node为真实服务节点列表
- public ConsistentHash(HashFunction hashFunction, int numberOfReplicas,
- Collection<T> nodes) {
- this.hashFunction = hashFunction;
- this.numberOfReplicas = numberOfReplicas;
- for (T node : nodes) {
- add(node);
- }
- }
- /**
- * 增加一个服务节点 用服务节点名+虚拟节点编号做Hash运算
- * 在这个例子中每个服务节点有1000个虚拟节点
- * @param node
- */
- public void add(T node) {
- for (int i = 0; i < numberOfReplicas; i++) {
- circle.put(hashFunction.hash(node.toString() + i), node);
- }
- }
- /**
- * 删除一个服务节点 将虚拟节点全部删除
- * @param node
- */
- public void remove(T node) {
- for (int i = 0; i < numberOfReplicas; i++) {
- circle.remove(hashFunction.hash(node.toString() + i));
- }
- }
- /**
- * 根据缓存的key找出其所有缓存服务器
- * @param key
- */
- public T get(Object key) {
- if (circle.isEmpty()) {
- return null;
- }
- int hash = hashFunction.hash(key);
- // System.out.println("hash---: " + hash);
- if (!circle.containsKey(hash)) {
- SortedMap<Integer, T> tailMap = circle.tailMap(hash);
- hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
- }
- // System.out.println("hash---: " + hash);
- return circle.get(hash);
- }
- public static void main(String[] args) {
- HashSet<String> set = new HashSet<String>();
- set.add("A");
- set.add("B");
- set.add("C");
- set.add("D");
- Map<String, Integer> map = new HashMap<String, Integer>();
- //定义一个一致性hash对象,有四个节点,分别有1000个虚拟节点
- ConsistentHash<String> consistentHash = new ConsistentHash<String>(
- new HashFunction(), 1000, set);
- //定义10000条缓存 key为0到9999
- int count = 10000;
- for (int i = 0; i < count; i++) {
- //根据缓存key去取hash环中对应真实节点名称
- String key = consistentHash.get(i);
- //变量key为真实的名称
- //然后在一个map中开始计数
- if (map.containsKey(key)) {
- map.put(consistentHash.get(i), map.get(key) + 1);
- } else {
- map.put(consistentHash.get(i), 1);
- }
- }
- showServer(map);
- map.clear();
- //移除一台真实节点A
- consistentHash.remove("A");
- System.out.println("<<<< remove server A >>>>");
- for (int i = 0; i < count; i++) {
- String key = consistentHash.get(i);
- if (map.containsKey(key)) {
- map.put(consistentHash.get(i), map.get(key) + 1);
- } else {
- map.put(consistentHash.get(i), 1);
- }
- }
- showServer(map);
- map.clear();
- consistentHash.add("E");
- System.out.println("<<<< add server E >>>>");
- for (int i = 0; i < count; i++) {
- String key = consistentHash.get(i);
- if (map.containsKey(key)) {
- map.put(consistentHash.get(i), map.get(key) + 1);
- } else {
- map.put(consistentHash.get(i), 1);
- }
- }
- showServer(map);
- map.clear();
- consistentHash.add("F");
- System.out.println("<<<< add server F,缓存数量加倍 >>>>");
- count = count * 2;
- for (int i = 0; i < count; i++) {
- String key = consistentHash.get(i);
- if (map.containsKey(key)) {
- map.put(consistentHash.get(i), map.get(key) + 1);
- } else {
- map.put(consistentHash.get(i), 1);
- }
- }
- showServer(map);
- }
- public static void showServer(Map<String, Integer> map) {
- for (Entry<String, Integer> m : map.entrySet()) {
- System.out.println("server " + m.getKey() + " : " + m.getValue()
- + "个");
- }
- }
- static class HashFunction {
- int hash(Object key) {
- return Md5Encrypt.md5(key.toString()).hashCode();
- }
- }
- }
代码中用了的MD5方法如下:
- public class Md5Encrypt {
- /**
- * Used building output as Hex
- */
- private static final char[] DIGITS = { '0', '1', '2', '3', '4', '5', '6',
- '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
- /**
- * 对字符串进行MD5加密
- */
- public static String md5(String text) {
- MessageDigest msgDigest = null;
- try {
- msgDigest = MessageDigest.getInstance("MD5");
- } catch (NoSuchAlgorithmException e) {
- throw new IllegalStateException(
- "System doesn't support MD5 algorithm.");
- }
- try {
- msgDigest.update(text.getBytes("utf-8"));
- } catch (UnsupportedEncodingException e) {
- throw new IllegalStateException(
- "System doesn't support your EncodingException.");
- }
- byte[] bytes = msgDigest.digest();
- String md5Str = new String(encodeHex(bytes));
- return md5Str;
- }
- public static char[] encodeHex(byte[] data) {
- int l = data.length;
- char[] out = new char[l << 1];
- // two characters form the hex value.
- for (int i = 0, j = 0; i < l; i++) {
- out[j++] = DIGITS[(0xF0 & data[i]) >>> 4];
- out[j++] = DIGITS[0x0F & data[i]];
- }
- return out;
- }
- }
代码输出的结果如下,能看得出来每台服务器均匀地保存数据:
- server D : 2487个
- server A : 2675个
- server B : 2373个
- server C : 2465个
- <<<< remove server A >>>>
- server D : 3417个
- server B : 3148个
- server C : 3435个
- <<<< add server E >>>>
- server D : 2486个
- server E : 2522个
- server B : 2460个
- server C : 2532个
- <<<< add server F,缓存数量加倍 >>>>
- server D : 3994个
- server E : 4102个
- server F : 4014个
- server B : 3937个
- server C : 3953个
分布式缓存设计:一致性Hash算法的更多相关文章
- 分布式缓存的一致性Hash算法 2 32
w 李智慧
- 分布式缓存一致性hash算法理解
今天阅读了一下大型网络技术架构这本苏中的分布式缓存一致性hash算法这一节,针对大型分布式系统来说,缓存在该系统中必不可少,分布式集群环境中,会出现添加缓存节点的需求,这样需要保障缓存服务器中对缓存的 ...
- 分布式缓存一致性hash算法
当服务器不多,并且不考虑扩容的时候,可直接使用简单的路由算法,用服务器数除缓存数据KEY的hash值,余数作为服务器下标即可. 但是当业务发展,网站缓存服务需要扩容时就会出现问题,比如3台缓存服务器要 ...
- 分布式缓存技术memcached学习(四)—— 一致性hash算法原理
分布式一致性hash算法简介 当你看到“分布式一致性hash算法”这个词时,第一时间可能会问,什么是分布式,什么是一致性,hash又是什么.在分析分布式一致性hash算法原理之前,我们先来了解一下这几 ...
- 分布式缓存技术memcached学习系列(四)—— 一致性hash算法原理
分布式一致性hash算法简介 当你看到"分布式一致性hash算法"这个词时,第一时间可能会问,什么是分布式,什么是一致性,hash又是什么.在分析分布式一致性hash算法原理之前, ...
- Nginx+Memcache+一致性hash算法 实现页面分布式缓存(转)
网站响应速度优化包括集群架构中很多方面的瓶颈因素,这里所说的将页面静态化.实现分布式高速缓存就是其中的一个很好的解决方案... 1)先来看看Nginx负载均衡 Nginx负载均衡依赖自带的 ngx_h ...
- 一致性Hash算法在Redis分布式中的使用
由于redis是单点,但是项目中不可避免的会使用多台Redis缓存服务器,那么怎么把缓存的Key均匀的映射到多台Redis服务器上,且随着缓存服务器的增加或减少时做到最小化的减少缓存Key的命中率呢? ...
- 分布式一致性hash算法
写在前面 在学习Redis的集群内容时,看到这么一句话:Redis并没有使用一致性hash算法,而是引入哈希槽的概念.而分布式缓存Memcached则是使用分布式一致性hash算法来实现分布式存储. ...
- 7.redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?
作者:中华石杉 面试题 redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗? 面试官心理分析 在前几年, ...
随机推荐
- CCLayer设置anchorPoint无效的问题
最近刚发现一个问题,anchorPoint这个属性虽然是属于CCNode的,但是CCLayer设置anchorPoint没有效果.CCLayer的anchorPoint被默认设定在(0, 0)位置 ...
- Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用
Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用 原创 2017-04-13 嘟嘟MD 嘟爷java超神学堂 前言 前面几章介绍了一些基础,但都是静 ...
- gvim配置到命令行可以使用
下载安装gvim后,把gvim的安装目录添加到环境变量.
- 【C++程序员学 python】python 之奇葩地方
一.python 奇葩之一:没有花括号.没有分号 先来一个C类型的函数 void main() { int i = 0; for(int j = 0; j< 6;j++) { i = i +j; ...
- 【WPF】BusyIndicator做Loading遮罩层
百度了一下,粗略看了几个国内野人的做法,花了时间看下去感觉不太好用(比如有Loading居然只是作为窗体的一个局部控件的,没法全屏遮罩,那要你有何用?),于是谷歌找轮子去. 好用的轮子:http:// ...
- Object中有哪些方法?
有一种遗憾就是,每天都见到你,但是却不关注你!等到面试的时候才后悔莫及. Object类中有9大public方法: equals:判断两个对象"相等" hashCode:获取对象的 ...
- Activiti 获取定义
ProcessDefinitionEntity d = (ProcessDefinitionEntity)((RepositoryServiceImpl)repositoryService).getD ...
- ftp 长传报错553 可能是选的目录不对
ftp> put /root/20180711tmp.txt /cc.txt local: /root/20180711tmp.txt remote: /cc.txt 200 PORT comm ...
- [ES6]探究数据绑定之Proxy
知识储备 Proxy 方式实现数据绑定中涉及到 Proxy.Reflect.Set.Map 和 WeakMap,这些都是 ES6 的新特性. Proxy Proxy 对象代理,在目标对象之前架设一层拦 ...
- powerdesigner与mysql数据库的连接
(1).在桌面新建一个mysql.dpc文件,里面可以什么内容也没有. (2).打开powerdesigner,操作如下: (3).把mysql.dcp文件选中: (4).建立连接属性: (5).测试 ...