前几篇文章主要介绍了Springboot+Sharding-JDBC在分库分表中的实践,那么在实际场景中,我们可能会有需求对已经分表的表节点进行扩容。那么在分表算法为求余的情况下,如果增加一个节点,会导致大部分已存在的数据多要进行迁移,工程量巨大。
那除了求余分表算法外,还有其他算法能在缩扩容场景下更好的工作吗?!接下来我们一起看下一致性Hash算法在分库分表中的应用。
一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,目的是解决分布式缓存的问题。 在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题
一致性哈希算法将整个哈希值空间映射成一个虚拟的圆环,整个哈希空间的取值范围为0— 2 32 2^{32} 232-1。整个空间按顺时针方向组织。0— 2 32 2^{32} 232在零点中方向重合。接下来使用Hash算法对服务请求进行映射,将服务请求使用哈希算法算出对应的hash值,然后根据hash值的位置沿圆环顺时针查找,第一台遇到的服务器就是所对应的处理请求服务器。当增加一台新的服务器,受影响的数据仅仅是新添加的服务器到其环空间中前一台的服务器(也就是顺着逆时针方向遇到的第一台服务器)之间的数据,其他都不会受到影响。综上所述,一致性哈希算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性;
那么在分表应用中,如下图,我们先对table1、table2、table3、table4进行hash计算得到key1、key2、key3、key4并映射到范围为0— 2 32 2^{32} 232-1的环中;加入此时我们是通过id进行分表操作,那么在存储时,我们先对id进行hash计算得出hahs值(hash(id1)),得到的值沿圆环顺时针查找遇到的第一个表节点,即是数据存储的真实表结点;接下来我们看下在Sharding-jdbc中的实际应用
public class ConsistentHashAlgorithm {
//虚拟节点,key表示虚拟节点的hash值,value表示虚拟节点的名称
@Getter
private SortedMap<Long, String> virtualNodes = new TreeMap<>();
//当节点的数目很少时,容易造成数据的分布不均,所以增加虚拟节点来平均数据分布
//虚拟节点的数目;虚拟节点的生成主要是用来让数据尽量均匀分布
//虚拟节点是真实节点的不同映射而已
//比如真实节点user1的hash值为100,那么我们增加3个虚拟节点user1-1、user1-2、user1-3,分别计算出来的hash值可能就为200,345,500;通过这种方式来将节点分布均匀
private static final int VIRTUAL_NODES = 3;
public ConsistentHashAlgorithm() {
}
public ConsistentHashAlgorithm(SortedMap<Long, String> virtualTableNodes, Collection<String> tableNodes) {
if (Objects.isNull(virtualTableNodes)) {
virtualTableNodes = initNodesToHashLoop(tableNodes);
}
this.virtualNodes = virtualTableNodes;
}
public SortedMap<Long, String> initNodesToHashLoop(Collection<String> tableNodes) {
SortedMap<Long, String> virtualTableNodes = new TreeMap<>();
for (String node : tableNodes) {
for (int i = 0; i < VIRTUAL_NODES; i++) {
String s = String.valueOf(i);
String virtualNodeName = node + "-VN" + s;
long hash = getHash(virtualNodeName);
virtualTableNodes.put(hash, virtualNodeName);
}
}
return virtualTableNodes;
}
/**
* 通过计算key的hash
* 计算映射的表节点
*
* @param key
* @return
*/
public String getTableNode(String key) {
String virtualNode = getVirtualTableNode(key);
//虚拟节点名称截取后获取真实节点
if (StringUtils.isNotBlank(virtualNode)) {
return virtualNode.substring(0, virtualNode.indexOf("-"));
}
return null;
}
/**
* 获取虚拟节点
* @param key
* @return
*/
public String getVirtualTableNode(String key) {
long hash = getHash(key);
// 得到大于该Hash值的所有Map
SortedMap<Long, String> subMap = virtualNodes.tailMap(hash);
String virtualNode;
if (subMap.isEmpty()) {
//如果没有比该key的hash值大的,则从第一个node开始
Long i = virtualNodes.firstKey();
//返回对应的服务器
virtualNode = virtualNodes.get(i);
} else {
//第一个Key就是顺时针过去离node最近的那个结点
Long i = subMap.firstKey();
//返回对应的服务器
virtualNode = subMap.get(i);
}
return virtualNode;
}
/**
* 使用FNV1_32_HASH算法计算key的Hash值
*
* @param key
* @return
*/
public long getHash(String key) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < key.length(); i++)
hash = (hash ^ key.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;
}
}
该步骤会在应用启动时初始化分表的表结点,提前进行hash计算
public class InitTableNodesToHashLoop {
@Resource
private ShardingDataSource shardingDataSource;
@Getter
private HashMap<String, SortedMap<Long, String>> tableVirtualNodes = new HashMap<>();
@PostConstruct
public void init() {
try {
ShardingRule rule = shardingDataSource.getRuntimeContext().getRule();
Collection<TableRule> tableRules = rule.getTableRules();
ConsistentHashAlgorithm consistentHashAlgorithm = new ConsistentHashAlgorithm();
for (TableRule tableRule : tableRules) {
String logicTable = tableRule.getLogicTable();
tableVirtualNodes.put(logicTable,
consistentHashAlgorithm.initNodesToHashLoop(
tableRule.getActualDataNodes()
.stream()
.map(DataNode::getTableName)
.collect(Collectors.toList()))
);
}
} catch (Exception e) {
log.error("分表节点初始化失败 {}", e);
}
}
}
public class ConsistentShardingAlgorithm
implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {
/**
* 精确分片
* 一致性hash算法
*/
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
//获取已经初始化的分表节点
InitTableNodesToHashLoop initTableNodesToHashLoop =
SpringContextUtils.getBean(InitTableNodesToHashLoop.class);
if (CollectionUtils.isEmpty(availableTargetNames)) {
return shardingValue.getLogicTableName();
}
//这里主要为了兼容当联表查询时,如果两个表非关联表则
//当对副表分表时shardingValue这里传递进来的依然是主表的名称,
//但availableTargetNames中确是副表名称,所有这里要从availableTargetNames中匹配真实表
ArrayList<String> availableTargetNameList = new ArrayList<>(availableTargetNames);
String logicTableName = availableTargetNameList.get(0).replaceAll("[^(a-zA-Z_)]", "");
SortedMap<Long, String> tableHashNode =
initTableNodesToHashLoop.getTableVirtualNodes().get(logicTableName);
ConsistentHashAlgorithm consistentHashAlgorithm = new ConsistentHashAlgorithm(tableHashNode,
availableTargetNames);
return consistentHashAlgorithm.getTableNode(String.valueOf(shardingValue.getValue()));
}
/**
* 范围查询规则
* 可以根据实际场景进行修改
* Sharding.
*
* @param availableTargetNames available data sources or tables's names
* @param shardingValue sharding value
* @return sharding results for data sources or tables's names
*/
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> shardingValue) {
return availableTargetNames;
}
}
tables:
t_user: #t_user表
key-generator-column-name: id #主键
actual-data-nodes: ds0.t_user${0..39} #数据节点,均匀分布
table-strategy: #分表策略使用一致性hash算法
standard:
sharding-column: id
precise-algorithm-class-name: com.none.sharding.infrastruc.shardingAlgorithm.ConsistentShardingAlgorithm
以上即是一致性hash算法在分表分库中的实际应用;虽然一致性hash算法能在节点伸缩的时候尽量减少数据的迁移,但是当虚拟节点数量很多时依然会造成不少数据迁移,所以前期进行规划时一定要考虑虚拟节点的倍数设置。
Demo地址:Github