一、什么是hash、哈希函数、哈希表
- Hash
- 一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值
- 哈希函数
- 通过kry获取到所要查找的位置:hash函数就是根据key计算出应该存储地址的位置,而哈希表是基于哈希函数建立的一种查找表
- 哈希(Hash)算法
- 即散列函数。它是一种单向密码体制,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程。同时,哈希函数可以将任意长度的输入经过变化以后得到固定长度的输出。哈希函数的这种单向特征和输出数据长度固定的特征使得它可以生成消息或者数据。
二、常用哈希函数的构造方法
- 直接定制法
- 哈希函数为关键字的线性函数如 H(key)=a*key+b,这种构造方法比较简便,均匀,但是有很大限制,仅限于地址大小=关键字集合的情况
- 数字分析法
- 假设关键字集合中的每个关键字key都是由s位数字组成(k 1 , k 2 , … … , k n ),分析key中的全体数据,并从中提取分布均匀的若干位或他们的组合构成全体
- 平方取中法
- 如果关键字的每一位都有某些数字重复出现频率很高的现象,可以先求关键字的平方值,通过平方扩大差异,而后取中间数位作为最终存储地址
- 折叠法
- 如果数字的位数很多,可以将数字分割为几个部分,取他们的叠加和作为hash地址
- 比如key=123 456 789
- 我们可以存储在61524,取末三位,存在524的位置;该方法适用于数字位数较多且事先不知道数据分布的情况
- 如果数字的位数很多,可以将数字分割为几个部分,取他们的叠加和作为hash地址
- 除留余数法(用的较多)
- H(key)=key MOD p (p<=m m为表长)
- 很明显,如何选取p是个关键问题。
- 比如我们存储3 6 9,那么p就不能取3,因为 3 MOD 3 == 6 MOD 3 == 9 MOD 3,p应为不大于m的质数或是不含20以下的质因子的合数,这样可以减少地址的重复(冲突)
- MOD 取余
三、哈希碰撞(hash冲突)及处理
- 散列函数,都会存在一定的散列碰撞,因此存在一定的重复风险
- 处理方法
- 开放寻址法
- 所有的元素都在散列表中,每一个表项或包含动态集合的一个元素,或包含NIL。这种方法中散列表可能被填满,以至于不能插入任何新的元素
- 在开放寻址法中,当要插入一个元素时,可以连续地检查或探测散列表的各项,直到有一个空槽来放置待插入的关键字为止。
- 有三种技术用于开放寻址法:线性探测、二次探测以及双重探测
- 在 Java 中的 ThreadLocalMap 就是采用了开放寻址法来解决哈希冲突,因为开放寻址法在极端环境下时间复杂度会退化成 O(n),所以适用于数据量较少的场景
- 链地址法
- 链地址法也叫链表法,就是插入一个元素时,如果发现当前位置已经有元素,则以当前节点为头节点(尾插法)或者尾结点(头插法)构造一个链表
- 如果进一步优化的话,可以将链表修改为红黑树等数组结构,比如 jdk 1.8 之后的 HashMap 就是采用的这种方式进行优化
- 开放寻址法
四、MD5碰撞
- 概念:两个不同的字符串,通过逆算散列会出现值一定的可能
- 常见的碰撞方法:利用计算机的资源尝试碰撞已知的MD5码
- 穷举法
- 穷举法就是不停地尝试各种字符的排列组合,看哪一个组合的MD5码能对上。缺点是太耗费时间。举个例子,假设我们要破解一个6位大小写字母和数字混合的密码,那么一共有 (26 + 26 + 10) ^ 6 种组合。这个数的大小超过500亿
- 字典法
- 将原文和对应的MD5值以映射表的形式存放起来,利用空间换时间,在映射表中查找对应的结果
- 用字典法实现md5解密的网站:http://md5.cn/
- 穷举法
五、一致性hash
- 出现的原因:在分布式环境下,通常会使用hash MOD N(hash取余)将请求分发到不同的服务器上,但是当服务器增加或删除的时候,会造成一致性hash的问题
- 一致性Hash的原理
- 一致性hash算法+虚拟节点的实现
-
环型Hash空间
- 根据常用的Hash,是将key哈希到一个长为232的桶中,即0~232-1的数字空间,最后通过首尾相连,我们可以想象成一个闭合的圆
- 根据常用的Hash,是将key哈希到一个长为232的桶中,即0~232-1的数字空间,最后通过首尾相连,我们可以想象成一个闭合的圆
-
把数据通过一定的Hash算法处理后,映射到环上
- 例如:我们有Object1、Object2、Object3、Object4,通过Hash算法求出值如下
- Hash(Object1) = key1
- Hash(Object2) = key2
- Hash(Object3) = key3
- Hash(Object4) = key4;
- 例如:我们有Object1、Object2、Object3、Object4,通过Hash算法求出值如下
-
将机器信息通过hash算法映射到环上
- 一般情况下是对机器的信息通过计算hash,然后以顺时针方向计算,将对象信息存储在相应的位置。
- 假设三个服务器
-
NODE1、NODE2、NODE3
-
将三个服务器通过hash散列到环形空间
-
数据通过顺时针旋转,找到对应的服务器
-
-
虚拟节点
- 如果当服务器的节点距离太近,node1和node2举例太近,大部分的数据都会被转到node2节点
- node2的服务器频率压力过高,造成node2效率低
- 如果node2宕机,瞬间压力集中到node1,会造成node1崩溃等
- 虚拟节点的推出就是为了解决节点的分布不均衡问题
- 将真是节点虚拟映射多个子节点分不到环形hash上,保证数据均匀的分发到不同节点,避免瓶颈
- 将真是节点虚拟映射多个子节点分不到环形hash上,保证数据均匀的分发到不同节点,避免瓶颈
- 如果当服务器的节点距离太近,node1和node2举例太近,大部分的数据都会被转到node2节点
-
六、一致性hash算法+虚拟节点的实现
- hash算法的选择
- ava代码不要使用hashcode函数,这个函数结果不够散列,而且会有负值需要处理。
- 这种计算Hash值的算法有很多,比如CRC32_HASH、FNV1_32_HASH、KETAMA_HASH等
- 其中KETAMA_HASH是默认的MemCache推荐的一致性Hash算法,用别的Hash算法也可以,比如FNV1_32_HASH算法的计算效率就会高一些
- 数据结构的选择。根据算法原理,我们的算法有几个要求
- 要能根据hash值排序存储
- 排序存储要被快速查找 (List不行)
- 排序查找还要能方便变更 (Array不行)
- 另外,由于二叉树可能极度不平衡。所以采用红黑树是最稳妥的实现方法。Java中直接使用TreeMap即可。
hash一致负载均衡
package com.grea.qz.config;
import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* hash一致负载均衡
*/
public class ConsistencyHashing {
// 虚拟节点的个数
private static final int VIRTUAL_NUM = 5;
// 虚拟节点分配,key是hash值,value是虚拟节点服务器名称
private static SortedMap<Integer, String> shards = new TreeMap<Integer, String>();
// 真实节点列表
private static List<String> realNodes = new LinkedList<String>();
//模拟初始服务器
private static String[] servers = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.5", "192.168.1.6"};
static {
for (String server : servers) {
realNodes.add(server);
System.out.println("真实节点[" + server + "] 被添加");
for (int i = 0; i < VIRTUAL_NUM; i++) {
String virtualNode = server + "&&VN" + i;
int hash = getHash(virtualNode);
shards.put(hash, virtualNode);
System.out.println("虚拟节点[" + virtualNode + "] hash:" + hash + ",被添加");
}
}
}
/**
* 获取被分配的节点名
*
* @param node
* @return
*/
public static String getServer(String node) {
int hash = getHash(node);
Integer key;
SortedMap<Integer, String> subMap = shards.tailMap(hash);
if (subMap.isEmpty()) {
key = shards.lastKey();
} else {
key = subMap.firstKey();
}
String virtualNode = shards.get(key);
return virtualNode.substring(0, virtualNode.indexOf("&&"));
}
/**
* 添加节点
*
* @param node
*/
public static void addNode(String node) {
if (!realNodes.contains(node)) {
realNodes.add(node);
System.out.println("真实节点[" + node + "] 上线添加");
for (int i = 0; i < VIRTUAL_NUM; i++) {
String virtualNode = node + "&&VN" + i;
int hash = getHash(virtualNode);
shards.put(hash, virtualNode);
System.out.println("虚拟节点[" + virtualNode + "] hash:" + hash + ",被添加");
}
}
}
/**
* 删除节点
*
* @param node
*/
public static void delNode(String node) {
if (realNodes.contains(node)) {
realNodes.remove(node);
System.out.println("真实节点[" + node + "] 下线移除");
for (int i = 0; i < VIRTUAL_NUM; i++) {
String virtualNode = node + "&&VN" + i;
int hash = getHash(virtualNode);
shards.remove(hash);
System.out.println("虚拟节点[" + virtualNode + "] hash:" + hash + ",被移除");
}
}
}
/**
* FNV1_32_HASH算法
*/
private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++)
hash = (hash ^ str.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
// 如果算出来的值为负数则取其绝对值
if (hash < 0) hash = Math.abs(hash);
return hash;
}
public static void main(String[] args) {
//模拟客户端的请求
String[] nodes = {"127.0.0.1", "10.9.3.253", "192.168.10.1"};
for (String node : nodes) {
System.out.println("[" + node + "]的hash值为" + getHash(node) + ", 被路由到结点[" + getServer(node) + "]");
}
// 添加一个节点(模拟服务器上线)
addNode("192.168.1.7");
// 删除一个节点(模拟服务器下线)
delNode("192.168.1.2");
for (String node : nodes) {
System.out.println("[" + node + "]的hash值为" + getHash(node) + ", 被路由到结点[" + getServer(node) + "]");
}
}
}
动态分配权重
package com.grea.qz.config;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 个人那句不同的服务配置,动态分配权重
*/
public class ConsistencyHashingLoadFactor {
// 真实节点列表
private static List<Machine> realNodes = new ArrayList<>();
// 虚拟节点,key是Hash值,value是虚拟节点信息
private static SortedMap<Integer, String> shards = new TreeMap<>();
static {
realNodes.add(new Machine("192.168.1.1", LoadFactor.Memory8G));
realNodes.add(new Machine("192.168.1.2", LoadFactor.Memory16G));
realNodes.add(new Machine("192.168.1.3", LoadFactor.Memory32G));
realNodes.add(new Machine("192.168.1.4", LoadFactor.Memory16G));
for (Machine node : realNodes) {
for (int i = 0; i < node.getMemory().getVrNum(); i++) {
String server = node.getHost();
String virtualNode = server + "&&VN" + i;
int hash = getHash(virtualNode);
shards.put(hash, virtualNode);
}
}
}
/**
* 获取被分配的节点名
*
* @param node
* @return
*/
public static Machine getServer(String node) {
int hash = getHash(node);
Integer key;
SortedMap<Integer, String> subMap = shards.tailMap(hash);
if (subMap.isEmpty()) {
key = shards.lastKey();
} else {
key = subMap.firstKey();
}
String virtualNode = shards.get(key);
String realNodeName = virtualNode.substring(0, virtualNode.indexOf("&&"));
for (Machine machine : realNodes) {
if (machine.getHost().equals(realNodeName)) {
return machine;
}
}
return null;
}
/**
* 添加节点
*
* @param node
*/
public static void addNode(Machine node) {
if (!realNodes.contains(node)) {
realNodes.add(node);
System.out.println("真实节点[" + node + "] 上线添加");
for (int i = 0; i < node.getMemory().getVrNum(); i++) {
String virtualNode = node.getHost() + "&&VN" + i;
int hash = getHash(virtualNode);
shards.put(hash, virtualNode);
System.out.println("虚拟节点[" + virtualNode + "] hash:" + hash + ",被添加");
}
}
}
/**
* 删除节点
*
* @param node
*/
public static void delNode(Machine node) {
String host = node.getHost();
Iterator<Machine> it = realNodes.iterator();
while (it.hasNext()) {
Machine machine = it.next();
if (machine.getHost().equals(host)) {
it.remove();
System.out.println("真实节点[" + node + "] 下线移除");
for (int i = 0; i < node.getMemory().getVrNum(); i++) {
String virtualNode = node.getHost() + "&&VN" + i;
int hash = getHash(virtualNode);
shards.remove(hash);
System.out.println("虚拟节点[" + virtualNode + "] hash:" + hash + ",被移除");
}
}
}
}
/**
* FNV1_32_HASH算法
*/
private static int getHash(String str) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < str.length(); i++)
hash = (hash ^ str.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
// 如果算出来的值为负数则取其绝对值
if (hash < 0)
hash = Math.abs(hash);
return hash;
}
public static void main(String[] args) {
// 模拟客户端的请求
String[] nodes = {"127.0.0.1", "10.9.3.253", "192.168.10.1"};
for (String node : nodes) {
System.out.println("[" + node + "]的hash值为" + getHash(node) + ", 被路由到结点[" + getServer(node) + "]");
}
// 添加一个节点(模拟服务器上线)
addNode(new Machine("192.168.1.7", LoadFactor.Memory16G));
// 删除一个节点(模拟服务器下线)
delNode(new Machine("192.168.1.1", LoadFactor.Memory8G));
for (String node : nodes) {
System.out.println("[" + node + "]的hash值为" + getHash(node) + ", 被路由到结点[" + getServer(node) + "]");
}
}
}
/**
* 机器类
*
* @author yangkuanjun
*/
class Machine {
private String host;
private LoadFactor memory;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public LoadFactor getMemory() {
return memory;
}
public void setMemory(LoadFactor memory) {
this.memory = memory;
}
public Machine(String host, LoadFactor memory) {
super();
this.host = host;
this.memory = memory;
}
@Override
public String toString() {
return "Machine [host=" + host + ", memory=" + memory + "]";
}
}
/**
* 负载因子
*
* @author yangkuanjun
*/
enum LoadFactor {
Memory8G(5), Memory16G(10), Memory32G(20);
private int vrNum;
LoadFactor(int vrNum) {
this.vrNum = vrNum;
}
public int getVrNum() {
return vrNum;
}
}