分表分库(百亿级大数据存储)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 100亿数据其实并不多,一个比较常见的数据分表分库模型:MySql数据库8主8从,每服务器8个库,每个库16张表,共1024张表(从库也有1024张表) ,每张表1000万到5000万数据,整好100亿到500亿数据!

NewLife.XCode是一个有15年历史的开源数据中间件,支持netcore/net45/net40,由新生命团队(2002~2019)开发完成并维护至今,以下简称XCode。

整个系列教程会大量结合示例代码和运行日志来进行深入分析,蕴含多年开发经验于其中,代表作有百亿级大数据实时计算项目。

开源地址:https://github.com/NewLifeX/X (求star, 938+)

 

XCode是重度充血模型,以单表操作为核心,不支持多表关联Join,复杂查询只能在where上做文章,整个select语句一定是from单表,因此对分表操作具有天然优势!

!! 阅读本文之前,建议回顾《百亿级性能》,其中“索引完备”章节详细描述了大型数据表的核心要点。

 

此处为语雀内容卡片,点击链接查看:https://www.yuque.com/smartstone/xcode/100billion


100亿数据其实并不多,一个比较常见的数据分表分库模型:

MySql数据库8主8从,每服务器8个库,每个库16张表,共1024张表(从库也有1024张表) ,每张表1000万到5000万数据,整好100亿到500亿数据!

 

例程剖析

例程位置:https://github.com/NewLifeX/X/tree/master/Samples/SplitTableOrDatabase

新建控制台项目,nuget引用NewLife.XCode后,建立一个实体模型(修改Model.xml):

<Tables Version="9.12.7136.19046" NameSpace="STOD.Entity" ConnName="STOD" Output="" BaseClass="Entity" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xs:schemaLocation="http://www.newlifex.com https://raw.githubusercontent.com/NewLifeX/X/master/XCode/ModelSchema.xsd" xmlns="http://www.newlifex.com/ModelSchema.xsd">

 <Table Name="History" Description="历史">

   <Columns>

     <Column Name="ID" DataType="Int32" Identity="True" PrimaryKey="True" Description="编号" />

     <Column Name="Category" DataType="String" Description="类别" />

     <Column Name="Action" DataType="String" Description="操作" />

     <Column Name="UserName" DataType="String" Description="用户名" />

     <Column Name="CreateUserID" DataType="Int32" Description="用户编号" />

     <Column Name="CreateIP" DataType="String" Description="IP地址" />

     <Column Name="CreateTime" DataType="DateTime" Description="时间" />

     <Column Name="Remark" DataType="String" Length="500" Description="详细信息" />

   </Columns>

   <Indexes>

     <Index Columns="CreateTime" />

   </Indexes>

 </Table>

</Tables>

在Build.tt上右键运行自定义工具,生成实体类“历史.cs”和“历史.Biz.cs”。不用修改其中代码,待会我们将借助该实体类来演示分表分库用法。

为了方便,我们将使用SQLite数据库,因此不需要配置任何数据库连接,XCode检测到没有名为STOD的连接字符串时,将默认使用SQLite。

此外,也可以通过指定名为STOD的连接字符串,使用其它非SQLite数据库。

 

按数字散列分表分库

大量订单、用户等信息,可采用crc16散列分表,我们把该实体数据拆分到4个库共16张表里面:

static void TestByNumber()

{

   XTrace.WriteLine("按数字分表分库");


   // 预先准备好各个库的连接字符串,动态增加,也可以在配置文件写好

   for (var i = 0; i < 4; i++)

   {

       var connName = $"HDB_{i + 1}";

       DAL.AddConnStr(connName, $"data source=numberData\\{connName}.db", null, "sqlite");

       History.Meta.ConnName = connName;


       // 每库建立4张表。这一步不是必须的,首次读写数据时也会创建

       //for (var j = 0; j < 4; j++)

       //{

       //    History.Meta.TableName = $"History_{j + 1}";


       //    // 初始化数据表

       //    History.Meta.Session.InitData();

       //}

   }


   //!!! 写入数据测试


   // 4个库

   for (var i = 0; i < 4; i++)

   {

       var connName = $"HDB_{i + 1}";

       History.Meta.ConnName = connName;


       // 每库4张表

       for (var j = 0; j < 4; j++)

       {

           History.Meta.TableName = $"History_{j + 1}";


           // 插入一批数据

           var list = new List<History>();

           for (var n = 0; n < 1000; n++)

           {

               var entity = new History

               {

                   Category = "交易",

                   Action = "转账",

                   CreateUserID = 1234,

                   CreateTime = DateTime.Now,

                   Remark = $"[{Rand.NextString(6)}]向[{Rand.NextString(6)}]转账[¥{Rand.Next(1_000_000) / 100d}]"

               };


               list.Add(entity);

           }


           // 批量插入。两种写法等价

           //list.BatchInsert();

           list.Insert(true);

       }

   }

}

