目录
背景
在Redis集群模式(cluster)中,多台Redis共同存储键值对时,当单台Redis宕机时,如何将其存储的键值对转移至集群中别的节点?
解决方式
直接取模
分布式数据存储时,经常要考虑数据分片,避免将大量的数据放在单表或单库中,造成查询等操作的耗时过长。比如,存储订单数据时使用三个mysql库(编号0,1,2),当一条订单数据过来时,对订单id求hash后与机器数量取模,hash(orderId) % 3,假如得到的结果是2,则这条数据会存储到编号为2的mysql中。分表分库存储时,根据数据库的主键或唯一键做hash,然后跟数据库机器的数量取模,从而决定该条数据放在哪个库中。
不足
每次Redis数量变化时之前的数据就需要重新hash做一次sharding。这种操作会导致服务在一定的时间不可用,而且每次扩缩容都会存在这个问题,十分耗费时间和资源
更好的策略
一致性哈希
一致性哈希
一致性哈希(Consistent Hashing)是一种分布式哈希算法,它利用哈希函数将数据分散到不同的节点上,同时保证在节点动态添加或删除时,数据尽可能地少受影响。
在分布式系统中,数据通常会被分散到不同的节点上进行处理和存储。常见的哈希算法会将数据映射到一个固定的节点上,当节点动态添加或删除时,会导致大量的数据迁移和重分布,造成系统性能的下降。
而一致性哈希算法不同于传统的哈希算法,它将数据映射到一个环形空间上,每个节点都对应一个点在环形空间上的位置。当有新的数据需要存储时,先通过哈希函数将数据映射到环形空间上的某一点,然后再将该数据存储到该点的后面的第一个节点上。如果节点动态添加或删除,数据只需要重新计算一小部分,而不需要全部重新计算,因此大大减小了数据迁移和重分布的成本,提高了系统的性能和可扩展性。
因此,一致性哈希算法解决了分布式系统中节点动态添加和删除时数据迁移和重分布的问题,减小了系统的负载和成本,提高了系统的性能和可扩展性。
主要思想
“存储节点” 和 “数据” 都映射到一个首尾相连的大小为2^31的hash环上,将数据从所在位置顺时针找第一台遇到的服务器节点,这个节点就是该key存储的服务器。因为是环形空间,0和2^32-1是重叠的。如果增删节点,仅影响该节点在hash环上顺时针相邻的后继节点,其他数据不会受到影响
容错性和可扩展性
无论新增还是减少一个节点,只会影响当前节点相邻的两个节点,并不改变其余节点及其相应的数据
数据倾斜
一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题,例如系统中只有两台服务器,此时必然造成大量数据集中到Node 2上,而只有极少量会定位到Node 1上。
为了解决数据倾斜问题,一致性Hash算法引入了虚拟节点机制,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在主机名的后面增加编号来实现。例如上面的情况,可以为每台服务器计算三个虚拟节点,于是可以分别计算 “Node 1#1”、“Node 1#2”、“Node 1#3”、“Node 2#1”、“Node 2#2”、“Node 2#3”的哈希值,于是形成六个虚拟节点
- 对于hash环来说,节点越多,数据分布越平稳。所以采用虚拟节点的方式,将一个节点虚拟成多个节点,保证环上有1000~2000个节点最佳。
- 一般10个Redis服务器的集群,每个节点可以虚拟100-200个节点,保证环上有1000-2000个节点
应用方式
通过集群前置的代理或客户端实现,目前有两种方法:
- 在redis的jedis客户端jar包就是实现了一致性hash算法(客户端模式)
- 在redis集群前面加上一层前置代理如Twemproxy也实现了hash一致性算法(代理模式)
实现代码
public class ConsistentHash {
// 环形空间的大小
private int circleSize;
// 哈希函数
private HashFunction hashFunction;
// 存储每个节点的信息
private List<Node> nodeList;
public ConsistentHash(List<Node> nodeList, HashFunction hashFunction, int circleSize) {
this.nodeList = new ArrayList<>(nodeList);
this.hashFunction = hashFunction;
this.circleSize = circleSize;
}
// 将数据存储到某个节点中
public void storeData(String data) {
int hash = hashFunction.hash(data);
int pos = findPosition(hash);
Node node = nodeList.get(pos);
node.storeData(data);
}
// 查找位置
private int findPosition(int hash) {
int pos = binarySearch(hash);
if (pos == nodeList.size()) {
pos = 0;
}
return pos;
}
// 二分查找
private int binarySearch(int hash) {
int start = 0;
int end = nodeList.size() - 1;
while (start <= end) {
int mid = (start + end) / 2;
int nodeHash = nodeList.get(mid).getHash();
if (nodeHash == hash) {
return mid;
} else if (nodeHash < hash) {
start = mid + 1;
} else {
end = mid - 1;
}
}
return start;
}
}
class HashFunction {
// 哈希函数
public int hash(String data) {
return Objects.hash(data);
}
}
class Node {
private int hash;
private List<String> dataList;
public Node(int hash) {
this.hash = hash;
this.dataList = new ArrayList<>();
}
public int getHash() {
return hash;
}
public void storeData(String data) {
dataList.add(data);
System.out.println("Data \"" + data + "\" is stored at Node " + hash);
}
}
class Test {
public static void main(String[] args) {
// 创建5个节点
List<Node> nodeList = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
Node node = new Node(i);
nodeList.add(node);
}
// 创建一致性哈希
ConsistentHash consistentHash = new ConsistentHash(nodeList, new HashFunction(), 100);
// 存储数据
consistentHash.storeData("data1");
consistentHash.storeData("data2");
consistentHash.storeData("data3");
}
}