关于c#:分表分库百亿级大数据存储

43次阅读

共计 5997 个字符,预计需要花费 15 分钟才能阅读完成。

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…
新建控制台我的项目,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”>

<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>

</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 亿多次添删改,总数据量数百亿。

正文完
 0