通过 DAL.AddConnStr 动态向系统注册连接字符串:

var connName = $"HDB_{i + 1}";


DAL.AddConnStr(connName, $"data source=numberData\\{connName}.db", null, "sqlite");

连接名必须唯一,且有规律,后面要用到。数据库名最好也有一定规律。

使用时通过Meta.ConnName指定后续操作的连接名,Meta.TableName指定后续操作的表名,本线程有效,不会干涉其它线程。

var connName = $"HDB_{i + 1}";

History.Meta.ConnName = connName;

History.Meta.TableName = $"History_{j + 1}";

注意,ConnName/TableName改变后,将会一直维持该参数,直到修改为新的连接名和表名。

指定表名连接名后,即可在本线程内持续使用,后面使用批量插入技术,给每张表插入一批数据。

 

运行效果如下:

 

 

 

 

连接字符串指定的numberData目录下,生成了4个数据库,每个数据库生成了4张表,每张表内插入1000行数据。

指定不存在的数据库和数据表时,XCode的反向工程将会自动建表建库,这是它独有的功能。(因异步操作,密集建表建库时可能有一定几率失败,重试即可)

 

按时间序列分表分库

日志型的时间序列数据,特别适合分表分库存储,定型拆分模式是,每月一个库每天一张表。

static void TestByDate()

{

   XTrace.WriteLine("按时间分表分库,每月一个库,每天一张表");


   // 预先准备好各个库的连接字符串,动态增加,也可以在配置文件写好

   var start = DateTime.Today;

   for (var i = 0; i < 12; i++)

   {

       var dt = new DateTime(start.Year, i + 1, 1);

       var connName = $"HDB_{dt:yyMM}";

       DAL.AddConnStr(connName, $"data source=timeData\\{connName}.db", null, "sqlite");

   }


   // 每月一个库,每天一张表

   start = new DateTime(start.Year, 1, 1);

   for (var i = 0; i < 365; i++)

   {

       var dt = start.AddDays(i);

       History.Meta.ConnName = $"HDB_{dt:yyMM}";

       History.Meta.TableName = $"History_{dt:yyMMdd}";


       // 插入一批数据

       var list = new List<History>();

       for (var n = 0; n < 1000; n++)

       {

           var entity = new History

           {

               Category = "交易",

               Action = "转账",

               CreateUserID = 1234,

               CreateTime = DateTime.Now,

               Remark = $"[{Rand.NextString(6)}]向[{Rand.NextString(6)}]转账[¥{Rand.Next(1_000_000) / 100d}]"

           };


           list.Add(entity);

       }


       // 批量插入。两种写法等价

       //list.BatchInsert();

       list.Insert(true);

   }

}

时间序列分表看起来比数字散列更简单一些,分表逻辑清晰明了。

 

 

 

 

例程遍历了今年的365天,在连接字符串指定的timeData目录下,生成了12个月份数据库,然后每个库里面按月生成数据表,每张表插入1000行模拟数据。

 

综上,分表分库其实就是在操作数据库之前,预先设置好 Meta.ConnName/Meta.TableName,其它操作不变!

 

分表查询

说到分表,许多人第一反应就是,怎么做跨表查询?

不好意思,不支持!

只能在多张表上各自查询,如果系统设计不合理,甚至可能需要在所有表上进行查询。

不建议做视图union,那样会无穷无尽,业务逻辑还是放在代码中为好,数据库做好存储与基础计算。

 

分表查询的用法与分表添删改一样:

static void SearchByDate()

