产品开发经验分享5:数据迁移

186 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情

在上一篇《产品开发经验分享4:微服务拆分》,我们讨论了一些推进微服务过程中遇到的一些问题,今天来着重讨论一下,服务在迁移过程中,数据迁移可能涉及到的点。

常用的数据依赖有MySQL和Redis。对MySQL来说,又可分为提供给读请求的数据,和提供写入的数据。

对于读数据来说,如果目标数据库跟源数据库存在一些差异,则需要我们对数据进行一些处理。

分表与聚合

比如,源数据库因为数据量过大,我们希望采用分表的形式去重新规划数据库,比如,可以根据用户id的后两位,拆分成100张表。

if __name__ == '__main__':
    with open('sql.txt', 'w') as f:
        for i in range(100):
            s = "0" + str(i) if i < 10 else str(i)
            sql = f"""CREATE TABLE `user_{s}` (
          `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
          `uid` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT 'up主mid',
          `task_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '任务id',
          `ctime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间,任务绑定时间(实际)',
          `mtime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
          PRIMARY KEY (`id`),
          UNIQUE KEY `uk_uid_tid` (`uid`, `task_id`),
          KEY `ix_mtime` (`mtime`)
        ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户和任务关联表';\n"""
            f.write(sql)

然后,可以先把源数据库的数据进行导出,再通过写脚本,按100张拼接成若干的SQL,进行到导入。

如果反过来,我们原来是用的分表,现在觉得分表不是很有必要,如何去做聚合呢?

INSERT INTO user_all (uid,task_id,ctime,mtime,is_del) SELECT uid, task_id,ctime,mtime,is_del FROM user_00;
INSERT INTO user_all (uid,task_id,ctime,mtime,is_del) SELECT uid, task_id,ctime,mtime,is_del FROM user_01;

通过上面这段SQL,可以将原来的表给聚合成新的一张表,然后直接迁移。

关联关系调整

还有一类数据迁移中常见的场景是,因为业务逻辑的调整,导致表结构变了,这时候需要先分析下业务有哪些调整,再对照下药写脚本进行导入。

比如之前我有遇到过,业务关系从一对多,变成了多对多,这时候,除了迁移数据之外,也不能忘记去补充新的关联关系。

if __name__ == "__main__":

    rows = []
    category_sql = []
    task_sql = []

    with open("import.csv") as f:
        reader = csv.DictReader(f, delimiter=',')
        for row in reader:
            rows.append(row)

    default_category_id = 1
    for row in rows:
        task_id = int(row["id"])
        sql = f"INSERT INTO task_category (task_id, category_id) VALUES ({task_id}, {default_category_id});\n"
        category_sql.append(sql)

    for row in rows:
        task_id = int(row["id"])
        title = row["title"]
        
        params = f"({task_id}, '{title}'"
        sql = f"INSERT INTO task (id, title) VALUES {params};\n"
        task_sql.append(sql)

    with open('inspiration0415.sql', 'w') as f:
        for i in category_sql:
            f.write(i)
        for i in task_sql:
            f.write(i)

写数据迁移

另一类比较麻烦是写数据迁移,因为这会影响到用户的写入操作。可以先评估下影响面,如果可以接受短暂的停服的话,则会容易很多,就变成了前面所总结的读数据迁移的过程。

如果不能接受停服,我们可能要采用双写的形式,即数据两边都在写,这样无论从新服务读,还是从老服务读,都能正常给用户提供服务。

详细的过程为:

先记录下老服务当前的id,然后老服务提供对外服务的过程中,同时往新服务进行写入,写入的时候,如果业务不要求id一致,可直接insert,否则就要考虑insert的时候带上id。

然后开始迁移老服务在该id之前的所有数据。这一步要考虑一下主键冲突。

迁移完成后,新服务的数据跟老服务就是一致的了,这时候新服务也开放读请求。

等流量全部到新服务上之后,老服务就可以停掉了。

写数据的迁移还是比较复杂的,如果能接受停服处理的话,会更简单一点。