分库分表
分库分表?为什么分库、分表?问题及解决?实现方式?中间件选择?
解决单一数据库和数据表在高并发和大数据量场景下的性能瓶颈问题。
单库性能瓶颈,磁盘容量不足,并发连接数不足
单表查询效率低,大量并发避免表锁竞争
分库
跨库事务问题:分布式事务
跨库无法JOIN问题:
分多次查询,业务代码中关联
字段冗余。避免关联操作。
通过binlog同步到ES中查询。
每个库中都保存所有模块都依赖到的全局表
借助ETL工具定时将关联表表做数据同步到聚合表
分表
跨节点的count,order by,group by等聚合函数问题:分多次查询,业务代码中聚合
数据迁移,容量规划,是否再次扩容等问题:
ID问题:
自增设置不同步长。比如3张表,步长为3,3张表的ID增长是1、4、7;2、5、8;3、6、9,不重复
UUID,简单,但不连续的主键插入导致页分裂,性能差
分布式ID
分页问题
全局视野法:分多次查询,业务代码中分页。数据比较准确;缺点是并发度低,查询慢
业务折衷法:禁止跳页查询,只有上一页和下一页,查询第一页时跟全局视野法一样。下一页则每个节点都查询大于上一页最大创建时间或者id的数据然后汇总,内存排序返回
Client模式、Proxy 模式
Sharding-JDBC:当当开源 client模式
cobar:阿里 Proxy方式 必须将拆分后的表分别放入不同的库
MyCat:开源 Proxy模式
TDDL(淘宝):Client模式
vitess:谷歌 集群基于Zookeeper管理,通过RPC方式处理数据,可支撑高流量
Sharding Sphere推荐,提供三种模式
Sharding-JDBC Client模式,不用部署,运维成本低,不需要代理层的二次转发请求,性能高,但是需要各个系统都重新升级版本再发布,都要耦合sharding-jdbc的依赖
Sharding-Proxy Proxy模式,跨语言
Sharding-Sidecar模式
垂直分库(表)、水平分库(表)区别?怎么分库分表?√如何选择分表键?分表策略?水平分表的路由方式?非分表键如何查询/查询条件不带分片键怎么办?如何避免热点问题数据倾斜?
水平分库(表):以字段为依据,按照一定策略(hash、range等),将一个库(表)中的数据拆分到多个库(表)中。每个库(表)的结构都一样,数据不一样,没有交集 id 1-1000
垂直分库:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。每个库的表结构、数据都不一样,没有交集。按照用户、商品、订单分到不同的库中。
垂直分表:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。每个表的结构、数据都不一样,每个表的字段至少有一列交集(主键)用于关联数据
主键ID:用户表
时间戳create_time:日志、订单表
路由:数据应该分到哪一张表。
根据范围:根据分片键的取值范围划分。同一年或者1-1000
优点:扩容简单。只需增加新表,不修改原数据
缺点:分段太小导致表数量多维护难;太大导致单表依然存在性能问题,建议100-2000w;数据分布不均匀(表数量大小不一)和热点问题(高频查询数量大的表)
使用范围+hash哈希取模结合的分表策略解决,分库用范围,比如id在0~4000、4000~8000万分到订单库1和2,扩容时id在8000万~1.2亿的数据分到订单库3。然后订单库内用hash取模划分不同表
根据Hash取模:选取某个列或者某几个列组合的值进行Hash运算后对数据库数取余分到不同数据库表
表数量多维护难,太少导致单表依然存在性能问题
优点:表分布均匀,不存在明显热点问题。
缺点:规划不好要扩容迁移需要二次分表,表数量增加导致重新计算位置并移动数据
一致性Hash:解决了扩容迁移需要二次分表问题
将哈希值空间组织成环,节点映射到环上。数据项映射到顺时针的第一个节点上。扩缩容只影响哈希环中相邻节点,减少数据迁移。但节点分布不均匀和节点故障数据迁移导致负载不均匀
Guava //bucket范围在0 ~ buckets之间int bucket = Hashing.consistentHash(id, buckets)
demo
public class Main {
//真实节点
private static String[] serverIpArray = new String[]{"192.168.1.100", "47.100.61.70"};
//虚拟节点
private static TreeMap<Integer, String> virtualNodeMap;
static {
virtualNodeMap = new TreeMap<>();
//默认为每个真实节点生成3个虚拟节点
for (String realIp : serverIpArray) {
for (int i = 0; i < 3; i++) {
//加上随机数使得虚拟节点的hash值更加分散,实际情况中,虚拟节点的hash值需要固定
String virtualIp = new Random().nextInt(10000) + "#" + realIp;
virtualNodeMap.put(getHash(virtualIp), realIp);
}
}
}
//获取ip的哈希值,可以有多种算法实现
//尽量避免ip相近,hash相近的情况
private static int getHash(String ip) {
int hashCode = Math.abs(ip.hashCode());
System.out.println(ip + ":" + hashCode);
return hashCode;
}
//由客户端获取最近的虚拟节点,返回虚拟节点对应的真实节点
private static String getRealServerIp(String client) {
int clientHash = getHash(client);
Integer higherKey = virtualNodeMap.higherKey(clientHash);
if (higherKey == null) {
//返回hash环中的第一个虚拟ip
return virtualNodeMap.get(virtualNodeMap.firstKey());
}
//返回比客户端的哈希值稍微大一点的虚拟ip
return virtualNodeMap.get(higherKey);
}
public static void main(String[] args) {
String[] clientIpArray = new String[]{"电脑", "平板", "手机"};
for (String client : clientIpArray) {
String realIp = getRealServerIp(client);
System.out.println(client + "连接到了" + realIp);
}
}
}
假设用户表根据userId做分表。但是用户根据手机号来登陆,而手机号是非分表键。
将用户信息冗余同步到ES查询(推荐)
基因法:非分表键解析出分表键,如订单号包含客户号。手机号似乎不适合冗余userId。
映射关系:创建映射表,只有分表键、非分表键两列字段。查询时先从映射表获得非分表键对应的分表键,然后再使用非分表键+分表键去查询对应的表。映射表可以改成缓存到Redis中
如何迁移到分库分表?/不停机扩容?怎么验数据一致性?评估分库数量?
停止部署法。凌晨停机,写迁移程序读旧数据库数据,通过中间件写入到分库分表中,结束后校验迁移前后一致性,没问题就迁移业务到新库
双写部署法,基于业务层/binlog
根据主键或者创建时间大小将表数据区分为历史数据和增量数据
读操作保持不变,将写操作写入到消息队列(新库)中和旧库中,
写迁移程序(1.查出最大id 2.每次取id>? and id <?+step步长的数据写入到新库)将表历史数据迁移到新库
写程序订阅消息队列/binlog中的数据通过中间件写入新库
使用定时任务检查新老库数据一致性,把差异补齐
没问题后读操作切换到新库
没问题后去除双写代码,将写操作指向新库
过段时间,确定旧库没有请求之后下线老库
先验数量,因为比较快。
验关键性的几个字段是否一致。如一次取50条,然后拼在一起用md5进行加密得到新字符串。比较新库两个字符串值是否一致。一致则继续比较下50条数据。不一致则用二分法确定不一致的数据在0-25条,还是26条-50条。以此类推找出不一致的数据并记录
MySQL单库不超过5千万条。分4~10个库