欢迎我们重视大众号「JAVA前哨」检查更多精彩共享文章,首要包括源码剖析、实际使用、架构思想、职场共享、产品考虑等等,一同欢迎我们加我个人微信「java_front」一同交流学习

1 理论知识

1.1 分库分表是否必要

分库分表的确能够处理单表数据量大这个问题,可是并非首选。由于分库分表至少引入了三个有必要处理的杰出问题。

榜首是分库分表计划自身具有的杂乱性。第二是本地事务失效问题,原本在同一个数据库中能够保证强一致性事务逻辑,分库之后事务失效。第三是难以聚合查询问题,由于分库分表后查询条件中有必要带有shardingKey,所以约束了很多查询场景。

咱们在之前文章《面试官问单表数据量大是否有必要分库分表》介绍过处理单表数据量过大问题,能够依照删、换、分、拆、异、热这六个字次序进行处理,而不是一上来就分库分表。

删是指删除历史数据并进行归档。换是指不要只使用数据库资源,有些数据能够存储至其它代替资源。分是指读写分离,添加多个读实例应对读多写少的互联网场景。拆是指分库分表,将数据涣散至不同的库表中减轻压力。异指数据异构,将一份数据依据不同事务需求保存多份。热是指热点数据,这是一个十分值得注意的问题。

1.2 分库分表两大维度

假定有一个电商数据库寄存订单、产品、付出三张事务表。跟着事务量越来越大,这三张事务数据表也越来越大,查询功用明显降低,数据拆分势在必行,那么数据拆分能够从纵向和横向两个维度进行。

1.2.1 纵向拆分

纵向拆分便是依照事务拆分,咱们将电商数据库拆分成三个库,订单库、产品库。付出库,订单表在订单库,产品表在产品库,付出表在付出库。这样每个库只需求存储本事务数据,物理阻隔不会相互影响。

02 纵向分表.jpg

1.2.2 横向拆分

依照纵向拆分计划,现在咱们现已有三个库了,平稳运行了一段时刻。可是跟着事务增长,每个单库单表的数据量也越来越大,逐渐抵达瓶颈。

这时咱们就要对数据表进行横向拆分,所谓横向拆分便是依据某种规则将单库单表数据涣散到多库多表,从而减小单库单表的压力。

横向拆分战略有很多计划,最重要的一点是选好ShardingKey,也便是依照哪一列进行拆分,怎样分取决于咱们拜访数据的方式。

(1) 规模分片

假如咱们挑选的ShardingKey是订单创立时刻,那么分片战略是拆分四个数据库,别离存储每季度数据,每个库包括三张表,别离存储每个月数据:

03 横向分表_规模分表.jpg

这个计划的长处是对规模查询比较友爱,例如咱们需求统计榜首季度的相关数据,查询条件直接输入时刻规模即可。这个计划的问题是简略发生热点数据。例如双11当天下单量特别大,就会导致11月这张表数据量特别大从而造成拜访压力。

(2) 查表分片

查表法是依据一张路由表决议ShardingKey路由到哪一张表,每次路由时首要到路由表里查到分片信息,再到这个分片去取数据。咱们剖析一个查表法思想使用实际案例。

Redis官方在3.0版别之后供给了集群计划RedisCluster,其中引入了哈希槽(slot)这个概念。一个集群固定有16384个槽,在集群初始化时这些槽会平均分配到Redis集群节点上。每个key恳求终究落到哪个槽核算公式是固定的:

SLOT = CRC16(key) mod 16384

一个key恳求过来怎样知道去哪台Redis节点获取数据?这就要用到查表法思想:

(1) 客户端衔接任意一台Redis节点,假定随机拜访到节点A
(2) 节点A依据key核算出slot值
(3) 每个节点都维护着slot和节点映射联系表
(4) 假如节点A查表发现该slot在本节点,直接回来数据给客户端
(5) 假如节点A查表发现该slot不在本节点,回来给客户端一个重定向指令,告知客户端应该去哪个节点恳求这个key的数据
(6) 客户端向正确节点主张衔接恳求

查表法计划长处是能够灵敏制定路由战略,假如咱们发现有的分片现已成为热点则修正路由战略。缺陷是多一次查询路由表操作添加耗时,并且路由表假如是单点也或许会有单点问题。

(3) 哈希分片

现在比较盛行的分片办法是哈希分片,相较于规模分片,哈希分片能够较为均匀将数据涣散在数据库中。咱们现在将订单库拆分为4个库编号为[0,3],每个库包括3张表编号为[0,2],如下图如所示:

