问题分析

经过几分钟的排查,数据库情况如下:

1、数据库采用Sqlserver 2008 R2,单表数据量21亿。

2、无水平或者垂直切分,但是采用了分区表。分区表策略是按时间降序分的区,将近30个分区。正因为分区表的原因,系统才保证了在性能不是太差的情况下坚持至今。

3、此表除聚集索引之外,无其他索引,无主键(主键其实是利用索引来快速查重的)。所以在频繁插入新数据的情况下,索引调整所耗费的性能比较低。

至于业务,不是太复杂。经过相关人员咨询,大约40%的请求为单条Insert,大约60%的请求为按class_id 和in_time(倒序)分页获取数据。Select请求全部命中聚集索引,所以性能非常高。这也是聚集索引之所以这样设计的目的。

解决问题

由于单表数据量已经超过21亿,并且2017年以前的数据几乎不影响业务,所以决定把2017年以前(不包括2017年)的数据迁移到新表,仅供以后特殊业务查询使用。经过查询大约有9亿数据量。

数据迁移工作包括三个步骤:

  • 从源数据表查询出要迁移的数据;

  • 把数据插入新表;

  • 把旧表的数据删除。

传统做法

这里申明一点,就算是传统的做法也需要分页获取源数据,因为你的内存一次性装载不下9亿条数据。

1、从源数据表分页获取数据,具体分页条数,太少则查询原表太频繁,太多则查询太慢。

SQL语句类似于:

SELECT * FROM (
SELECT *,ROW_NUMBER() OVER(ORDER BY class_id,in_time) p FROM  tablexx WHERE in_time <'2017.1.1'  
) t WHERE t.p BETWEEN 1 AND 100

2、把查询出来的数据插入目标数据表,这里强调一点,一定不要用单条插入策略,必须用批量插入。

3、把数据删除,其实这里删除还是有一个小难点,表没有标示列。

如果你的数据量不大,以上方法完全没有问题,但是在9亿这个数字前面,以上方法显得心有余而力不足。一个字:慢,太慢,非常慢。可以大体算一下,假如每秒可以迁移1000条数据,大约需要的时间为:900000000/1000/60=15000分钟。

大约需要10天......

改进做法

以上的传统做法弊端在哪里呢?

1、在9亿数据前查询必须命中索引,首推聚集索引。

2、如果你了解索引的原理,你应该明白,不停插入新数据的时候,索引在不停地更新、调整,以保持树的平衡等特性。尤其是聚集索引影响甚大,因为还需要移动实际的数据。

提取以上两点共同的要素,那就是聚集索引。相应的解决方案也就应运而生:

  • 按照聚集索分页引查询数据;

  • 批量插入数据迎合聚集索引,即按照聚集索引的顺序批量插入;

  • 按照聚集索引顺序批量删除。

由于做了表分区,如果有一种方式把2017年以前的分区直接在磁盘物理层面从当前表剥离,然后挂载到另外一个表,可算是神级操作。

如何挑战百万年薪的人工智能!

https://edu.csdn.net/topic/ai30?utm_source=csdn_bw

补充内容

1.  一个表的聚集索引的顺序就是实际数据文件的顺序,映射到磁盘上,本质上位于同一个磁道上,所以操作的时候磁盘的磁头不必跳跃着去操作。

2.  存储在硬盘中的每个文件都可分为两部分:文件头和存储数据的数据区。文件头用来记录文件名、文件属性、占用簇号等信息,文件头保存在一个簇并映射在FAT表(文件分配表)中。而真实的数据则是保存在数据区当中的。平常所做的删除,其实是修改文件头的前2个代码,这种修改映射在FAT表中,就为文件作了删除标记,并将文件所占簇号在FAT表中的登记项清零,表示释放空间,这也就是平常删除文件后,硬盘空间增大的原因。而真正的文件内容仍保存在数据区中,并未得以删除。要等到以后的数据写入,把此数据区覆盖掉,这样才算是彻底把原来的数据删除。如果不被后来保存的数据覆盖,它就不会从磁盘上抹掉。

NetCore 代码(实际运行代码)

1、第一步:由于聚集索引需要class_id ,所以宁可花2-4秒时间把要操作的class_id查询出来(ORM为dapper),并且升序排列:

DateTime dtMax = DateTime.Parse("2017.1.1");
   var allClassId = DBProxy.GeSourcetLstClassId(dtMax)?.OrderBy(s=>s);

2、按照第一步class_id 列表顺序查询数据,每个class_id 分页获取,然后插入目标表,全部完成然后删除源表相应class_id的数据(全部命中聚集索引):

D int pageIndex = 1; //页码
            int pageCount = 20000;//每页的数据条数
            DataTable tempData =null;
            int successCount = 0;
            foreach (var classId in allClassId)
            {
                tempData = null;
                pageIndex = 1;
                while (true)
                {
                    int startIndex = (pageIndex - 1) * pageCount+1;
                    int endIndex = pageIndex * pageCount;

tempData = DBProxy.GetSourceDataByClassIdTable(dtMax, classId, startIndex, endIndex);
                    if (tempData == null || tempData.Rows.Count==0)
                    {
                        //最后一页无数据了,删除源数据源数据然后跳出
                         DBProxy.DeleteSourceClassData(dtMax, classId);
                        break;
                    }
                    else
                    {
                        DBProxy.AddTargetData(tempData);
                    }
                    pageIndex++;
                }
                successCount++;
                Console.WriteLine($"班级:{classId} 完成,已经完成:{successCount}个");
            }