{

   // 预先准备好各个库的连接字符串,动态增加,也可以在配置文件写好

   var start = DateTime.Today;

   for (var i = 0; i < 12; i++)

   {

       var dt = new DateTime(start.Year, i + 1, 1);

       var connName = $"HDB_{dt:yyMM}";

       DAL.AddConnStr(connName, $"data source=timeData\\{connName}.db", null, "sqlite");

   }


   // 随机日期。批量操作

   start = new DateTime(start.Year, 1, 1);

   {

       var dt = start.AddDays(Rand.Next(0, 365));

       XTrace.WriteLine("查询日期:{0}", dt);


       History.Meta.ConnName = $"HDB_{dt:yyMM}";

       History.Meta.TableName = $"History_{dt:yyMMdd}";


       var list = History.FindAll();

       XTrace.WriteLine("数据:{0}", list.Count);

   }


   // 随机日期。个例操作

   start = new DateTime(start.Year, 1, 1);

   {

       var dt = start.AddDays(Rand.Next(0, 365));

       XTrace.WriteLine("查询日期:{0}", dt);

       var list = History.Meta.ProcessWithSplit(

           $"HDB_{dt:yyMM}",

           $"History_{dt:yyMMdd}",

           () => History.FindAll());


       XTrace.WriteLine("数据:{0}", list.Count);

   }

}

 

仍然是通过设置 Meta.ConnName/Meta.TableName 来实现分表分库。日志输出可以看到查找了哪个库哪张表。

这里多了一个 History.Meta.ProcessWithSplit  ,其实是快捷方法,在回调内使用连接名和表名,退出后复原。

 

分表分库后,最容易犯下的错误,就是使用时忘了设置表名,在错误的表上查找数据,然后怎么也查不到……

 

分表策略

根据这些年的经验:

  • Oracle适合单表1000万~1亿行数据,要做分区
  • MySql适合单表1000万~5000万行数据,很少人用MySql分区

如果统一在应用层做拆分,数据库只负责存储,那么上面的方案适用于各种数据库。

同时,单表数据上限,就是大家常问的应该分为几张表?在系统生命周期内(一般1~2年),确保拆分后的每张表数据总量在1000万附近最佳。

根据《百亿级性能》,常见分表策略如下:

  • 日志型时间序列表,如果每月数据不足1000万,则按月分表,否则按天分表。缺点是数据热点极为明显,适合热表、冷表、归档表的梯队架构,优点是批量写入和抽取性能显著;
  • 状态表(订单、用户等),按Crc16哈希分表,以1000万为准,决定分表数量,向上取整为2的指数倍(为了好算)。数据冷热均匀,利于单行查询更新,缺点是不利于批量写入和抽取;
  • 混合分表。订单表可以根据单号Crc16哈希分表,便于单行查找更新,作为宽表拥有各种明细字段,同时还可以基于订单时间建立一套时间序列表,作为冗余,只存储单号等必要字段。这样就解决了又要主键分表,又要按时间维度查询的问题。缺点就是订单数据需要写两份,当然,时间序列表只需要插入单号,其它更新操作不涉及。

至于是否需要分库,主要由存储空间以及性能要求决定。

 

分表与分区对比

还有一个很常见的问题,为什么使用分表而不是分区?

大型数据库Oracle、MSSQL、MySql都支持分区,前两者较多使用分区,MySql则较多分表。

分区和分表并没有本质的不同,两者都是为了把海量数据按照一定的策略拆分存储,以优化写入和查询。

  • 分区除了能建立子索引外,还可以建立全局索引,而分表不能建立全局索引;
  • 分区能跨区查询,但非常非常慢,一不小心就扫描所有分区;
  • 分表架构,很容易做成分库,支持轻易扩展到多台服务器上去,分区只能要求数据库服务器更强更大;
  • 分区主要由DBA操作,分表主要由程序员控制;

 

 

!!!某项目使用XCode分表功能,已经过生产环境三年半考验,日均新增4000万~5000万数据量,2亿多次添删改,总数据量数百亿。

