提示:下方有源代码地址,请自行拿取java
在互联网项目中比较经常使用到的关系型数据库是MySQL,随着用户和业务的增加,传统的单库单表模式难以知足大量的业务数据存储以及查询,单库单表中大量的数据会使写入、查询效率很是之慢,此时应该采起分库分表策略来解决。node
提示:如下是本篇文章正文内容,案例仅供参考git
假设目前有一个电商系统使用的是MySQL,要设计大数据量存储、高并发、高性能可扩展的方案,数据库中有用户表。用户会很是多,而且要实现高扩展性,你会怎么去设计? OK我们先看传统的分库分表方式 算法
固然还有些小伙伴知道按照省份/地区或必定的业务关系进行数据库拆分 OK,问题来了,如何保证合理的让数据存储在不一样的库不一样的表里呢?让库减小并发压力?应该怎么去制定分库分表的规则?不用急,这不就来了数据库
第一种方法们能够指定一个数据范围来进行分表,例如从1~1000000,1000001-2000000,使用一百万一张表的方式,以下图所示缓存
固然这种方法须要维护表的ID,特别是分布式环境下,这种分布式ID,在不使用第三方分表工具的状况下,建议使用Redis,Redis的incr操做能够轻松的维护分布式的表ID。服务器
RANGE方法优势: 扩容简单,提早建好库、表就好markdown
RANGE方法缺点: 大部分读和写都访会问新的数据,有IO瓶颈,这样子形成新库压力过大,不建议采用。架构
针对上述RANGE方式分表有IO瓶颈的问题,我们能够采用根据用户ID HASG取模的方式进行分库分表,如图所示: 并发
这样就能够将数据分散在不一样的库、表中,避免了IO瓶颈的问题。
HASH取模方法优势: 能保证数据较均匀的分散落在不一样的库、表中,减轻了数据库压力
HASH取模方法缺点: 扩容麻烦、迁移数据时每次都须要从新计算hash值分配到不一样的库和表
经过HASH取模也不是最完美的办法,那什么才是呢?
使用一致性HASH算法能完美的解决问题
普通HASH算法:
普通哈希算法将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值。哈希值是一段数据惟一且极其紧凑的数值表示形式。
普通的hash算法在分布式应用中的不足:在分布式的存储系统中,要将数据存储到具体的节点上,若是咱们采用普通的hash算法进行路由,将数据映射到具体的节点上,如key%n,key是数据的key,n是机器节点数,若是有一个机器加入或退出集群,则全部的数据映射都无效了,若是是持久化存储则要作数据迁移,若是是分布式缓存,则其余缓存就失效了。
一致性HASH算法: 按照经常使用的hash算法来将对应的key哈希到一个具备2^32次方个节点的空间中,即0~ (2^32)-1的数字空间中。如今咱们能够将这些数字头尾相连,想象成一个闭合的环形,以下图所示。
这个圆环首尾相连,那么假设如今有三个数据库服务器节点node一、node二、node3三个节点,每一个节点负责本身这部分的用户数据存储,假设有用户user一、user二、user3,咱们能够对服务器节点进行HASH运算,假设HASH计算后,user1落在node1上,user2落在node2上,user3落在user3上
OK,如今我们假设node3节点失效了
user3将会落到node1上,而以前的node1和node2数据不会改变,再假设新增了节点node4
你会发现user3会落到node4上,你会发现,经过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,仍是数据的迁移达到了最小,这样的算法对分布式集群来讲是很是合适的,避免了大量数据迁移,减少了服务器的的压力。
固然还有一个问题还须要解决,那就是平衡性。从图咱们能够看出,当服务器节点比较少的时候,会出现一个问题,就是此时必然形成大量数据集中到一个节点上面,极少数数据集中到另外的节点上面。
为了解决这种数据倾斜问题,一致性哈希算法引入了虚拟节点机制,即对每个服务节点计算多个哈希,每一个计算结果位置都放置一个节点,称为虚拟节点。具体作法能够先肯定每一个物理节点关联的虚拟节点数量,而后在ip或者主机名后面增长编号。例如上面的状况,能够为每台服务器计算三个虚拟节点,因而能够分别计算 “node 1-1”、“node 1-2”、“node 1-3”、“node 2-1”、“node 2-2”、“node 2-3”、“node 3-1”、“node 3-2”、“node 3-3”的哈希值,这样造成九个虚拟节点
例如user1定位到node 1-一、node 1-二、node 1-3上其实都是定位到node1这个节点上,这样可以解决服务节点少时数据倾斜的问题,固然这个虚拟节点的个数不是说固定三个或者至多、至少三个,这里只是一个例子,具体虚拟节点的多少,须要根据实际的业务状况而定。
一致性HASH方法优势: 经过虚拟节点方式能保证数据较均匀的分散落在不一样的库、表中,而且新增、删除节点不影响其余节点的数据,高可用、容灾性强。
一致性取模方法缺点: 嗯,比起以上两种,能够认为没有。
OK,不废话,接下来上单元测试,假设有三个节点,每一个节点有三个虚拟节点的状况
package com.hyh.core.test;
import com.hyh.utils.common.StringUtils;
import org.junit.Test;
import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 一致性HASH TEST
*
* @Author heyuhua
* @create 2021/1/31 19:50
*/
public class ConsistentHashTest {
//待添加入Hash环的服务器列表
private static String[] servers = {"192.168.5.1", "192.168.5.2", "192.168.5.3"};
//真实结点列表,考虑到服务器上线、下线的场景,即添加、删除的场景会比较频繁,这里使用LinkedList会更好
private static List<String> realNodes = new LinkedList<>();
//虚拟节点,key表示虚拟节点的hash值,value表示虚拟节点的名称
private static SortedMap<Integer, String> virtualNodes = new TreeMap<>();
//一个真实结点对应3个虚拟节点
private static final int VIRTUAL_NODES = 3;
/**
* 测试有虚拟节点的一致性HASH
*/
@Test
public void testConsistentHash() {
initNodes();
String[] users = {"user1", "user2", "user3", "user4", "user5", "user6", "user7", "user8", "user9"};
for (int i = 0; i < users.length; i++)
System.out.println("[" + users[i] + "]的hash值为" +
getHash(users[i]) + ", 被路由到结点[" + getServer(users[i]) + "]");
}
/**
* 先把原始的服务器添加到真实结点列表中
*/
public void initNodes() {
for (int i = 0; i < servers.length; i++)
realNodes.add(servers[i]);
for (String str : realNodes) {
for (int i = 0; i < VIRTUAL_NODES; i++) {
String virtualNodeName = str + "-虚拟节点" + String.valueOf(i);
int hash = getHash(virtualNodeName);
System.out.println("虚拟节点[" + virtualNodeName + "]被添加, hash值为" + hash);
virtualNodes.put(hash, virtualNodeName);
}
}
System.out.println();
}
//使用FNV1_32_HASH算法计算服务器的Hash值,这里不使用重写hashCode的方法,最终效果没区别
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;
}
//获得应当路由到的结点
private static String getServer(String key) {
//获得该key的hash值
int hash = getHash(key);
// 获得大于该Hash值的全部Map
SortedMap<Integer, String> subMap = virtualNodes.tailMap(hash);
String virtualNode;
if (subMap.isEmpty()) {
//若是没有比该key的hash值大的,则从第一个node开始
Integer i = virtualNodes.firstKey();
//返回对应的服务器
virtualNode = virtualNodes.get(i);
} else {
//第一个Key就是顺时针过去离node最近的那个结点
Integer i = subMap.firstKey();
//返回对应的服务器
virtualNode = subMap.get(i);
}
//virtualNode虚拟节点名称要截取一下
if (StringUtils.isNotBlank(virtualNode)) {
return virtualNode.substring(0, virtualNode.indexOf("-"));
}
return null;
}
}
复制代码
这里模拟9个用户对象hash后被路由的状况,看下结果
分库分表在分布式微服务架构环境下建议强烈使用一致性HASH算法来作,固然分布式环境下也会产生业务数据数据一致性、分布式事务问题,下期我们再来探讨数据一致性、分布式事务的解决方案
路漫漫其修远兮,吾愿与君上下而求索,感谢各位帅哥、靓妹的点赞、收藏和评论,咱们下期见。
关注我带你走进架构师的成长之路。
源码地址:点此查看源码.