04 横向分表_哈希分表_1.jpg

咱们挑选使用orderId作为ShardingKey,那么orderId=100这个订单会保存在哪张表?由于是分库分表,榜首步确认路由到哪一个库,取模核算结果表明库表序号:

db_index = 100 % 4 = 0

第二步确认路由到哪一张表:

table_index = 100 % 3 = 1

第三步数据路由到0号库1号表:
04 横向分表_哈希分表_2.jpg

在实际开发中路由逻辑并不需求咱们手动完成,由于有许多开源框架经过装备就能够完成路由功用,例如ShardingSphere、TDDL框架等等。

2 分库分表准备工作

2.1 核算库表数量

分几个库和几张表是在分库分表工作开端前有必要要回答的问题,咱们首要看看阿里巴巴开发手册的主张:单表行数超过500万行或许单表容量超过2GB才引荐进行分库分表,假如估计3年后数据量底子达不到这个等级,请不要在创立表时就分库分表。

咱们提取出这个主张的两个关键词500万、3年作为预估库表数的基线,假定事务数据日增量60万,那么应该怎么预估需求分多少个库,多少张表呢?

日增量60万核算3年后数据总量:

三年数据总量 = 60 * 365 * 3 = 65700

跟着后续事务发展日增量会超过60万,所以咱们要对数据总量进行冗余,冗余指数是多少依据事务状况而定,本文依照3倍冗余:

三年数据总量三倍冗余 = 65700 * 3 = 197100

依照单表500万并向上取整至2的幂次核算表数量

表数量 = 197100 / 500 = 394.2 向上取整 = 512

一切表放在一个库并不适宜,由于跟着数据量增大,拜访并发量也会呈正相关增大,一个数据库实例是难以支撑的。本文依照一个数据库实例包括32张表核算库数量:

库数量 = 512 / 32 = 16

2.2 shardingKey

确认shardingKey十分关键,由于作为分片指标,当数据拆分至多个库表之后,署理层只能依据shardingKey进行表路由。假定咱们设置了userId作为shardingKey,那么后续DML操作都有必要包括userId字段。可是现在有一种场景只要orderId作为查询条件,那么咱们应该怎么处理这种场景呢?

榜首种计划是规划orderId包括userId相关特征,这样即使只要订单号作为查询条件,也能够截取userId特征进行分片:

订单号 = 毫秒数 + 版别号 + userId后六位 + 大局序列号

第二种计划是数据异构,核心思想是以空间换时刻,一份数据依据不同维度存储到多个数据介质,数据异构一般分为如下类型。

数据异构至MySQL:咱们能够挑选orderId作为shardingKey存储至另一个数据库实例,那么orderId就能够作为条件进行查询。

数据异构至ES:假如每一个维度都新建一个数据库实例也是不现实的,所以咱们能够将数据同步至ES满足多维度查询需求。

数据异构至Hive:MySQL和ES能够满足实时查询需求,Hive能够满足离线剖析需求,报表等数据剖析工作无需经过主库,而是能够经过Hive进行。

现在又引出一个新问题,事务不或许每次都将数据写入多个数据源,这样会带来功用问题和数据一致性问题,所以需求一个管道进行各数据源之间同步,阿里开源的canal组件能够处理这个问题。

3 分库分表实例

在完结准备工作之后,咱们能够开端分库分表工作了。分库分表办法有很多种,可是说到底都是在处理两类数据:存量和增量。存量表明旧数据库现已存在的数据,增量表明不存在于旧数据库待新增或许变更的数据。依据存量和增量这两种类型,咱们能够将分库分表办法分为停服拆分和不断服拆分。

3.1 停服拆分

停服是指停止服务,系统不再接纳新事务数据,那么旧数据在分库分表这个时刻段内是静止不变的,数据悉数变为了存量数据。停服拆分一般分为三个阶段。

榜首阶段首要编写署理层和新DAO,署理层经过开关决议拜访旧表仍是新表,此刻流量仍是悉数拜访旧表:

00 停服_阶段1.jpg

第二阶段停止服务,整个使用都没有流量,旧表数据现已处于静止状态,此刻经过脚本将存量数据从旧表搬迁至新表:

00 停服_阶段2.jpg

第三阶段经过署理层拜访新表,假如出现过错能够停服排查问题:

00 停服_阶段3.jpg

3.2 不断服拆分

