分布式缓存设计:一致性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 算法吗? 面试官心理分析 在前几年, ...
随机推荐
- sqlalchemy使用
1.SQLAlchemy的作用 ORM对象关系映射技术 2.SQLAlchemy安装 pip install SQLAlchemy 查看SQLAlchemy版本 3.生成数据库连接 from sqla ...
- js 中object对象的操作
n = object对象 for(var p in n){ console.log(p);// 取得是key值 console.log(n[p]);//取得是value值 } 继之前js中数组的常用方 ...
- RTX——第12章 系统时钟节拍和时间管理
以下内容转载自安富莱电子: http://forum.armfly.com/forum.php 本章节为大家讲解 RTX 操作系统的时钟节拍和时间管理函数,其中时间管理函数是 RTX 的基本函数,初学 ...
- Android开发系列(十七):读取assets文件夹下的数据库文件
在做Android应用的时候,不可避免要用到数据库.可是当我们把应用的apk部署到真机上的时候,已经创建好的数据库及其里边的数据是不能随着apk一起安装到真机上的. (PS:这篇博客攻克了我前面博客中 ...
- elasticsearch安装与使用(4)-- 安装中文分词插件elasticsearch 的 jdbc
前言 elasticsearch(下面简称ES)使用jdbc连接mysql比go-mysql-elasticsearch的elasticsearch-river-jdbc能够很好的支持增量数据更新的问 ...
- Android Studion的Monitor中显示No Debuggable Application的解决方法
在使用Android Studion的时候,突然android Monitor中无法下拉显示调试项目,只是一直提示No Debuggable Application,然后上网搜索的解决办法: 第一种方 ...
- SQL SERVER 2005快捷键
一.SQL SERVER 2005快捷键 快捷键 功能 CTRL + SHIFT + B生成解决方案 CTRL + F7 生成编译 CTRL + O 打开文件 CTRL + SHIFT + O打开项目 ...
- 很有必要了解的HTML嵌套规则
最近在重新学习HTML的知识,算是对HTML的一个重新认识吧!别小看了这东西,一切的网页可都是以它为基础的!下面就详细归纳一下HTML标签的嵌套规则吧,希望对大家有所帮助. XHTML的标签有许多:d ...
- 写给测试人员:不是所有的bug都需要修复
用户往往对产品中各种各样的bug抱怨不已,而测试人员往往认为自己的职责就是揪出这些所有的bug并把它们全都修复.然而,这是一个误区.微软卓越测试工程总监Alan Page近日撰文,再次解释了有哪些bu ...
- [oracle] oracle的三种密码验证机制以及在windows和linux下的不同启动过程
oracle数据库的密码验证机制: ① 操作系统验证 拥有SYSDBA和SYSOPER的用户用该方式验证此时数据库无需启动,也无需开启监听和实例服务. 要求:本地组ora_dba中有该操作系统的登录用 ...