[NewLife.XCode]分表分库(百亿级大数据存储)
NewLife.XCode是一个有15年历史的开源数据中间件,支持netcore/net45/net40,由新生命团队(2002~2019)开发完成并维护至今,以下简称XCode。
整个系列教程会大量结合示例代码和运行日志来进行深入分析,蕴含多年开发经验于其中,代表作有百亿级大数据实时计算项目。
开源地址:https://github.com/NewLifeX/X(求star, 938+)
XCode是重度充血模型,以单表操作为核心,不支持多表关联Join,复杂查询只能在where上做文章,整个select语句一定是from单表,因此对分表操作具有天然优势!
!! 阅读本文之前,建议回顾《百亿级性能》,其中“索引完备”章节详细描述了大型数据表的核心要点。
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 = ; i < ; 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 = ; i < ; i++)
{
var connName = $"HDB_{i + 1}";
History.Meta.ConnName = connName; // 每库4张表
for (var j = ; j < ; j++)
{
History.Meta.TableName = $"History_{j + 1}"; // 插入一批数据
var list = new List<History>();
for (var n = ; n < ; n++)
{
var entity = new History
{
Category = "交易",
Action = "转账",
CreateUserID = ,
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 = ; i < ; i++)
{
var dt = new DateTime(start.Year, i + , );
var connName = $"HDB_{dt:yyMM}";
DAL.AddConnStr(connName, $"data source=timeData\\{connName}.db", null, "sqlite");
} // 每月一个库,每天一张表
start = new DateTime(start.Year, , );
for (var i = ; i < ; 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 = ; n < ; n++)
{
var entity = new History
{
Category = "交易",
Action = "转账",
CreateUserID = ,
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 = ; i < ; i++)
{
var dt = new DateTime(start.Year, i + , );
var connName = $"HDB_{dt:yyMM}";
DAL.AddConnStr(connName, $"data source=timeData\\{connName}.db", null, "sqlite");
} // 随机日期。批量操作
start = new DateTime(start.Year, , );
{
var dt = start.AddDays(Rand.Next(, ));
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, , );
{
var dt = start.AddDays(Rand.Next(, ));
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亿多次添删改,总数据量数百亿。
博文答疑
2019年9月9日晚上19点,钉钉企业群“新生命团队”,视频直播博文答疑。
今晚之后,如有问题,可以提问:https://github.com/NewLifeX/X/issues

系列教程
NewLife.XCode教程系列[2019版]
- 增删改查入门。快速展现用法,代码配置连接字符串
- 数据模型文件。建立表格字段和索引,名字以及数据类型规范,推荐字段(时间,用户,IP)
- 实体类详解。数据类业务类,泛型基类,接口
- 功能设置。连接字符串,调试开关,SQL日志,慢日志,参数化,执行超时。代码与配置文件设置,连接字符串局部设置
- 反向工程。自动建立数据库数据表
- 数据初始化。InitData写入初始化数据
- 高级增删改。重载拦截,自增字段,Valid验证,实体模型(时间,用户,IP)
- 脏数据。如何产生,怎么利用
- 增量累加。高并发统计
- 事务处理。单表和多表,不同连接,多种写法
- 扩展属性。多表关联,Map映射
- 高级查询。复杂条件,分页,自定义扩展FieldItem,查总记录数,查汇总统计
- 数据层缓存。Sql缓存,更新机制
- 实体缓存。全表整理缓存,更新机制
- 对象缓存。字典缓存,适用用户等数据较多场景。
- 百亿级性能。字段精炼,索引完备,合理查询,充分利用缓存
- 实体工厂。元数据,通用处理程序
- 角色权限。Membership
- 导入导出。Xml,Json,二进制,网络或文件
- 分表分库。常见拆分逻辑
- 高级统计。聚合统计,分组统计
- 批量写入。批量插入,批量Upsert,异步保存
- 实体队列。写入级缓存,提升性能。
- 备份同步。备份数据,恢复数据,同步数据
- 数据服务。提供RPC接口服务,远程执行查询,例如SQLite网络版
- 大数据分析。ETL抽取,调度计算处理,结果持久化
[NewLife.XCode]分表分库(百亿级大数据存储)的更多相关文章
- 百亿级小文件存储,JuiceFS 在自动驾驶行业的最佳实践
自动驾驶是最近几年的热门领域,专注于自动驾驶技术的创业公司.新造车企业.传统车厂都在这个领域投入了大量的资源,推动着 L4.L5 级别自动驾驶体验能尽早进入我们的日常生活. 自动驾驶技术实现的核心环节 ...
- [NewLife.XCode]百亿级性能
NewLife.XCode是一个有10多年历史的开源数据中间件,支持nfx/netcore,由新生命团队(2002~2019)开发完成并维护至今,以下简称XCode. 整个系列教程会大量结合示例代码和 ...
- Mycat分表分库
一.Mycat介绍 Mycat 是一个开源的分布式数据库系统,是一个实现了 MySQL 协议的的Server,前端用户可以把它看作是一个数据库代理,用 MySQL 客户端工具和命令行访问,而其后端可以 ...
- mycat原理及分表分库入门
1.什么是MyCat: MyCat是一个开源的分布式数据库系统,是一个实现了MySQL协议的服务器,前端用户可以把它看作是一个数据库代理,用MySQL客户端工具和命令行访问,而其后端可以用MySQL原 ...
- 总结下Mysql分表分库的策略及应用
上月前面试某公司,对于mysql分表的思路,当时简要的说了下hash算法分表,以及discuz分表的思路,但是对于新增数据自增id存放的设计思想回答的不是很好(笔试+面试整个过程算是OK过了,因与个人 ...
- cassandra百亿级数据库迁移实践
迁移背景 cassandra集群隔段时间出现rt飙高的问题,带来的影响就是请求cassandra短时间内出现大量超时,这个问题发生已经达到了平均两周一次的频率,已经影响到正常业务了.而出现这些问题的原 ...
- 重新学习Mysql数据13:Mysql主从复制,读写分离,分表分库策略与实践
一.MySQL扩展具体的实现方式 随着业务规模的不断扩大,需要选择合适的方案去应对数据规模的增长,以应对逐渐增长的访问压力和数据量. 关于数据库的扩展主要包括:业务拆分.主从复制.读写分离.数据库分库 ...
- Redis百亿级Key存储方案(转)
1 需求背景 该应用场景为DMP缓存存储需求,DMP需要管理非常多的第三方id数据,其中包括各媒体cookie与自身cookie(以下统称supperid)的mapping关系,还包括了supperi ...
- Redis百亿级Key存储方案
1 需求背景 该应用场景为DMP缓存存储需求,DMP需要管理非常多的第三方id数据,其中包括各媒体cookie与自身cookie(以下统称supperid)的mapping关系,还包括了supperi ...
随机推荐
- navicat连接mysql报错1251解决方案,从头搭建node + mysql 8.0 (本人亲测有效)
准备学node 好久了 一直没有动手去写,今天突发奇想,然后就安装了一个mysql (找了一个博客跟着步骤去安装的),然后打算用node 写个增删改查. 1.下载mysql安装包 地址: http ...
- springboot整合shiro进行权限管理
背景:springboot2.1,shiro1.4:由于目前的小项目没做登录,但是客户又需要加上权限,因此楼主就想到了shiro(这是单独的项目,需要集成后台管理系统) shiro简介 Apache ...
- 【LOJ#6682】梦中的数论(min_25筛)
[LOJ#6682]梦中的数论(min_25筛) 题面 LOJ 题解 注意题意是\(j|i\)并且\((j+k)|i\), 不难发现\(j\)和\((j+k)\)可以任意取\(i\)的任意因数,且\( ...
- Redis for OPS 05:哨兵HA Sentinel
写在前面的话 上一节的主从环境能够解决我们保证数据安全性的问题,但是却无法解决我们在主节点挂掉的时候服务继续使用的问题,同时也不能自动切换新的主. 我们运维的目的肯定是希望即使主库挂掉一个,服务依旧能 ...
- 登录注册案例(Servlet+JSP+Maven)
项目案例模板之登录注册的实现 案例演示 案例代码 设计表 pom.xml <dependencies> <dependency> <groupId>jun ...
- .net 数据源DataSet 转换成模型
/// <summary> /// DataSet转换成model 自动赋值返回集合 /// </summary> /// <typeparam name="T ...
- python基础(35):协程
1. 前言 之前我们学习了线程.进程的概念,了解了在操作系统中进程是资源分配的最小单位,线程是CPU调度的最小单位.按道理来说我们已经算是把cpu的利用率提高很多了.但是我们知道无论是创建多进程还是创 ...
- JAVA 基础篇
一.数组 1. 什么是数组? 数组和变量差不多,也是可以存放数据的,但是数组可以存放多个数据,而且多个数据的数据类型统一 格式 数据类型 [] 数组名称; 还有一种等效的写法,不推荐 数据类型 数组名 ...
- 使用Fiddler抓取手机HTTP流量包
- PC端配置 设置允许远程连接. - PC端共享上网 需要在PC上进行手机抓包,必须使手机流量经过PC,在文章<简单两行,实现无线WiFi共享上网,手机抓包再也不用愁了>中介绍了如何在P ...
- iOS中的GCD线程
一.什么是GCD 全称是Grand Central Dispatch ,纯C语言编写,提供非常多强大的函数,是目前苹果官网推荐的多线程开发方法,NSOperation 便是基于GCD的封装 ...