-
一致性哈希算法到底是用来干什么的,它能解决业务中的哪些痛点,哪里需要这种算?相信大家都是带着疑问来的吧,先不急,咱们先从工作中常见的分表业务说起:
- 从分表说起
- 日渐膨胀的order表
拿工作中相关的支付来说吧,购买支付相关的order表,随着用户规模的日益扩大,购买业务的日益增长订单表会日益增大,当数据量增加到千万甚至上亿行级别时,即使走了索引,查询效率依旧很低,就需要对order表进行分表了 - 分表维度
我们先假定order表分10张表,此时我们需要考虑将原有order表中一张表的数据分配到order_part_x表中,需要考虑两个方面的问题:1.分表后,数据尽可能均匀分配;2.以前主表的查询可以很轻松迁移到分表中;考虑到order表查询都是通过用户user_id来查询的,可以考虑将原order表中user_id和分表数直接取余,当然,为了尽可能散列,可以将user_id取hash值,再和分表数进行取余操作,即:hash(user_id)%size,以此来确定数据会落入哪一个分表中,查询订单的时候同理,通过此方式可以先获取到特定用户的订单会落入到哪一个分表中,确定了order_part_x,那么查询就和之前单表查询一样了。
- 日渐膨胀的order表
- cache的数据分配
假设业务需要,现有N台cache服务器,我们需要将key-value类型的数据均匀分配到这N台cache服务器中,以此减轻数据库的压力。还是类似上文分表的思路,通过计算hash(key)%size,以余数来确定该数据该存放到哪一台cache,这样通过key,就可以确定数据该到或者到了哪台机器上了。这么做看似没什么问题,但是考虑到以下几种特殊情况:当其中一台cache服务器宕机了,需要将其中一台缓存服务器拿掉,那么对应的,数据到server的映射关系就变成了hash(key)%(size-1),当数据需求继续增长,需要更多的缓存服务器,我们将cache server增加1台,那么之前数据到server的映射关系就变成了hash(key)%(size+1),这种变动直接会导致之前cache中存放的数据几乎全部失效,因缓存无法命中,所有查询请求将会直接冲击数据库服务器,导致严重后果。显然,这种哈希取余的算法已经完全不适合做cache的分配了。 - hash一致算法
既然如此,有没有更好的办法,既解决了数据到缓存server的分配,又能灵活应对cache server的增减呢?就来到了刚所说的hash一致算法来了(Consistent Hashing),用自己的话简单来说,就是节点不再是具体的一个值了,而是一个范围区间,数据取哈希值落在对应节点的范围中间,就可以确定落在哪个节点上了
- 节点和数据
- 将节点node通过取hash值,映射到 [0, 2^32 - 1]范围之内,映射的规则可为 IP、hostname 等。
可以将其想象为一个圆,映射的节点Node_n和下一个节点占据了圆的一个范围[a,b),如下图所示:
- 将数据object映射到数值空间 [0, 2^32 - 1]
规则可以简要描述为:对于所有满足 hash(node) <= hash(object) 的节点,选择 hash(node) 最大的节点存放 object。如果没有满足上述条件的节点,选择 hash(node) 最小的节点存放该 object,如下图所示:
- 当某个节点宕机时:
假设上图中的node 2节点挂了,我们只需要将node 2节点的数据迁移到node 3来,其他节点没任何影响
- 新增一个节点:
假设Node 2和Node 3直接新增一个Node 2_1节点,只需要将原有Node 2的一部分数据迁移到Node 2_1上面去,其他节点也没任何影响
- 下面,采用java语言来实现上述算法,看代码:
package com.zyq; import java.security.MessageDigest; import java.util.ArrayList; import java.util.List; import java.util.SortedMap; import java.util.TreeMap; /** * Created with IntelliJ IDEA. * User: zouyiq@gmail.com * Date: 2018/1/4 * Time: 22:55 * To change this template use File | Settings | File Templates. */ public class ConsistentHash { public static void main(String[] args) { ServerInfo s1 = new ServerInfo("192.168.1.1"); ServerInfo s2 = new ServerInfo("192.168.1.2"); ServerInfo s3 = new ServerInfo("192.168.1.3"); ServerInfo s4 = new ServerInfo("192.168.1.4"); ServerInfo s5 = new ServerInfo("192.168.1.5"); List<ServerInfo> servers = new ArrayList<ServerInfo>(); servers.add(s1); servers.add(s2); servers.add(s3); servers.add(s4); servers.add(s5); ConsistentHash consistencyHash = ConsistentHash.getInstance(servers); for (int i = 0; i < 10; i++) { ServerInfo serverInfo = consistencyHash.hindServer(new Integer(i)); System.out.println("数据=i"+"命中了服务器:ip=" + serverInfo.getIp()); } } private static ConsistentHash instance; private TreeMap<Integer, ServerInfo> circle; private List<ServerInfo> servers; private ConsistentHash(List<ServerInfo> servers) { this.servers = servers; init(); //init once } public static ConsistentHash getInstance(List<ServerInfo> servers) { if (instance == null) { instance = new ConsistentHash(servers); } return instance; } private void init() { circle = new TreeMap<Integer, ServerInfo>(); for (ServerInfo server : servers) { Integer md5Hash = MD5.md5(server.getIp()).hashCode(); circle.put(md5Hash, server); } } public void removeServers(List<ServerInfo> servers){ for(ServerInfo serverInfo : servers){ servers.remove(serverInfo); } init(); } public void addSever(ServerInfo serverInfo){ servers.add(serverInfo); init(); } /** * hint by data hash * * @param data * @return */ public ServerInfo hindServer(Object data) { Integer hash = MD5.md5(data.toString()).hashCode(); // 这里对象的hash算法需要改进一下 SortedMap<Integer, ServerInfo> mapDistrict = circle.tailMap(hash); return mapDistrict == null ? circle.get(circle.firstKey()) : circle.get(mapDistrict.firstKey()); } } class ServerInfo { public ServerInfo(String ip) { this.ip = ip; } private String ip; public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } } class MD5 { // MD5加密。32位 public static String md5(String inStr) { MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("MD5"); } catch (Exception e) { System.out.println(e.toString()); e.printStackTrace(); return ""; } char[] charArray = inStr.toCharArray(); byte[] byteArray = new byte[charArray.length]; for (int i = 0; i < charArray.length; i++) byteArray[i] = (byte) charArray[i]; byte[] md5Bytes = md5.digest(byteArray); StringBuffer hexValue = new StringBuffer(); for (int i = 0; i < md5Bytes.length; i++) { int val = ((int) md5Bytes[i]) & 0xff; if (val < 16) hexValue.append("0"); hexValue.append(Integer.toHexString(val)); } return hexValue.toString(); } }
- 增加虚拟节点提高散列
节点的分配是由节点的hash值决定的,当节点数量很少的时候,容易造成数据的分配不均匀,为了解决上述问题,可以将真实节点映射出一些虚拟节点,一个真实节点对应多个虚拟节点,数据以上文提到的方式对应到虚拟节点,进而由虚拟节点映射到具体真实节点,使数据的分布更加散列,如下图所示,原有的node 2节点对应node_2_a和node_2_b节点,object映射到node_2_a或者node_2_b都会对应到真实节点node 2上去。
同理的,带虚拟节点的hash一致算法的java实现:
package com.zyq.vitural; import java.security.MessageDigest; import java.util.ArrayList; import java.util.List; import java.util.SortedMap; import java.util.TreeMap; /** * Created with IntelliJ IDEA. * User: zouyiq@gmail.com * Date: 2018/1/4 * Time: 22:55 * To change this template use File | Settings | File Templates. */ public class ConsistentHash { public static void main(String[] args) { ServerInfo s1 = new ServerInfo("192.168.1.1"); ServerInfo s2 = new ServerInfo("192.168.1.2"); ServerInfo s3 = new ServerInfo("192.168.1.3"); ServerInfo s4 = new ServerInfo("192.168.1.4"); ServerInfo s5 = new ServerInfo("192.168.1.5"); List<ServerInfo> servers = new ArrayList<ServerInfo>(); servers.add(s1); servers.add(s2); servers.add(s3); servers.add(s4); servers.add(s5); ConsistentHash consistencyHash = ConsistentHash.getInstance(servers,100); for (int i = 0; i < 100; i++) { ServerInfo serverInfo = consistencyHash.hindServer(new Integer(i)); System.out.println("数据=i"+"命中了服务器:ip=" + serverInfo.getIp()); } } private static ConsistentHash instance; private TreeMap<Integer, ServerInfo> circle; private List<ServerInfo> servers; private Integer vituralServerCount; /** * 虚拟server的生成策略 */ private void vituralServerListInit(){ circle = new TreeMap<Integer, ServerInfo>(); for(ServerInfo serverInfo : servers){ Integer orginalHash = MD5.md5(serverInfo.getIp()).hashCode(); for (int i = 0; i < vituralServerCount; i++) { Integer vituralHash = MD5.md5(serverInfo.getIp() + "-" + orginalHash + "-" + i).hashCode(); circle.put(vituralHash,serverInfo); } } } private ConsistentHash(List<ServerInfo> servers,Integer count) { this.servers = servers; this.vituralServerCount = count; vituralServerListInit(); //init once } public static ConsistentHash getInstance(List<ServerInfo> servers,Integer count) { if (instance == null) { instance = new ConsistentHash(servers,count); } return instance; } public void removeServers(List<ServerInfo> servers){ for(ServerInfo serverInfo : servers){ servers.remove(serverInfo); } vituralServerListInit(); } public void addSever(ServerInfo serverInfo){ servers.add(serverInfo); vituralServerListInit(); } /** * hint by data hash * * @param data * @return */ public ServerInfo hindServer(Object data) { Integer hash = MD5.md5(data.toString()).hashCode(); // 这里对象的hash算法需要改进一下 SortedMap<Integer, ServerInfo> mapDistrict = circle.tailMap(hash); return mapDistrict == null ? circle.get(circle.firstKey()) : circle.get(mapDistrict.firstKey()); } } class ServerInfo { public ServerInfo(String ip) { this.ip = ip; } private String ip; public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } } class MD5 { // MD5加密。32位 public static String md5(String inStr) { MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("MD5"); } catch (Exception e) { System.out.println(e.toString()); e.printStackTrace(); return ""; } char[] charArray = inStr.toCharArray(); byte[] byteArray = new byte[charArray.length]; for (int i = 0; i < charArray.length; i++) byteArray[i] = (byte) charArray[i]; byte[] md5Bytes = md5.digest(byteArray); StringBuffer hexValue = new StringBuffer(); for (int i = 0; i < md5Bytes.length; i++) { int val = ((int) md5Bytes[i]) & 0xff; if (val < 16) hexValue.append("0"); hexValue.append(Integer.toHexString(val)); } return hexValue.toString(); } }
- memcached中一致性hash算法的实现细节
Memcached是一个自由开源的,高性能,分布式内存对象缓存系统。看了下java版client的实现,里面数据对应节点的映射就用到了一致性hash算法,以具体实现为例:private volatile TreeMap<Long, MemcachedNode> ketamaNodes; //虚拟节点对应物理节点的映射,key即虚拟节点的hash值
protected TreeMap<Long, MemcachedNode> getKetamaNodes() {
return ketamaNodes;
}MemcachedNode getNodeForKey(long hash) {
final MemcachedNode rv;
if (!ketamaNodes.containsKey(hash)) {
// Java 1.6 adds a ceilingKey method, but I'm still stuck in 1.5
// in a lot of places, so I'm doing this myself.
SortedMap<Long, MemcachedNode> tailMap = getKetamaNodes().tailMap(hash);//通过object的hash值获取虚拟节点map
if (tailMap.isEmpty()) {
hash = getKetamaNodes().firstKey();
} else {
hash = tailMap.firstKey();//大于object的hash值中,取hash值最小的那个虚拟节点作为命中节点
}
}
rv = getKetamaNodes().get(hash);//通过虚拟节点映射获得真实的物理节点
return rv;
}
浙公网安备 33010602011771号