长文图解:单张表数据量太大问题怎么解决?请记住这六个字

欢迎你们关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思惟、职场分享、产品思考等等,同时欢迎你们加我微信「java_front」一块儿交流学习java


1 文章概述

在业务发展初期单表彻底能够知足业务需求,阿里巴巴开发手册也建议:单表行数超过500万行或者单表容量超过2GB才推荐进行分库分表,若是预计三年后数据量根本达不到这个级别请不要在建立表时就分库分表。redis

可是随着业务的发展和深刻,单表数据量不断增长,逐渐成为业务系统的瓶颈。这是为何呢?sql

从宏观层面分析任何物体都必然有其物理极限。例如1965年英特尔创始人摩尔预测:集成电路上可容纳的元器件的数目,约每隔24个月增长一倍,性能提高一倍,即计算机性能每两年翻一番。数据库

可是摩尔定律会有终点吗?有些科学家认为摩尔定律是有终点的:半导体芯片单位面积可集成的元件数量是有极限的,由于半导体芯片制程工艺的物理极限为2到3纳米。固然也有科学家不支持这种说法,可是咱们能够从中看出物理极限是很难突破的,当单表数据量达到必定规模时必然也达到极限。缓存

从细节层面分析数据保存在数据库,其实是保存在磁盘中,一次磁盘IO操做须要经历寻道、旋转延时、数据传输三个步骤,那么一次磁盘IO耗时公式以下:安全

单次IO时间 = 寻道时间 + 旋转延迟 + 传送时间
复制代码

整体来讲上述操做都较为耗时,速度和内存相比有着数量级的差距,当数据量一大磁盘这一瓶颈将更加明显。那么应该怎么办呢?处理单表数据量过大有如下六字口诀:删、换、分、拆、异、热。微信

删是指删除历史数据并进行归档。换是指不要只使用数据库资源,有些数据能够存储至其它替代资源。分是指读写分离,增长多个读实例应对读多写少的互联网场景。异指数据异构,将一份数据根据不一样业务需求保存多份。热是指热点数据是一个很是值得注意的问题。markdown


2 删

咱们分析这样一个场景:消费者会常常查询一年以前的订单记录吗?答案是通常不会,或者说这种查询需求量很小。根据上述分析那么一年前的数据咱们就没有必要放在单表这张业务主表,能够将一年前的数据迁移到历史归档表。架构

在查询历史数据表时,能够限制查询条件如必须选择日期范围,例如日期范围不能超过X个月等等从而减轻查询压力。并发

处理历史存量数据比较简单,由于存量数据通常是静态的,此时状态已经再也不改变了。数据处理通常分为如下两个步骤:

(1) 迁移一年前数据至历史归档表
(2) 根据主键分批删除主表数据
复制代码

不能一次性删除全部数据,由于数据量太大可能会引起超时,而是应该根据ID分批删除,例如每次删除500条数据。第一步查询一年前主键最大值和最小值,这是咱们须要删除的数据范围:

SELECT
MIN(id) AS minId, 
MAX(id) AS maxId 
FROM biz_table 
WHERE create_time < DATE_SUB(now(),INTERVAL 1 YEAR)
复制代码

第二步删除数据时不能一次性所有删掉,由于极可能会超时,咱们能够经过代码动态更新endId进行批量删除操做:

DELETE FROM biz_table 
WHERE id >= #{minId}
AND id <= #{maxId}
AND id <= #{endId}
LIMIT 500
复制代码

3 换

换是指换一个存储介质,固然并非说彻底替换,而是用其它存储介质对数据库作一个补充。例如海量流水记录,这类数据量级是巨量的,根本不适合存储在MySQL数据库中,那么这些数据能够存在哪里呢?

如今互联网公司通常都具有与之规模相对应的大数据服务或者平台,那么做为业务开发者要善于应用公司大数据能力,减轻业务数据库压力。


3.1 消息队列

这些海量数据能够存储至Kafka,由于其本质上就是分布式的流数据存储系统。使用Kafka有以下优势:

第一个优势是Kafka社区活跃功能强大,已经成为了一种事实上的工业标准。大数据不少组件都提供了Kafka接入组件,通过生产验证而且对接成本较小,能够为下游业务提供更多选择。

第二个优势是Kafka具备消息队列自己的优势例如解耦、异步和削峰。

假设这些海量数据都已经存储在Kafka,如今咱们但愿这些数据能够产生业务价值,这涉及到两种数据分析任务:离线任务和实时任务。

