程序员如何快速迁移 10 亿级数据?

本文介绍了一种优化的大数据迁移方案,通过改进数据查询、批量插入和删除策略,显著提高了迁移效率,尤其适用于超过21亿条记录的超大规模数据迁移场景。

 

640?wx_fmt=gif

640?wx_fmt=png

640?wx_fmt=png

 

640?wx_fmt=png

问题分析

 

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

 

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

 

640?wx_fmt=png

 

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

 

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

 

640?wx_fmt=png

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

 

 

640?wx_fmt=png

解决问题

 

 

由于单表数据量已经超过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天......

 

 

640?wx_fmt=png

改进做法

 

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

 

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

 

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

 

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

 

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

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

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

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

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

https://edu.youkuaiyun.com/topic/ai30?utm_source=csdn_bw

 

 

 

640?wx_fmt=png

补充内容

 

 

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

 

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

 

 

640?wx_fmt=png

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情况(不高):

 

640?wx_fmt=png

 

磁盘队列情况(不高):

 

640?wx_fmt=png

 

640?wx_fmt=png

写在最后

 

 

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

 

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

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

  • 合理设置SqlBulkCopy参数;

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

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

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

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


640?wx_fmt=jpeg

 热 文 推 荐 

腾讯往事:微信其实就是第四代 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 "点个在看吧!"

640?wx_fmt=png喜欢就点击“在看”吧!

