尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从SQL Server到OceanBase:手游核心库迁移实战与避坑指南

发布时间:2026/9/26 23:06:14

资讯中心
01
ARTICLE

从SQL Server到OceanBase:手游核心库迁移实战与避坑指南

从SQL Server到OceanBase:手游核心库迁移实战与避坑指南
去年下半年我们团队接到一个挺实际的活帮一家马来西亚手游公司把核心数据库从 SQL Server 迁到 OceanBase。这家公司产品主要在东南亚发行休闲游戏为主日活几十万后端有玩家账号、充值流水、排行榜、礼包码、运营后台一大堆业务全都压在 SQL Server 上。评估完现状之后我们发现这不是一次简单的“导出再导入”里面涉及兼容性改造、增量同步、割接切换、回滚预案甚至还要和吉隆坡那边的运维同事跨时区配合。这篇就把我们当时的完整思路和执行过程写出来给正在做同类国产数据库迁移的团队一个参考。1. 从 SQL Server 出走马来西亚游戏业务的三个真实痛点1.1 并发写入瓶颈与排行榜查询卡顿这家公司的游戏业务有一个很典型的东南亚特征现金玩家占比高充值接口在晚上和节假日会出现很明显的峰值。SQL Server 在单机架构下跑得还算稳定但到了晚间活动时段玩家同时写充值流水、更新钱包余额、刷新排行榜几个大表的锁竞争一下子就上来了。我们当时抓到的瓶颈主要集中在三张表recharge_order充值订单、player_balance玩家余额、leaderboard_daily每日排行榜。排行榜尤其痛苦。SQL Server 的普通索引在千万级行数下做“全局排名查询”时计算成本很高运营要看的又是实时排名经常是一条 SQL 打过来几个从库 CPU 同时飙到 80% 以上。用过 SQL Server 的同学都知道这种情况不是加索引能彻底解决的ROWNUMBER() OVER全表排序的成本摆在那里。我们试过用汇总表、缓存中间层但最终还是觉得必须从引擎层面换一种思路。1.2 许可证成本与扩容限制马来西亚分公司和国内母公司走的是统一财务口径SQL Server 的许可证费用每年都在涨。更麻烦的是核心库跑在云上 Windows 虚拟机里规格受限于单台机器的 CPU 和内存上限想要横向扩展就得做 AlwaysOn 可用性组操作复杂度高而且对网络延迟敏感。东南亚几个机房的网络链路本身就不是特别稳定跨地域同步经常出现日志分发延迟运维同学半夜被叫起来处理同步中断的次数太多了。我们评估过把架构改为“分库分表 读写分离”但按当时的团队规模和运维人力这套方案要自己处理分布式事务、全局 ID 生成、跨库 JOIN代价太高。于是目标慢慢锁定到了 OceanBase 上——它原生就是分布式架构可以横向扩展同时兼容 MySQL 协议应用侧改造相对可控。1.3 备份恢复窗口过长这是压垮骆驼的最后一根稻草。当时核心库全备一次要 6 小时以上日志备份每 15 分钟一断恢复演练时目标恢复时间要按小时算。游戏业务对数据一致性要求极高一旦出问题玩家充值的钱丢了可不是小事。OceanBase 在这块的优势是物理备份与多副本机制以及基于日志的实时恢复能力至少能让我们把恢复目标压缩到分钟级。综合这几个原因项目正式立项目标是在四个月内完成从 SQL Server 到 OceanBase 的迁移。2. 迁移前必须做的“兼容性摸底”一张清单走天下2.1 目标端模式选型OceanBase MySQL 模式很多人一上来就问“OceanBase 不是兼容 Oracle 吗”实际上 OceanBase 支持 MySQL 和 Oracle 两套模式。我们这次选的是 MySQL 模式原因是业务团队对 MySQL 的生态更熟悉Java 后端连接 MySQL 的工具链、监控、运维经验都可以直接复用。选型确定之后第一件事不是动手迁移而是把现有 SQL Server 库里的对象完整盘点一遍。我们当时梳理出来的对象包括几百张业务表、两百多个存储过程、几十个视图、十来个触发器还有若干作业任务SQL Server Agent Job。这里面最耗时间的不是表是存储过程和视图里的 T-SQL 写法。很多历史代码是早几任开发留下来的里面充斥着WITH (NOLOCK)、GETDATE()、ISNULL()、CONVERT()、TOP这类 SQL Server 专属语法迁到 MySQL 模式全部要改。2.2 数据类型和函数差异对照我们做了个简单的对照表给开发团队每迁一个存储过程就对着表改效率提高很多。这里贴一份核心对照SQL ServerOceanBaseMySQL 模式备注INT/BIGINTINT/BIGINT直接兼容NVARCHAR(n)VARCHAR(n) CHARACTER SET utf8mb4注意索引长度限制DATETIMEDATETIME(3)保留毫秒精度UNIQUEIDENTIFIERVARCHAR(36)或使用UUID()生成BITTINYINT(1)注意驱动返回类型差异MONEYDECIMAL(19,4)避免浮点精度问题IMAGE/TEXTLONGBLOB/LONGTEXT大字段单独评估ROWVERSION/TIMESTAMP删除或改为BIGINT业务不要依赖该字段XMLJSON/LONGTEXT应用侧解析逻辑需调整函数层面GETDATE()换成NOW()ISNULL()换成IFNULL()CHARINDEX()换成INSTR()LEN()换成CHAR_LENGTH()TOP n换成LIMIT n。这些看着简单但一个存储过程里可能混着十来个函数漏改一个就报错。我们的做法是先靠自动化脚本批量扫描关键字再人工逐个 review。2.3 自增列迁移ID 断档与冲突的隐患SQL Server 的IDENTITY自增列和 MySQL 的AUTO_INCREMENT看着相似实际切换时有一个特别容易被忽略的坑如果目标表的自增起始值没有设置成“原表当前最大值 1”一旦应用写入新数据就会出现主键冲突。我们当时的做法是在导出数据后记录每张表的当前自增值导入完成后用ALTER TABLE ... AUTO_INCREMENT n手工调整。另外还要注意 SQL Server 允许IDENTITY_INSERT ON后显式插入自增列但 OceanBase MySQL 模式对显式插入自增列的限制更严格需要先在会话里设置对应的 SQL mode。所有批量插入脚本都要考虑到这个差异否则同步中断后重放日志时会卡住。3. 搬迁执行影子库、全量导出与增量同步的配合3.1 影子库试跑先让应用在新库上“跑一遍”正式迁移之前我们花了两周时间搭建了一个影子环境从生产 SQL Server 备份中恢复出一套完整的库然后把对象转换脚本跑一遍导入到 OceanBase 测试租户。这个环境的核心价值在于让新代码提前接受真实流量的检验而不是等割接那天才第一次见面。影子库试跑阶段我们把应用的所有模块都指到新库上让 QA 团队按原有用例回归一遍。结果发现了很多兼容性 bug有些旧存储过程在 SQL Server 里能跑但到了 MySQL 模式下会报“Unknown column”“syntax error”之类的错误还有些应用代码里写了 SQL Server 特有的分页写法OFFSET ... FETCH NEXT虽然驱动是 JDBC但 SQL 方言还残留着必须改成LIMIT。影子库阶段发现的 bug 越多割接那天的风险就越小这句话在项目结束后体会特别深。3.2 全量数据迁移bcp 导出与 obloader 导入组合全量迁移阶段我们的总体方案是使用 SQL Server 自带的bcp工具把关键大表导出为 CSV 文件。通过obloader将 CSV 批量导入 OceanBase。小表直接用应用侧脚本或 DTS 工具同步不走文件导出。bcp导出的时候有几个注意点。第一是要统一字符集我们用了-C 65001指定 UTF-8 编码避免导出后中文和马来文乱码。第二是字段分隔符要选一个数据里几乎不会出现的字符比如\t或者|||否则某行数据里正好包含逗号时导入端解析会错位。第三是导出大表时要分批做比如按主键范围分片每片 500 万行避免bcp长时间运行占用源库太多资源。导入端我们前后对比过两种方式直接使用INSERT INTO ... VALUES批量写入以及使用obloader并行导入。实测下来obloader明显更高效它对 OceanBase 内部做了并行分片和写入优化2.8 亿行的最大表按 20 个并发文件导入大概 4 小时左右跑完。如果自己写脚本一条条插入估计要几十个小时。3.3 增量同步开启 CDC 并自研消费端全量导入完成后业务库还在持续产生新数据这时候就需要增量同步把源库和目标库的差距补上。我们评估过官方的迁移服务对 SQL Server 源端的支持力度发现还不是特别成熟索性用了 SQL Server 自带的 CDCChange Data Capture机制对需要同步的表开启 CDC然后自研了一个增量消费程序读取变更日志经过类型映射和函数替换后写入 OceanBase。CDC 的开启方式不复杂核心命令大致是EXEC sys.sp_cdc_enable_db; EXEC sys.sp_cdc_enable_table source_schema dbo, source_name recharge_order, role_name NULL, supports_net_changes 0;消费端程序我们用的是 Java 写的每秒钟轮询一次 CDC 表获取__$operation字段判断操作类型1删除2插入3更新前镜像4更新后镜像然后翻译成对应的 SQL 语句写入 OceanBase。更新操作在 CDC 里默认产生两条记录需要合并成一条UPSERT我们直接用了 OceanBase 的INSERT ... ON DUPLICATE KEY UPDATE语法省掉了先查后写的往返开销。增量同步期间最重要的监控指标是延迟。我们给消费程序加了 Prometheus 监控每次同步一批数据就记录当前水位时间如果延迟超过 30 秒就告警。整个双跑阶段持续了大概三周增量延迟基本稳定在 5 秒以内没有出现过丢失数据的情况。3.4 数据校验行数、抽样与 checksum 三重验证增量同步稳定运行之后还要回答一个灵魂拷问两边的数据到底一不一样我们做了三层校验第一层是行数校验按表对比源端和目标端的COUNT(*)每天跑一次不等就报警。第二层是抽样校验对每张表随机抽取几百行逐字段比对值是否一致重点看DATETIME、DECIMAL、NVARCHAR这几个容易出问题的类型。第三层是自定义 checksum对关键大表按主键分片把每片所有字段拼起来算 MD5再对比两端结果。MD5 一致基本可以认为数据完全对齐。校验脚本本身不复杂难的是跑批时的资源控制。千万级以上的表做全字段 MD5 很吃 CPU我们特意把校验任务安排在凌晨业务低峰期执行并且限了并发数避免影响生产。4. 迁移路上的高发坑点兼容语法、事务隔离与字符集4.1 T-SQL 存储过程迁移的真实工作量存储过程迁移是这次项目里工作量最大的部分也是坑最多的部分。有些存储过程长达几百行里面既有动态 SQL又用了临时表、游标、递归 CTE改起来相当头疼。汇总一下我们遇到的高频问题。首先是WITH (NOLOCK)全部要去掉。SQL Server 的NOLOCK提示用于避免行锁阻塞但在 OceanBase MySQL 模式下没有这个语法。去掉之后要注意业务能否接受读已提交下的轻微锁等待我们这里通过把隔离级别调整为READ COMMITTED基本没有感受到明显性能回退。其次是临时表的差异。SQL Server 的#temp表在 MySQL 模式下没有对应概念我们统一改成了普通表 事务处理或者尽量用子查询 / CTE 替代。如果确实需要临时表记得在会话或事务结束后显式清理否则连接池复用连接时可能出现表已存在的报错。再就是动态 SQL 的拼接。SQL Server 的EXEC(SELECT ...)在 MySQL 模式下可以使用PREPARE/EXECUTE/DEALLOCATE但参数占位符从p1要改成?。如果代码里拼接了大量字符串这一步非常容易出问题。我们的处理方式是优先改造为预编译语句实在不行才用动态 SQL并且做好白名单校验防止注入风险。4.2 事务隔离级别与死锁行为差异SQL Server 默认隔离级别是READ COMMITTEDOceanBase MySQL 模式默认是REPEATABLE READ。如果不显式调整某些长事务的锁范围和可见性会和我们预想的不一致进而影响并发表现。我们当时把所有租户级和会话级的默认隔离级别都调成了READ COMMITTEDSET GLOBAL transaction_isolation READ-COMMITTED;还要留意的是死锁行为差异。SQL Server 的死锁检测机制比较“温和”会选一个代价较小的会话回滚让另一个继续执行OceanBase 在极端并发下也有一套死锁检测但具体表现会受事务执行计划影响。我们遇到过几次因事务里更新顺序不一致导致的死锁解决办法很老套却很有效——所有涉及多表更新的代码统一按照表名排序后依次加锁避免交叉持锁。4.3 字符集与排序规则NVARCHAR 的隐藏成本SQL Server 的NVARCHAR默认按 UTF-16 存储Java 后端读取后显示一切正常。但迁到 OceanBase 后我们统一使用utf8mb4字符集这里有一个非常容易被坑的点utf8mb4下一个中文字符占 4 字节一个普通的VARCHAR(255)在大多数索引场景下可能超过 InnoDB 的索引长度限制3072 字节。当时有几张表的nickname字段定义为NVARCHAR(255)在 SQL Server 里建索引没问题迁移后在 OceanBase 里直接报“Specified key was too long; max key length is 3072 bytes”。我们的处理方案是把这类字段统一改成VARCHAR(191)或者使用前缀索引nickname(64)。这带来一个业务影响如果玩家昵称很长排序或匹配的精度可能下降但实际游戏场景里几乎没有超过 191 个字符的昵称测试验证后完全可用。另外排序规则也要考虑。原先 SQL Server 里用的是Chinese_PRC_CI_ASOceanBase 的 MySQL 模式默认可能是utf8mb4_general_ci或utf8mb4_0900_ai_ci两者对大小写和重音字符的处理有差异。如果业务里有对字符串排序敏感的功能比如按昵称排序的排行榜要提前确认排序结果是否符合预期。4.4 数据库账号与权限模型变化SQL Server 的登录名、用户名、角色体系和 OceanBase MySQL 模式的账号体系差异很大。SQL Server 里一个登录名可以映射到多个库而 OceanBase 的账号通常是“用户名租户名”的形式权限按数据库对象去授予。迁移过程中我们给应用单独创建了专用账号只授予SELECT、INSERT、UPDATE、DELETE和必要的EXECUTE权限不授予 DDL 权限减少误操作风险。还有一个小细节是连接串。SQL Server 的 JDBC 连接串长这样jdbc:sqlserver://1.2.3.4:1433;DatabaseNamemydb切到 OceanBase 后我们通过 OBProxy 连接端口是 2883连接串变成 MySQL 格式jdbc:mysql://10.0.0.10:2883/myob?useUnicodetruecharacterEncodingutf8mb4useSSLfalse驱动从com.microsoft.sqlserver.jdbc.SQLServerDriver换成com.mysql.cj.jdbc.Driver这行改动看似简单但应用里如果有依赖“SQL Server 专属连接属性”的地方需要逐个排查。5. 割接那晚停服窗口、连接串切换与回滚预案5.1 切换前检查清单割接前一周我们列了一张很细的检查清单这里挑几条关键的分享所有对象迁移完成且跑完三轮数据校验源库和目标库行数一致。增量同步延迟最低降到 0并持续观察 15 分钟以上。应用所有连接串、配置文件、环境变量中不再包含旧库地址。运维监控面板新增了 OceanBase 相关指标包括租户 CPU、内存、活跃会话数、慢 SQL 数量。数据库账号权限验证完毕应用账号能从测试环境正常连接到目标租户并执行全部核心 SQL。回滚方案确认旧库保留只读访问网络策略不变保证可以随时切回。5.2 应用侧只改一个配置由于迁移过程中我们已经把 SQL 方言都改成了 MySQL 兼容写法并且通过影子库做了充分验证真正割接那晚的应用改动其实很小运维把配置中心里的数据源连接串统一替换掉然后滚动重启应用实例。我们的停服窗口选在凌晨 4 点到 6 点吉隆坡和北京没有时差配合起来还比较顺畅。当天晚上的步骤大致是停掉应用写入流量前端置维护页。等待增量同步延迟归零。停掉 CDC 消费程序记录最终水位点。再跑一次全库行数校验确认零差异。切换连接串重启应用。验证核心接口登录、充值、排行榜、礼包码按预跑脚本逐项检查。确认无异常后放量 10% 用户进入观察 20 分钟再全量放开。整个过程比预想顺利唯一的小插曲是切换后有几台应用节点的数据库连接池没有及时释放旧连接导致启动时报了几条连接超时。解决办法是让运维把连接池的testOnBorrow打开强制校验连接可用性旧的坏连接自动剔除。5.3 回滚预案宁可备而不用不可用而不备虽然大家都希望一次成功但回滚预案必须提前写好。我们的回滚触发条件是切换后核心业务接口错误率超过 1%或数据库活跃会话数持续超过预期阈值或玩家充值链路出现数据不一致。回滚的操作其实很简单把配置中心的连接串改回 SQL Server 地址重启应用。因为迁移期间源库一直还在正常接收写入旧库数据并没有断档。唯一的损失是切换窗口内产生的少量新数据这部分我们通过 CDC 消费端把 OceanBase 上的增量反向灌回 SQL Server可以追平。当然OceanBase 到 SQL Server 的反向同步没有现成工具我们是用针对业务表的ON DUPLICATE KEY UPDATE叠加业务主键去重实现的形式上有点粗糙但胜在可控。最终我们没有触发回滚但把这个方案完整演练过两次。每一次演练都会发现新的问题比如连接池参数不合适、反向同步脚本时间戳格式不兼容等通通在正式割接前修掉了。5.4 上线后的持续观察与调优割接并不意味着项目结束。上线后的头两周我们保持每天两次数据对账同时关注 OceanBase 的执行计划。有一个比较明显的优化点原先在 SQL Server 上习惯用IN (SELECT ...)的写法在 OceanBase 上某些场景会生成低效的执行计划需要改成JOIN或者加/* PARALLEL */提示。我们还把之前 SQL Server 里一批手工维护的索引统计更新作业换成了 OceanBase 的自动统计信息收集省掉了 DBA 不少重复劳动。至于性能数据最明显的变化是排行榜查询的响应时间从原来的 500 毫秒以上降到了 100 毫秒以内晚高峰充值流水写入的锁等待基本消失数据库 CPU 峰值也低了不少。如果让我重新做一次这个迁移大概率会把节奏放得更稳一点。不是说技术方案需要大改而是团队对新数据库的“手感”需要时间培养——SQL Server 和 OceanBase 的运维习惯、错误日志解读、慢 SQL 分析思路看着相似实际差别不小。上线后一个月内我们花了大量时间给开发和运维同学做内部培训把迁移期间踩过的坑整理成文档后续再有新项目迁过来照着这套流程走就快多了。另外想提醒一句数据库迁移项目的成功不是“切完那一刻”决定的而是从现在到你未来半年每一次发版、每一次数据修复、每一次业务变更中体现的。给团队留下足够的缓冲期比任何技术方案都重要。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。