离线任务对实时性要求不是很高,例如天天、每周、每个月的数据报表统计分析,咱们可使用基于MapReduce数据仓库工具Hive进行报表统计。

实时任务对实时性要求高,例如根据用户相关行为推荐用户感兴趣的商品,提升用户购买体验和效率,可使用Flink进行流处理分析。例如运营后台查询分析,能够将数据同步至ES进行检索。

还有一种分类方式是将任务分为批处理任务和流处理任务,咱们能够这么理解:离线任务通常使用批处理技术,实时任务通常使用流处理技术。


3.2 API

上一个章节咱们使用了Kafka进行海量数据存储,因为其强大兼容性和集成度,能够做为数据中介将数据进行中转和解耦。

固然咱们并非必须使用Kafka进行中转,例如咱们直接可使用相关Java API将数据存入Hive、ES、HBASE等。可是我并不推荐这种作法,由于将保存流水这样操做耦合进业务代码并不合适,违反了高内聚低耦合的原则,尽可能不要使用。


3.3 缓存

从广义上理解换这个字,咱们还能够引入Redis这种远程缓存,把Redis放在MySQL前面,拦下一些高频读请求,可是要注意缓存穿透和击穿问题。

缓存穿透和击穿从最终结果上来讲都是流量绕过缓存打到了数据库,可能会致使数据库挂掉或者系统雪崩,可是仔细区分仍是有一些不一样,咱们分析一张业务读取缓存通常流程图。

咱们用文字简要描述这张图:

(1) 业务查询数据时首先查询缓存,若是缓存存在数据则返回,流程结束

(2) 若是缓存不存在数据则查询数据库,若是数据库不存在数据则返回空数据,流程结束

(3) 若是数据库存在数据则将数据写入缓存并返回数据给业务,流程结束

假设业务方要查询A数据,缓存穿透是指数据库根本不存在A数据,因此根本没有数据能够写入缓存,致使缓存层失去意义,大量请求会频繁访问数据库。

缓存击穿是指请求在查询数据库前,首先查缓存看看是否存在,这是没有问题的。可是并发量太大,致使第一个请求尚未来得及将数据写入缓存,后续大量请求已经开始访问缓存,这是数据在缓存中仍是不存在的,因此瞬时大量请求会打到数据库。

咱们可使用分布式锁加上自旋解决这个问题,本文给出一段示例代码,具体原理和代码实现请参看我以前的文章:流程图+源码深刻分析:缓存穿透和击穿问题出现原理以及可落地解决方案

/** * 业务回调 * * @author 微信公众号「JAVA前线」 * */
public interface RedisBizCall {

    /** * 业务回调方法 * * @return 序列化后数据值 */
    String call();
}

/** * 安全缓存管理器 * * @author 微信公众号「JAVA前线」 * */
@Service
public class SafeRedisManager {
    @Resource
    private RedisClient RedisClient;
    @Resource
    private RedisLockManager redisLockManager;

    public String getDataSafe(String key, int lockExpireSeconds, int dataExpireSeconds, RedisBizCall bizCall, boolean alwaysRetry) {
        boolean getLockSuccess = false;
        try {
            while(true) {
                String value = redisClient.get(key);
                if (StringUtils.isNotEmpty(value)) {
                    return value;
                }
                /** 竞争分布式锁 **/
                if (getLockSuccess = redisLockManager.tryLock(key, lockExpireSeconds)) {
                    value = redisClient.get(key);
                    if (StringUtils.isNotEmpty(value)) {
                        return value;
                    }
                    /** 查询数据库 **/
                    value = bizCall.call();

                    /** 数据库无数据则返回**/
                    if (StringUtils.isEmpty(value)) {
                        return null;
                    }

                    /** 数据存入缓存 **/
                    redisClient.setex(key, dataExpireSeconds, value);
                    return value;
                } else {
                    if (!alwaysRetry) {
                        logger.warn("竞争分布式锁失败,key={}", key);
                        return null;
                    }
                    Thread.sleep(100L);
                    logger.warn("尝试从新获取数据,key={}", key);
                }
            }
        } catch (Exception ex) {
            logger.error("getDistributeSafeError", ex);
            return null;
        } finally {
            if (getLockSuccess) {
                redisLockManager.unLock(key);
            }
        }
    }
}
复制代码

4 分

咱们首先看一个概念:读写比。互联网场景中通常是读多写少,例如浏览20次订单列表信息才会进行1次确认收货,此时读写比例就是20:1。面对读多写少这种状况咱们能够作什么呢?