相关实践学习
基于MaxCompute的热门话题分析
本实验围绕社交用户发布的文章做了详尽的分析,通过分析能得到用户群体年龄分布,性别分布,地理位置分布,以及热门话题的热度。
SaaS 模式云数据仓库必修课
本课程由阿里云开发者社区和阿里云大数据团队共同出品,是SaaS模式云原生数据仓库领导者MaxCompute核心课程。本课程由阿里云资深产品和技术专家们从概念到方法,从场景到实践,体系化的将阿里巴巴飞天大数据平台10多年的经过验证的方法与实践深入浅出的讲给开发者们。帮助大数据开发者快速了解并掌握SaaS模式的云原生的数据仓库,助力开发者学习了解先进的技术栈,并能在实际业务中敏捷的进行大数据分析,赋能企业业务。 通过本课程可以了解SaaS模式云原生数据仓库领导者MaxCompute核心功能及典型适用场景,可应用MaxCompute实现数仓搭建,快速进行大数据分析。适合大数据工程师、大数据分析师 大量数据需要处理、存储和管理,需要搭建数据仓库?学它! 没有足够人员和经验来运维大数据平台,不想自建IDC买机器,需要免运维的大数据平台?会SQL就等于会大数据?学它! 想知道大数据用得对不对,想用更少的钱得到持续演进的数仓能力?获得极致弹性的计算资源和更好的性能,以及持续保护数据安全的生产环境?学它! 想要获得灵活的分析能力,快速洞察数据规律特征?想要兼得数据湖的灵活性与数据仓库的成长性?学它! 出品人:阿里云大数据产品及研发团队专家 产品 MaxCompute 官网 https://www.aliyun.com/product/odps&nbsp;
相关文章
|
3月前
|
存储 算法 数据挖掘
【2023年中国高校大数据挑战赛 】赛题 B DNA 存储中的序列聚类与比对 Python实现
本文介绍了2023年中国高校大数据挑战赛赛题B的Python实现方法,该赛题涉及DNA存储技术中的序列聚类与比对问题,包括错误率分析、序列聚类、拷贝数分布图的绘制以及比对模型的开发。
82 1
【2023年中国高校大数据挑战赛 】赛题 B DNA 存储中的序列聚类与比对 Python实现
|
1月前
|
存储 消息中间件 大数据
大数据-69 Kafka 高级特性 物理存储 实机查看分析 日志存储一篇详解
大数据-69 Kafka 高级特性 物理存储 实机查看分析 日志存储一篇详解
36 4
|
1月前
|
消息中间件 存储 缓存
大数据-71 Kafka 高级特性 物理存储 磁盘存储特性 如零拷贝、页缓存、mmp、sendfile
大数据-71 Kafka 高级特性 物理存储 磁盘存储特性 如零拷贝、页缓存、mmp、sendfile
53 3
|
1月前
|
存储 消息中间件 大数据
大数据-70 Kafka 高级特性 物理存储 日志存储 日志清理: 日志删除与日志压缩
大数据-70 Kafka 高级特性 物理存储 日志存储 日志清理: 日志删除与日志压缩
41 1
|
1月前
|
存储 消息中间件 大数据
大数据-68 Kafka 高级特性 物理存储 日志存储概述
大数据-68 Kafka 高级特性 物理存储 日志存储概述
26 1
|
1月前
|
存储 算法 NoSQL
大数据-138 - ClickHouse 集群 表引擎详解3 - MergeTree 存储结构 数据标记 分区 索引 标记 压缩协同
大数据-138 - ClickHouse 集群 表引擎详解3 - MergeTree 存储结构 数据标记 分区 索引 标记 压缩协同
34 0
|
1月前
|
存储 消息中间件 分布式计算
大数据-137 - ClickHouse 集群 表引擎详解2 - MergeTree 存储结构 一级索引 跳数索引
大数据-137 - ClickHouse 集群 表引擎详解2 - MergeTree 存储结构 一级索引 跳数索引
36 0
|
1月前
|
存储 SQL 分布式计算
大数据-127 - Flink State 04篇 状态原理和原理剖析:状态存储 Part2
大数据-127 - Flink State 04篇 状态原理和原理剖析:状态存储 Part2
20 0
|
1月前
|
存储 消息中间件 大数据
大数据-126 - Flink State 03篇 状态原理和原理剖析:状态存储 Part1
大数据-126 - Flink State 03篇 状态原理和原理剖析:状态存储 Part1
59 0
|
3月前
|
存储 缓存 NoSQL
深入解析Memcached:内部机制、存储结构及在大数据中的应用
深入解析Memcached:内部机制、存储结构及在大数据中的应用