DBProxy 完整代码:

class DBProxy
    {
        //获取要迁移的数据所有班级id
        public static IEnumerable<int> GeSourcetLstClassId(DateTime dtMax)
        {
            var connection = Config.GetConnection(Config.SourceDBStr);
            string Sql = @"SELECT class_id FROM  tablexx WHERE in_time <@dtMax GROUP BY class_id ";
            using (connection)
            {
                return connection.Query<int>(Sql, new { dtMax = dtMax }, commandType: System.Data.CommandType.Text);

}
        }

public static DataTable GetSourceDataByClassIdTable(DateTime dtMax, int classId, int startIndex, int endIndex)
        {
            var connection = Config.GetConnection(Config.SourceDBStr);
            string Sql = @" SELECT * FROM (
                        SELECT *,ROW_NUMBER() OVER(ORDER BY in_time desc) p FROM  tablexx WHERE in_time <@dtMax  AND class_id=@classId
                        ) t WHERE t.p BETWEEN @startIndex AND @endIndex ";
            using (connection)
            {
                DataTable table = new DataTable("MyTable");
                var reader = connection.ExecuteReader(Sql, new { dtMax = dtMax, classId = classId, startIndex = startIndex, endIndex = endIndex }, commandType: System.Data.CommandType.Text);
                table.Load(reader);
                reader.Dispose();
                return table;
            }
        }
         public static int DeleteSourceClassData(DateTime dtMax, int classId)
        {
            var connection = Config.GetConnection(Config.SourceDBStr);
            string Sql = @" delete from  tablexx WHERE in_time <@dtMax  AND class_id=@classId ";
            using (connection)
            {
                return connection.Execute(Sql, new { dtMax = dtMax, classId = classId }, commandType: System.Data.CommandType.Text);

}
        }
        //SqlBulkCopy 批量添加数据
        public static int AddTargetData(DataTable data)
        {
            var connection = Config.GetConnection(Config.TargetDBStr);
            using (var sbc = new SqlBulkCopy(connection))
            {
                sbc.DestinationTableName = "tablexx_2017";               
                sbc.ColumnMappings.Add("class_id", "class_id");
                sbc.ColumnMappings.Add("in_time", "in_time");
                .
                .
                .
                using (connection)
                {
                    connection.Open();
                    sbc.WriteToServer(data);
                }               
            }
            return 1;
        }

}

运行报告:

程序本机运行,开VPN连接远程DB服务器,运行1分钟,迁移的数据数据量为1915560,每秒约3万条数据,1915560 / 60=31926 条/秒。

CPU情况(不高):

磁盘队列情况(不高):

写在最后

在以下情况下速度还将提高:

  • 源数据库和目标数据库硬盘为ssd,并且分别为不同的服务器;

  • 迁移程序和数据库在同一个局域网,保障数据传输时候带宽不会成为瓶颈;

  • 合理设置SqlBulkCopy参数;

  • 大多数场景下每次批量插入的数据量达不到设置的值,因为有的class_id 对应的数据量就几十条,甚至几条而已,打开关闭数据库连接也是需要耗时的;

  • 单纯的批量添加或者批量删除操作。

作者:菜菜,一个奔走在通往互联网更高之路的工程师,热衷于互联网技术。目前就职于某互联网教育公司,应用服务端主要负责人。拥有10年+互联网开发经验。热衷于高性能、高并发、分布式技术领域的研究。 主要工作语言为C#和Golang 。

声明:本文为作者投稿,版权归对方所有,编辑郭芮。


 热 文 推 荐 

☞ 腾讯往事:微信其实就是第四代 QQ 邮箱

☞ 5G 爆发前夕,将渗透哪些领域?

☞ 直接拿来用!VS Code 最强插件指南

虎口夺食! 打破Facebook谷歌垄断, MIT大神和他的区块链数据库传奇! |人物志

☞杨超越第一,Python第二

以安全之名:2019年DevSecOps社区调研白皮书解读

☞少儿编程只学会 Coding 就够了?比这更重要的是……

☞身为程序员的父母,你年薪多少才能让“码二代” 不输起跑线上?

System.out.println("点个在看吧!");
console.log("点个在看吧!");
print("点个在看吧!");
printf("点个在看吧!\n");
cout << "点个在看吧!" << endl;
Console.WriteLine("点个在看吧!");
Response.Write("点个在看吧!");
alert("点个在看吧!")
echo "点个在看吧!"

喜欢就点击“在看”吧!

