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

 

博文答疑

2019年9月9日晚上19点,钉钉企业群“新生命团队”,视频直播博文答疑。

今晚之后,如有问题,可以提问:https://github.com/NewLifeX/X/issues

 

 

 

系列教程

NewLife.XCode教程系列[2019版]

  1. 增删改查入门。快速展现用法,代码配置连接字符串
  2. 数据模型文件。建立表格字段和索引,名字以及数据类型规范,推荐字段(时间,用户,IP)
  3. 实体类详解。数据类业务类,泛型基类,接口
  4. 功能设置。连接字符串,调试开关,SQL日志,慢日志,参数化,执行超时。代码与配置文件设置,连接字符串局部设置
  5. 反向工程。自动建立数据库数据表
  6. 数据初始化。InitData写入初始化数据
  7. 高级增删改。重载拦截,自增字段,Valid验证,实体模型(时间,用户,IP)
  8. 脏数据。如何产生,怎么利用
  9. 增量累加。高并发统计
  10. 事务处理。单表和多表,不同连接,多种写法
  11. 扩展属性。多表关联,Map映射
  12. 高级查询。复杂条件,分页,自定义扩展FieldItem,查总记录数,查汇总统计
  13. 数据层缓存。Sql缓存,更新机制
  14. 实体缓存。全表整理缓存,更新机制
  15. 对象缓存。字典缓存,适用用户等数据较多场景。
  16. 百亿级性能。字段精炼,索引完备,合理查询,充分利用缓存
  17. 实体工厂。元数据,通用处理程序
  18. 角色权限。Membership
  19. 导入导出。Xml,Json,二进制,网络或文件
  20. 分表分库。常见拆分逻辑
  21. 高级统计。聚合统计,分组统计
  22. 批量写入。批量插入,批量Upsert,异步保存
  23. 实体队列。写入级缓存,提升性能。
  24. 备份同步。备份数据,恢复数据,同步数据
  25. 数据服务。提供RPC接口服务,远程执行查询,例如SQLite网络版
  26. 大数据分析。ETL抽取,调度计算处理,结果持久化 
posted @ 2019-09-09 09:07  大石头  阅读(6941)  评论(28编辑  收藏  举报