咱们能够部署多台MySQL读库专门接收读请求,主库接收写请求并经过binlog实时同步的方式将数据同步至读库。MySQL官方即提供这种能力,进行简单配置便可。

那么客户端怎么知道访问读库仍是写库呢?推荐使用ShardingSphere组件,经过配置将读写请求分别路由至读库或者写库。


5 拆

若是删除了历史数据并采用了其它存储介质,也用了读写分离,可是单表数据仍是太大怎么办?这时咱们只能拆分数据表,即把单库单表数据迁移到多库多张表中。

假设有一个电商数据库,存放在订单、商品、支付三张业务表。随着业务量愈来愈大,这三张业务数据表也愈来愈大,咱们就以这个例子进行分析。


5.1 垂直拆分

垂直拆分就是按照业务拆分,咱们将电商数据库拆分红三个库,订单库、商品库。支付库,订单表在订单库,商品表在商品库,支付表在支付库。这样每一个库只须要存储本业务数据,物理隔离不会互相影响。


5.2 水平拆分

按照垂直拆分方案,如今咱们已经有三个库了,平稳运行了一段时间。可是随着业务增加,每一个单库单表的数据量也愈来愈大,逐渐到达瓶颈。

这时咱们就要对数据表进行水平拆分,所谓水平拆分就是根据某种规则将单库单表数据分散到多库多表,从而减少单库单表的压力。

水平拆分策略有不少,核心是选中Sharding Key,也就是按照哪一列进行拆分,怎么分取决于咱们访问数据的方式。


5.2.1 范围分片

如今咱们要对订单库进行水平拆分,ShardingKey是订单建立时间,拆分策略以下:

(1) 拆分为四个数据库,分别存储每一个季度的数据
(2) 每一个库三张表,分别存储每月的数据
复制代码

上述方法优势是对范围查询比较友好,例如须要统计第一季度的相关数据,查询条件直接输入时间范围便可。

可是这个方案问题是容易产生热点数据。例如双11当天下单量特别大,就会致使11月这张表数据量特别大从而形成访问压力。


5.2.2 查表分片

查表法是根据一张路由表决定ShardingKey路由到哪一张表,每次路由时首先到路由表里查一下获得分片信息,再到这个分片去取数据。

咱们来看一个查表法实际案例。Redis官方在3.0版本以后提供了集群方案Redis Cluster,其中引入了哈希槽(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) 客户端再向正确节点发起链接请求
复制代码

查表法优势是能够灵活制定路由策略,若是咱们发现有的分片已经成为热点则修改路由策略。缺点是多一次查询路由表操做增长耗时,并且路由表若是是单点也可能会有单点问题。


5.2.3 哈希分片

如今比较流行的分片方法是哈希分片,相较于范围分片,哈希分片能够较为均匀将数据分散在数据库中。

咱们如今将订单库拆分为4个库编号为[0,3],每一个库4张表编号为[0,3],以下图如所示:

如今咱们使用orderId做为ShardingKey,那么orderId=100的订单会保存在哪张表?咱们来计算一下。因为是分库分表,那么首先肯定路由到哪个库,取模计算获得序号为0表示路由到db[0]

db_index = 100 % 4 = 0
复制代码

库肯定了接着在db[0]进行取模表路由

table_index = 100 % 4 = 0
复制代码

最终这条数据应该路由至下表

db[0]_table[0]
复制代码

最终计算结果以下图所示:

在实际开发中最终路由到哪张表,并不须要咱们本身算,由于有许多开源框架就能够完成路由功能,例如ShardingSphere、TDDL等等。


6 异

如今咱们数据已经水平拆分完成,使用了哈希分片方法,ShardingKey是orderId。这时客户端须要查询orderId=111的数据,查询语句很简单以下:

SELECT * FROM order WHERE orderId = 111
复制代码

这个语句没有问题,由于查询条件包含orderId,能够路由到具体的数据表。

如今若是业务想要查询用户维度的数据,但愿查询userId=222的数据,如今问题来了:如下这个语句能够查出数据吗?

SELECT * FROM order WHERE userId = 222
复制代码

答案是能够查出数据,可是须要扫描全部库的全部表,由于没法根据userId路由到具体某一张表,这样时间成本会很是高,这种场景怎么办呢?

这就要用到数据异构的思想。所谓数据异构核心是用空间换时间,简单一句话就是一份数据按照不一样业务的需求保存多份,这样作是由于存储硬件成本不是很高,而互联网场景对响应速度要求很高。