程序员如何快速迁移 10 亿级数据?相关推荐

  1. 程序员过关斩将--快速迁移10亿级数据

    菜菜呀,咱们业务BJKJ有个表数据需要做迁移 程序员主力 Y总 现在有多少数据? 菜菜 大约21亿吧,2017年以前的数据没有业务意义了,给你半天时间把这个事搞定,绩效给你A 程序员主力 Y总 有绩效 ...

  2. 10亿级存储挑战!看一看、微信广告、微信支付、小程序都在用的存储系统究竟是怎么扛住的?!...

    导读:10亿级,是微信用户的数量级.这个庞大数字的背后,是"看一看"."微信广告"."微信支付"."小程序"等业务对数据 ...

  3. 10亿级存储挑战!看一看、微信广告、微信支付、小程序都在用的存储系统究竟是怎么扛住的?!

    背景:两个十亿级的挑战 PaxosStore 是微信内广泛应用的强一致性的分布式存储系统,它广泛支撑了微信的在线应用,峰值过亿TPS,运行在数千台服务器上,在线服务场景下性能强悍.但在软件开发中没有银 ...

  4. 新浪付稳:揭秘微博如何10分钟快速应对百亿级访问量

    本文转载自:新浪付稳:揭秘微博如何10分钟快速应对百亿级访问量 新浪微博几亿+的用户量,热点事件给其带来数倍流量瞬间暴增,如何不影响用户体验,又不增加巨大的服务器成本投入对技术是一个挑战. 作者:谢海 ...

  5. PHP 程序员最易犯10种错误

    PHP程序员经常犯的10中错误,大多数和安全相关.看看你犯了几种 1.不转意html entities 一个基本的常识:所有不可信任的输入(特别是用户从form中提交的数据) ,输出之前都要转意. e ...

  6. PHP程序员最易犯10种错误

    PHP是个伟大的web开发语言,灵活的语言,但是看到php程序员周而复始的犯的一些错误.我做了下面这个列表,列出了PHP程序员经常犯的10中错误,大多数和安全相关.看看你犯了几种. 1.不转意html ...

  7. Staking来袭,10亿级市场打开! | 火星总编时刻NO.31

    Staking将为PoS带来下游金融的机会. 本文旨在传递更多市场信息,不构成任何投资建议. 文 | 林中路 出品 | 火星财经APP(ID:hxcj24h) 火星财经APP(ID:hxcj24h)一 ...

  8. PHP程序员最易犯10种错误_莫枫恋_新浪博客

    PHP是个伟大的web开发语言,灵活的语言,但是看到php程序员周而复始的犯的一些错误.我做了下面这个列表,列出了PHP程序员经常犯的10中错误,大多数和安全相关.看看你犯了几种. 1.不转意html ...

  9. 28岁程序员,赚“1个亿”退休:运气,也是实力的一部分

    最近看到一句话非常戳心:同是90后,有人28岁赚了一个亿退休,有人28岁刚刚从学校毕业,还在苦苦谋求一份最基础的工作. 也许,这就是人生. 而这个在28岁赚了一笔钱选择退休的小伙子,就是最近在网上爆火 ...

最新文章

  1. 发布Web Service 注意的问题
  2. MFC 使用datetimepicker获取时间
  3. gpib安装包 python_ioctl errorno:25在使用pythongpib的GPIB通信中
  4. 【大话数据结构算法】哈夫曼树
  5. 寻找 ASP.NET 2.0 老师
  6. mysql的库与表的增删查改
  7. IE6 CSS bug: position:relative元素被overflow:auto/scroll元素包含
  8. 树莓派 python_树莓派笔记08—Python流水灯
  9. python separator_在Python中使用分隔符变量拆分字符串
  10. 一串数字中有两个只出现一次的数字其余都是成对相同,求这两个数
  11. Python 学习第一周
  12. React ~ 生命周期
  13. python 安装talib包
  14. 关于KL距离(KL Divergence)
  15. 天勤数据结构——绪论
  16. css鼠标hover的时候变成小手型
  17. 5G无线关键技术 — 灵活频谱共享技术
  18. 银河麒麟V10 SELinux启动问题
  19. 服务器2012分辨率不能修改,《F1 2012》无法修改分辨率解决方法
  20. 三维点云学习(6)7-3D Object Detection-KITTI object detection evaluation(2)-kitt 数据集文件分卷解压方式

热门文章

  1. linux java new date_Linux java Tomcat 项目中 new Date 获取时间 8小时 时差
  2. 怎么查电脑系统版本_重装系统PE内找不到硬盘怎么办?只需要这些设置即可…...
  3. ROI区域提取(图上直接利用鼠标事件提取坐标点,可视化显示)
  4. 开花(在b数组中二分查找a数组元素)
  5. notepad++ python指定anaconda环境以及代码补全设置
  6. 电力电容器行业调研报告 - 市场现状分析与发展前景预测(2021-2027年)
  7. linux远程桌面MacOS,如何在Linux或macOS中使用远程桌面连接到Windows 10 | MOS86
  8. android弹窗不能手动关闭_Android弹窗的实现及相关bug
  9. 打算升级到 Monterey?我们为你准备了一份 macOS 安全升级指南
  10. 对话阿里云李飞飞:数据库迎来开源新时代 | 《新程序员》