### 架构师和程序员的核心思维差异有哪些? 虽然架构师通常是从优秀程序员成长而来,但两者在**思维方式、关注重点、决策维度和责任范围**上有本质区别。可以用一句话概括: > **程序员解决“怎么做”(How),架构师决定“做什么”和“为什么这么做”(What & Why)。** --- 下面我们从多个维度详细对比两者的思维差异,并辅以代码示例说明。 --- ## 🔍 一、关注点不同 | 维度 | 程序员(Developer) | 架构师(Architect) | |------|---------------------|-----------------------| | **目标** | 实现功能、完成任务 | 设计系统、控制全局风险 | | **时间视角** | 当前迭代/本周需求 | 未来6个月~3年技术演进 | | **问题焦点** | “这个接口怎么写?” | “这个服务要不要拆?” | ### 🧩 示例:用户注册功能 ```python # 程序员视角:实现注册逻辑 def register_user(username, password, email): if not validate_email(email): return {"error": "邮箱格式错误"} hashed = hash_password(password) db.execute("INSERT INTO users ...") send_welcome_email(email) # 同步发送 return {"success": True} ``` ✅ 完成功能 ✔️ ⚠️ 但可能忽略: - 邮件发送失败怎么办? - 并发注册时会不会重复插入? - 用户量暴涨怎么办? --- ```text # 架构师视角:思考整体设计 1. 注册是否要异步化? → 改用消息队列解耦 2. 是否需要限流? → 防止刷号攻击 3. 数据库选型? → MySQL 还是 NoSQL? 4. 是否微服务化? → 用户服务独立部署 5. 如何灰度发布? → 流量切分策略 6. 故障如何降? → 邮件发不出去是否阻塞注册? ``` ➡️ 最终方案可能是: ```mermaid graph LR A[前端] --> B(API Gateway) B --> C{User Service} C --> D[(MySQL)] C --> E[Kafka] E --> F[Email Worker] ``` --- ## 🔍 二、抽象层次不同 | 层次 | 程序员 | 架构师 | |------|------|--------| | 抽象粒度 | 函数、类、模块 | 系统、组件、交互关系 | | 思维方式 | 自顶向下编码 | 自底向上建模 + 自顶向下设计 | 🔧 程序员关心: - 变量命名是否规范 - 函数是否有副作用 - 单元测试覆盖率 🧱 架构师关心: - 模块边界是否清晰(高内聚低耦合) - 是否符合六边形架构 / 清晰架构 - 服务间通信是同步还是异步 --- ## 🔍 三、权衡(Trade-off)能力差异 架构师必须在多个冲突目标之间做平衡,而程序员更专注于执行既定方案。 | 权衡项 | 典型问题 | |--------|----------| | ✅ 性能 vs 成本 | 用 Redis 缓存全量数据提升性能,但成本翻倍值不值? | | ✅ 可靠性 vs 复杂度 | 引入 Kafka 提高可靠性,但运维复杂度上升 | | ✅ 快速上线 vs 可维护性 | 直接硬编码业务规则可快速交付,但后期难以扩展 | 📌 架构师常说的一句话是:“It depends.”(视情况而定) 而程序员更倾向于:“给我一个明确的方案,我来实现。” --- ## 🔍 四、风险预判与容错设计 | 场景 | 程序员常见做法 | 架构师考虑的问题 | |------|----------------|------------------| | 调用第三方 API | 直接调用,出错就抛异常 | 是否重试?熔断机制?降策略? | | 数据库压力大 | 加索引 | 是否读写分离?分库分表?缓存穿透怎么防? | | 服务宕机 | 查日志重启 | 是否自动恢复?是否有备用节点? | ### 💡 代码对比:调用支付网关 ```python # 程序员写法:直接调用 def pay(order_id, amount): response = requests.post("https://payment-gateway/pay", json={...}) return response.json() ``` 潜在问题: - 网络超时? - 网关挂了? - 重试导致重复扣款? --- ```python # 架构师推动的设计:具备容错能力 import backoff from circuitbreaker import circuit @backoff(retry_on=ConnectionError, max_tries=3) @circuit(failure_threshold=5, recovery_timeout=60) def pay(order_id, amount): try: response = requests.post(url, json={...}, timeout=3) if response.status_code == 503: raise ServiceUnavailable return response.json() except Timeout: log_error("Payment timeout") raise ``` 同时建议: - 使用幂等令牌防止重复支付; - 记录事务日志用于对账; - 引入 Saga 模式处理分布式事务。 --- ## 🔍 五、沟通与决策方式不同 | 角色 | 决策方式 | 沟通对象 | |------|---------|-----------| | 程序员 | 执行者:按需求文档开发 | 和产品经理确认细节,和同事联调接口 | | 架构师 | 决策者:制定技术方向 | 和高管谈成本与战略,和技术团队定规范 | 🎯 架构师不仅要懂技术,还要: - 听懂业务语言; - 向非技术人员解释技术限制; - 在“理想架构”和“现实约束”之间找到平衡点。 --- ## 🔍 六、工具使用层不同 | 工具类型 | 程序员用法 | 架构师用法 | |--------|------------|-------------| | Git | 提交代码、合并分支 | 设计分支策略(Git Flow vs Trunk-Based) | | Docker | 打包应用 | 设计镜像仓库策略、安全扫描流程 | | Kubernetes | 部署自己的服务 | 设计命名空间隔离、RBAC 权限模型、Ingress 控制器选型 | | CI/CD | 触发构建 | 设计整条流水线:自动化测试、灰度发布、回滚机制 | --- ## ✅ 总结:核心思维差异一览表 | 维度 | 程序员 | 架构师 | |------|--------|---------| | **目标** | 实现功能 | 控制系统复杂性 | | **时间跨度** | 短期(当前迭代) | 中长期(半年以上) | | **抽象层** | 代码层 | 系统层、平台层 | | **决策依据** | 需求文档 | 业务目标 + 技术趋势 + 成本约束 | | **关注质量属性** | 功能正确性 | 可靠性、可扩展性、安全性、可维护性 | | **典型问题** | “这段代码怎么优化?” | “这个系统五年后还能支撑吗?” | | **失败后果** | 某个功能出 bug | 整个系统不可用或重构代价巨大 | --- ### 💡 成长路径建议:程序员如何向架构师转型? 1. **多问“为什么”**:不要只接受需求,要理解背后的业务动机。 2. **学习系统设计**:掌握 CAP、一致性模型、分布式事务等理论。 3. **参与技术选型讨论**:主动提出替代方案并分析利弊。 4. **写设计文档(ADR)**:记录架构决策及其理由。 5. **模拟演练**:尝试为现有系统做重构设计或迁移上云方案。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值