对于上述须要使用userId进行查询的场景,咱们彻底能够新建库和表,数量和结构与订单库表彻底一致,惟一不一样点是ShardingKey改用userId,这样就可使用userId查询了。

如今又引出一个新问题,业务不可能每次都将数据写入多个数据源,这样会带来性能问题和数据一致行为。怎么解决老库和新库数据同步问题?咱们可使用阿里开源的canal组件解决这个问题,看一张官网介绍canal架构图:

canal主要用途是基于MySQL数据库增量日志解析,提供增量数据订阅和消费服务,工做原理以下:

(1) canal假装成为MySQL slave模拟交互协议向master发送dump协议

(2) master收到canal发送的dump请求,开始推送binlog给canal

(3) canal解析binlog并发送到存储目的地,例如MySQL、Kafka、Elasticsearch
复制代码

canal组件下游能够对接不少其它数据源,这样给业务提供了更多选择。咱们能够像上述实例中新建用户维度订单表,也能够将数据存在ES中提供运营检索能力。


7 热

咱们来分析这样一个场景:社交业务有一张用户关系表,主要记录谁关注了谁。其中有一个明星粉丝特别多,若是以userId做为分片,那么其所在分片数据量就会特别大。

不只分片数据量特别大,并且能够预见这个分片访问频率也会很是高。此时数据量大而且访问频繁,颇有可能形成系统压力。


7.1 热点概念

咱们将访问行为称为热点行为,将访问对应的数据称为热点数据。咱们经过实例来分析。

在电商双11活动中百分之八十的访问量会集中在百分之二十的商品上。用户刷新、添加购物车、下单被称为热点行为,相应商品数据就被称为热点数据。

在微博场景中大V发布一条消息会得到极大的访问量。用户对这条消息的浏览、点赞、转发、评论被称为热点行为,这条消息数据被称为热点数据。

在秒杀场景中参与秒杀的商品会得到极大的瞬时访问量。用户对这个商品的频繁刷新、点击、下单被称为热点行为,参与秒杀的商品数据被称为热点数据。

咱们必须将热点数据进行一些处理,使得热点访问更加流畅,更是为了保护系统免于崩溃。下面咱们从发现热点数据、处理热点数据来展开分析。


7.2 发现热点数据

发现热点数据有两种方式:静态发现和动态发现。

静态发现:在开始秒杀活动以前,参与商家必定知道哪些商品参与秒杀,那么他们能够提早将这些商品报备告知平台。

在微博场景中,具备影响力的大V通常都很知名,网站运营同窗能够提早知道。技术同窗还能够经过分析历史数据找出TOP N数据。对于这些能够提早预判的数据,彻底能够经过后台系统上报,这样系统能够提早作出预处理。

动态发现:有些商品可能并无上报为热点商品,可是在实际销售中却很是抢手。在微博场景中,有些话题热度忽然升温。这些数据成为事实上的热点数据。对于这些没法提早预判的数据,须要动态进行判断。

咱们须要一个热点发现系统去主动发现热点数据。大致思路是首先异步收集访问日志,再统计单位时间内访问频次,当超过必定阈值时能够判断为热点数据。


7.3 处理热点问题

(1) 热点行为

热点行为能够采起高频检测方式,若是发现频率太高则进行限制。或者采用内存队列实现的生产者与消费者这种异步化方式,消费者根据能力处理请求。

(2) 热点数据

处理热点数据也没有什么固定之法,仍是要根据业务形态来进行处理,我通常采用如下方案配合执行。

(1) 选择合适ShardingKey进行分库分表
 
(2) 异构数据至其它适合检索的数据源例如ES

(3) 在MySQL以前设置缓存层

(4) 尽可能不在MySQL进行耗时操做(例如聚合)
复制代码

8 文章总结

本文咱们详细介绍处理单表数据量过大的六字口诀:删、换、分、拆、异、热。

这并非意味这每次遇到单表数据量过大状况六种方案所有都要使用,例如拆分数据表成本确实比较高,会带来分布式事务、数据难以聚合等问题,若是不分表能够解决那么就不要分表,核心仍是根据自身业务状况选择合适的方案。


欢迎你们关注公众号「JAVA前线」查看更多精彩分享文章,主要包括源码分析、实际应用、架构思惟、职场分享、产品思考等等,同时欢迎你们加我微信「java_front」一块儿交流学习

相关文章
相关标签/搜索