停服拆分计划比较简略,可是在分表这段时刻没有事务流量,对事务是有损的,所以咱们一般采用不断服拆分计划,一边有流量拜访,一边进行分库分表,此刻数据不只有存量还有增量,相对而言会杂乱一些。

榜首阶段首要编写署理层和新DAO,署理层经过开关决议拜访旧表仍是新表,此刻流量仍是悉数拜访旧表:

01 不断服_阶段1.jpg

第二阶段敞开双写,增量数据不只在旧表新增和修正,也在新表新增和修正,日志或许临时表记录下写入新表ID起始值,旧表中小于这个值的数据便是存量数据:

02 不断服_阶段2.jpg

第三阶段存量数据搬迁,经过脚本将存量数据写入新表:

03 不断服_阶段3.jpg

第四阶段停读旧表改读新表,此刻新表现已承载了一切读写事务,可是不要马上停写旧表,需求坚持双写一段时刻。

不断写旧表有两个原因:榜首是由于假如读新表出现问题,还能够将读流量切回旧表。第二是由于能够进行数据校正,例如新表和旧表数据都同步至Hive,选取几天的数据进行校正,从而验证数据同步的准确性。

04 不断服_阶段4.jpg

第五阶段当读写新表一段时刻之后,没有发生事务问题,能够停写旧表:

05 不断服_阶段5.jpg

3.3 署理层完成

署理层完成了新旧数据源切换,需求尽量削减事务层代码的侵入性,而适配器形式能够有用削减对事务层的侵入性。咱们首要看看旧数据拜访目标和事务服务:

// 订单数据目标
public class OrderDO {
    private String orderId;
    private Long price;
    public String getOrderId() {
        return orderId;
    }
    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }
    public Long getPrice() {
        return price;
    }
    public void setPrice(Long price) {
        this.price = price;
    }
}
// 旧DAO
public interface OrderDAO {
    public void insert(OrderDO orderDO);
}
// 事务服务
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderDAO orderDAO;
    @Override
    public String createOrder(Long price) {
        String orderId = "orderId_123";
        OrderDO orderDO = new OrderDO();
        orderDO.setOrderId(orderId);
        orderDO.setPrice(price);
        orderDAO.insert(orderDO);
        return orderId;
    }
}

引入新数据源拜访目标:

// 新数据目标
public class OrderNewDO {
    private String orderId;
    private Long price;
}
// 新DAO
public interface OrderNewDAO {
    public void insert(OrderNewDO orderNewDO);
}

适配器形式削减事务代码侵入性:

// 署理层
public class OrderDAOProxy implements OrderDAO {
    private OrderDAO orderDAO;
    private OrderNewDAO orderNewDAO;
    public OrderDAOProxy(OrderDAO orderDAO, OrderNewDAO orderNewDAO) {
        this.orderDAO = orderDAO;
        this.orderNewDAO = orderNewDAO;
    }
    @Override
    public void insert(OrderDO orderDO) {
        if(ApolloConfig.routeNewDB) {
            OrderNewDO orderNewDO = new OrderNewDO();
            orderNewDO.setPrice(orderDO.getPrice());
            orderNewDO.setOrderId(orderDO.getOrderId());
            orderNewDAO.insert(orderNewDO);
        } else {
            orderDAO.insert(orderDO);
        }
    }
}
// 事务服务
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderDAO orderDAO;
    @Resource
    private OrderNewDAO orderNewDAO;
    @Override
    public String createOrder(Long price) {
        String orderId = "orderId_123";
        OrderDO orderDO = new OrderDO();
        orderDO.setOrderId(orderId);
        orderDO.setPrice(price);
        new OrderDAOProxy(orderDAO, orderNewDAO).insert(orderDO);
        return orderId;
    }
}

4 文章总结

分库分表具有三个有必要面对的问题:计划自身杂乱性、本地事务失效问题、难以聚合查询问题,所以分库分表计划并非处理海量数据问题的首选。

假如有必要分库分表,首要进行容量预估并挑选适宜的shardingKey,其次依据实际事务挑选停服或许不断服计划,假如挑选不断服计划,注意坚持新表和旧表双写一段时刻,从而验证数据准确性,希望本文对我们有所帮助。

5 延伸阅读

一种简略可落地的分布式事务计划

面试官问单表数据量大是否有必要分库分表

欢迎我们重视大众号「JAVA前哨」检查更多精彩共享文章,首要包括源码剖析、实际使用、架构思想、职场共享、产品考虑等等,一同欢迎我们加我个人微信「java_front」一同交流学习