Logo
异构双写

异构双写 #

随着国内信创地不断推进,越来越多企业选择将传统的 Oracle、SQLServer 数据库,替换为自主可控的国产数据库或开源数据库。数据库作为业务系统的基石,在技术架构中一直扮演着重要的角色。 因此在数据库的替换过程中,我们需要考虑包括但不限于系统的安全性、稳定性、时效性以及替换风险。为了规避新架构所带来的不确定风险,如兼容性、数据一致性、流量控制及逃生方案等,DBPlusEngine 提供了异构数据双写功能,通过自动的 SQL 翻译和在线切流能力,能够帮助企业以最小的代价实现数据库替换。

概述 #

SphereEx-DBPlusEngine 的异构双写插件提供了存量数据迁移、存量数据一致性校验、增量数据双写、分布式事务强一致保证、基于业务 SQL 特征的灰度切流、高可用自动退化、自动 SQL 翻译、SQL Hint 干预 SQL 翻译以及双写组合分片等能力。

这些能力能够高效地完成存量数据的迁移并保证数据的一致性。增量数据部分,用户可以配置双写规则,将写操作同时写入到多个异构数据库中,通过分布式事务可以保证数据的强一致。 双写过程中,原有业务 SQL 中的方言,能够通过 DBPlusEngine 的 SQL 翻译器自动翻译为目标方言,业务代码几乎无需改动,当遇到少数自动 SQL 翻译不支持的场景,用户也可以通过 SQL Hint 干预 SQL 翻译的结果,保证业务兼容性。 此外,DBPlusEngine 还提供了在线切流功能,能够按比例切换读流量进行灰度验证,当目标端出现问题时,自动退化功能能够快速切换回源端数据库,保证业务的可用性。

基本概念 #

写数据源 #

写数据源 writeDataSourceNames 用于配置异构双写需要写入的数据源,可以配置源端数据源和目标端数据源,在整个数据库替换过程中,通过动态更新写数据源可以实现单写和双写的切换。

读数据源 #

读数据源 readDataSourceNames 用于配置异构双写需要读取的数据源,可以配置源端数据源和目标端数据源,当同时配置源端和目标端数据源,异构双写功能会使用负载均衡器对查询请求进行分配。

主数据源 #

主数据源 primaryDataSourceName 用于声明在异构双写中哪个数据源作为主数据源,在进行高可用自动退化时,会判断故障数据源是否为主数据源,主数据源故障,会抛出异常中断业务,其他数据源故障则会退化至主数据源读写。 此外,DBPlusEngine 在进行异构数据库双写时,会优先写入主数据源,然后再写入其他数据源,保证主数据源中的数据完整可靠。写操作的返回结果会使用主数据源的执行结果,即使在源端数据库和目标端数据库不一致的情况下,也可以保证写业务正常运行。

拦截器 #

拦截器 interceptorName 用于实现基于 SQL 特征的读流量灰度切换,目前可选项包括:拦截全部读流量(默认)、基于正则拦截读请求、基于 SQL 拦截读请求以及基于表拦截读请求

负载均衡算法 #

负责均衡算法 loadBalancerName 用于声明读流量负载均衡的方式,目前可选项包括:ROUND_ROBIN(默认)、RANDOM 和 WEIGHT

主键生成策略 #

主键生成策略 keyGenerateStrategies 用于声明异构双写功能的主键生成策略,用户可以为特定的表配置主键生成算法,从而保证双写数据的主键一致,目前可选项包括:SNOWFLAKE(默认)、UUID、NANOID 和 REDIS_AUTO_INCREMENT

适用场景 #

适用于企业进行数据库替换的场景,通过双写、SQL 翻译及在线动态切流的方式,平稳地完成数据库替换。

使用前提 #

  • 源端数据库和目标端数据库必须支持 XA 协议,并且数据库账号需要具有 XA 权限;
  • 数据库账号需要具备建表、删表权限,存量数据迁移基于更新时间戳字段实现,对于删除操作更新时间戳无法获取变更记录,需要创建临时表存储删除数据的主键,并在完成目标数据库的同步后删除临时表。

使用限制 #

  • 不支持存储过程、函数、视图、触发器等对象的使用,所有操作都需要经过 DBPlusEngine 进行处理;
  • 异构双写组合使用分片功能时,不支持分片功能限制使用的 SQL 语句,具体列表可以参考分片功能使用限制

注意事项 #

异构双写期间,应该尽量避免对相关表执行 DDL 操作,避免由于 DDL 语句不兼容导致双写出错,如果确实需要执行 DDL 操作,需要根据 DDL 类型制定方案。

原理介绍 #

异构双写功能主要包括存量数据迁移、增量数据双写、分布式事务强一致保证、基于业务 SQL 特征的灰度切流、高可用自动退化、自动 SQL 翻译、SQL Hint 干预 SQL 翻译以及双写组合分片等能力。下面我们将逐一介绍这些能力的原理。

存量数据迁移 #

存在数据迁移任务开始前,需要用户在业务表中增加 last_update_time 字段,用于区分任务开启后写入的增量数据。增加完成字段后,应用程序需要对接 DBPlusEngine。

对于 Insert/Update 语句,DBPlusEngine 会改写逻辑 SQL,在逻辑 SQL 中增加 last_update_time 字段后再写入到数据库中。而对于 Delete 语句,则会在源端数据库维护额外的删除表,确保能够记录增量更新中的删除操作。

通过 DBPlusEngine 维护 last_update_time 字段后,用户可以执行 DistSQL 开启数据迁移。数据迁移任务内部会记录当前时间戳,然后对存量数据进行迁移,存量数据迁移完成后,任务内部会继续进行增量数据迁移,增量数据迁移会通过时间戳范围获取增量数据,然后分批次写入到目标库表中。

增量迁移阶段,用户可以执行 DistSQL 开启一致性校验,DBPlusEngine 默认提供了实时一致性校验功能,能够动态地对数据库进行校验,校验过程中检测不一致的数据会记录下来,等待增量任务完成后再次校验,当连续 3 轮校验通过时,则认为一致性校验通过,此时用户可以执行 COMMIT 语句切换到双写规则。

SQL 方言翻译 #

DBPlusEngine 支持自动 SQL 翻译,会基于逻辑 SQL 解析生成的 AST,并且结合元数据进行语义分析,然后根据不同方言的特征,对语法、函数、标识符等进行转换,业务代码几乎无需改造。SQL 翻译器可选类型参考 内置算法 - SQL 翻译器 针对极少数数据库语义不兼容的 SQL,例如 Oracle MERGE 语句,DBPlusEngine 提供了 SQL 翻译 Hint 功能,用户可以通过 SQL Hint 干预 SQL 翻译的结果,保证业务兼容性,具体参考 Hint - SQL 翻译器

增量数据双写 #

DBPlusEngine 完成存量数据迁移后,会切换到增量数据双写,双写主要分为三个阶段,第一阶段用户通过 DistSQL 可以将写流量路由到源端和目标端的数据库,通过分布式事务保证数据的强一致性。 第二阶段仍然保持双写,并通过读流量负载均衡算法(读流量负载均衡算法参考 内置算法 - 负载均衡算法),可以动态调整分配到目标端的读流量比例,直到全部读流量访问目标端数据库。第三阶段则完全将读写流量切换至目标端数据库,完成数据库的切换。

高可用自动退化 #

DBPlusEngine 支持高可用自动退化,当目标端数据库出现故障时,DBPlusEngine 会自动切换至源端数据库单写单读,并将分布式事务退化为单机事务,保证业务的可用性。 当出现高可用退化时,此时会记录 last_update_time,当目标端数据库恢复时,需要根据该时间戳同步增量变更数据,确保源端和目标端数据一致后,再开启增量数据双写功能,并进行读流量切换。

使用指南 #

使用异构双写功能完成数据库替换时,需要遵循以下步骤进行准备实施,DBPlusEngine 提供了全流程的改造和替换平台。

  1. 替换准备

针对待迁移系统做好评估工作,确定迁移原则、方式、窗口期等。确定产品选型,并给予相应的培训。

  1. 替换评估

根据架构设计确定资源投入及应用修改,针对数据库对象修改进行评估,并根据评估结果给出工作量。

  1. 替换改造

对现有数据结构及语句进行修改(结构修改、SQL方言转换)。如必要还需进行应用改造。针对完成功能进行测试。

  1. 迁移数据

确定数据迁移方案,进行结构及数据迁移,并对迁移结果进行质量比对。

  1. 上线交割

对完成SQL进行审计,提供双轨并行运行能力。利用灰度测试进行验证,并完成业务流量的切换。

  1. 运行保障

提供在线运维能力,保证现有环境的高可用,提供运行优化及应急响应。

操作指南 #

具体操作 DBPlusEngine 进行异构双写时,需要按照如下的操作步骤进行。

  1. 梳理业务逻辑,评估系统中是否包含存储过程、函数、视图、触发器及其他形式的库内计算,并进行改造去除;
  2. 针对需要双写迁移的业务表,统一增加 last_update_time 字段,并接入 DBPlusEngine 进行维护,DBPlusEngine 注册双写数据源,并将规则配置为源端双写单读 Oracle,如果业务中使用了 Oracle 自增序列,则需要配置 DBPlusEngine 分布式主键功能;
  3. 开启高可用自动退化功能,实时探测目标端可用性,当目标端出现不可用性,DBPlusEngine 将会自动回退至源端单写单独;
  4. 使用 DBPlusEngine 进行存量数据迁移,并使用实时一致性校验功能保证数据一致;
  5. 根据双写测试情况,逐步将读流量切换至目标端数据库,可以配置读流量拦截器针对部分场景先进行验证切流,稳定后再逐步切换更多流量,直至全部读流量切换;
  6. 选择合适的时机,将写操作切换至目标端数据库,完成数据库替换。

配置示例 #

本配置示例以 Oracle 数据库切换 MySQL 数据库为例,介绍了异构双写的完整使用流程,异构双写推荐采用混合部署架构,Proxy 接入端适合进行配置管理和数据迁移,JDBC 接入端适合接入应用程序,保证最佳的性能。 关于混合部署架构,请参考部署架构

SQL 翻译器转换 DDL 语句 & 目标端执行 #

由于 Oracle 和 MySQL 的 DDL 语句不兼容,需要先通过 SQL 翻译器将 DDL 语句转换为 MySQL 方言,然后人工检查转换后的 DDL 语句,包括字段类型、函数以及语法上的差异,避免自动翻译不准确可能导致的问题。 人工检查转换的 DDL 语句后,在目标端数据库批量执行 DDL 语句。

Proxy 初始化配置 #

由于异构双写推荐使用混合部署架构,需要先配置 Proxy 的 global.yaml 文件,开启分布式事务并且指定 SQL 解析方言为 Oracle。为了支持异构双写 SQL 自动翻译,需要配置 sqlTranslator,使用 SQLINES 实现自动翻译。 此外,由于 Oracle 协议为私有协议,Proxy 接入端访问时,需要通过 MySQL 协议及 MySQL 客户端进行访问。配置完成后启动 Proxy。

transaction:
 defaultType: XA
 providerType: Narayana

sqlParser:
  sqlParserType: Oracle
  sqlStatementCache:
    initialCapacity: 2000
    maximumSize: 65535
  parseTreeCache:
    initialCapacity: 128
    maximumSize: 1024
    
sqlTranslator:
  sqlTranslatorCache:
    initialCapacity: 2000
    maximumSize: 65535
  type: SphereEx:SQLINES
  useOriginalSQLWhenTranslatingFailed: true

props:
  proxy-frontend-database-protocol-type: MySQL

Proxy 创建逻辑库 & 注册存储单元 #

  1. 创建逻辑库

使用 mysql -uroot -h127.0.0.1 -p -P3307 -c -A 登录 Proxy,然后创建 DUAL_WRITE 逻辑库,用于业务程序访问。

CREATE DATABASE DUAL_WRITE;
USE DUAL_WRITE;
  1. 注册存储单元

再执行 REGISTER 语句注册 Oracle 和 MySQL 存储单元(修改为不同环境对应的 Oracle 和 MySQL URL 和账号):

REGISTER STORAGE UNIT oracle_ds (
    URL="jdbc:oracle:thin:@localhost:1521/test",
    USER="root",
    PASSWORD="123456",
    PROPERTIES("maximumPoolSize"="50","idleTimeout"="30000")
), mysql_ds (
    URL="jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useSSL=false",
    USER="root",
    PASSWORD="123456",
    PROPERTIES("maximumPoolSize"="50","idleTimeout"="30000")
);

注册成功后,可以执行 SHOW STORAGE UNITS; 语句查看存储单元。

SHOW STORAGE UNITS;

Proxy 创建双写规则 & 双写单读 Oracle #

注册完存储单元后,执行以下 DistSQL 语法创建双写规则,并指定读流量访问 Oracle 数据库,然后执行 LOAD 语句加载双写数据源中的单表:

-- CREATE,创建双写规则
CREATE DUAL_WRITE RULE dual_write_ds (
    PRIMARY_STORAGE_UNIT=oracle_ds,
    WRITE_STORAGE_UNITS(oracle_ds, mysql_ds),
    READ_STORAGE_UNITS(oracle_ds)
);

-- 再执行 LOAD 加载全部单表
LOAD SINGLE TABLE *.*;
     
-- SHOW TABLES 查看加载的单表
SHOW TABLES;

更多双写 DistSQL 语法请参考双写 DistSQL

Proxy 配置分布式主键生成策略 #

如果业务 SQL 中使用了 Oracle 自增序列,则需要配置分布式主键生成策略,并调整业务逻辑中的 INSERT 语句,去除 Oracle 自增序列生成的主键,采用 DBPlusEngine 生成的分布式主键。 执行以下 DistSQL 配置分布式主键策略:

CREATE DUAL_WRITE KEY_GENERATE_STRATEGY t_order (
  COLUMN=order_id,TYPE(NAME="snowflake")),
t_order_item (
  COLUMN=item_id,TYPE(NAME="snowflake", properties("key"="value"))
);

更多双写分布式主键 DistSQL 语法请参考双写 DistSQL。双写支持的分布式主键类型参考内置算法 - 分布式序列算法

Proxy 开启可用性自动退化 #

开启双写功能后,为了避免目标端数据库故障导致业务不可用,需要开启高可用自动退化功能,可以在 Proxy global.yaml 配置中增加 storage-node-heartbeat-interval-millisecondsstorage-node-heartbeat-retry-times 配置。 storage-node-heartbeat-interval-milliseconds 配置用于指定存储节点心跳检测间隔,storage-node-heartbeat-retry-times 配置用于指定存储节点心跳检测重试次数。 可用性自动退化的时长 = JOB 轮训间隔 + 数据库连接池超时时长 * Retry 次数 + 更新 Rule 耗费的时间(秒级别),用户需要根据业务场景要求进行调整。更多属性配置请参考参数 - 集群参数列表

props:
  # 存储节点心跳检测间隔
  storage-node-heartbeat-interval-milliseconds: 5
  # 存储节点心跳检测重试次数
  storage-node-heartbeat-retry-times: 3

如果 Proxy 已经启动,可以通过 DistSQL 实现在线更新。

-- 设置参数
SET DIST VARIABLE storage_node_heartbeat_interval_milliseconds = 5;
SET DIST VARIABLE storage_node_heartbeat_retry_times = 3;
    
-- 查看参数 
SHOW DIST VARIABLE WHERE name = storage_node_heartbeat_interval_milliseconds;
SHOW DIST VARIABLE WHERE name = storage_node_heartbeat_retry_times;

存量数据迁移和一致性校验 #

  1. 创建逻辑库

开启存量数据迁移任务前,需要临时创建一个逻辑库 DUAL_WRITE_MIGRATION 用于迁移处理:

CREATE DATABASE DUAL_WRITE_MIGRATION;
USE DUAL_WRITE_MIGRATION;
  1. 注册存储单元

然后再注册目标端 MySQL 数据源和源端 Oracle 迁移数据源,并加载全部单表:

-- 注册目标端 MySQL 数据源
REGISTER STORAGE UNIT mysql_ds (
    URL="jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useSSL=false",
    USER="root",
    PASSWORD="root",
    PROPERTIES("maximumPoolSize"="50","idleTimeout"="30000")
);

-- 再执行 LOAD 加载全部单表
LOAD SINGLE TABLE *.*;

-- 注册迁移源端数据源
REGISTER MIGRATION SOURCE STORAGE UNIT oracle_ds (
    URL="jdbc:oracle:thin:@localhost:1521/1521",
    USER="root",
    PASSWORD="root",
    PROPERTIES("maximumPoolSize"="50","idleTimeout"="30000")
);
  1. 开启迁移任务

然后使用 MIGRATE 语句启动数据迁移。

-- 目标端表大小写需要和数据库保持一致
MIGRATE TABLE oracle_ds.t_order INTO t_order,
oracle_ds.t_order INTO t_order;
  1. 查看迁移任务列表

查看数据迁移作业列表 DistSQL:

SHOW MIGRATION LIST;
  1. 查看迁移任务详情

查看数据迁移详情 DistSQL:

-- 替换 jobId 为实际值,可以查看迁移进度
SHOW MIGRATION STATUS jobId;
  1. 开启一致性校验

迁移完成后,执行数据一致性校验 DistSQL,更多算法请参考数据一致性校验算法

CHECK MIGRATION jobId BY TYPE (NAME='SphereEx:DATA_DIFF');
  1. 查看一致性校验进度

查询数据一致性校验进度 DistSQL:

SHOW MIGRATION CHECK STATUS jobId;
  1. 完成迁移任务

执行以下 DistSQL 完成迁移任务。

COMMIT MIGRATION jobId;

Proxy 切换读流量访问比例 #

当存量数据完成迁移后,需要保持 MySQL 和 Oracle 双写,并将读流量按照比例切换至 MySQL,可以基于权重算法,配置 Oracle: MySQL9: 1,即切换 10% 流量到 MySQL。 使用如下 DistSQL 更新双写规则,READ_STORAGE_UNITS 增加 mysql_ds,此时负载均衡算法会基于权重,将 10% 流量切换到 MySQL:

-- ALTER,修改双写规则(拦截全部流量)
ALTER DUAL_WRITE RULE dual_write_ds (
    PRIMARY_STORAGE_UNIT=oracle_ds,
    WRITE_STORAGE_UNITS(oracle_ds, mysql_ds),
    READ_STORAGE_UNITS(oracle_ds, mysql_ds),
    LOAD_BALANCE_ALGORITHM(TYPE(NAME="WEIGHT", properties("oracle_ds"="9", "mysql_ds"="1")))
);

-- ALTER,修改双写规则(仅拦截特定 SQL 流量)
ALTER DUAL_WRITE RULE dual_write_ds (
    PRIMARY_STORAGE_UNIT=oracle_ds,
    WRITE_STORAGE_UNITS(oracle_ds, mysql_ds),
    READ_STORAGE_UNITS(oracle_ds, mysql_ds),
    LOAD_BALANCE_ALGORITHM(TYPE(NAME="WEIGHT", properties("oracle_ds"="9", "mysql_ds"="1"))),
    INTERCEPT_ALGORITHM(TYPE(NAME="SphereEx:SQL_INTERCEPT", properties("sqls"="SELECT * FROM t_order;")))
);

执行完成后,可以使用如下 SHOW 语句查看规则是否生效。

-- SHOW,查看双写规则
SHOW DUAL_WRITE RULES;
SHOW DUAL_WRITE RULE dual_write_ds;

根据线上业务运行情况,可以逐步调大 MySQL 承载读流量的比例,直到 100% 流量由 MySQL 承载,完全切换读流量 DistSQL 如下,READ_STORAGE_UNITS 只配置 MySQL 存储单元:

-- ALTER,修改双写规则
ALTER DUAL_WRITE RULE dual_write_ds (
    PRIMARY_STORAGE_UNIT=oracle_ds,
    WRITE_STORAGE_UNITS(oracle_ds, mysql_ds),
    READ_STORAGE_UNITS(mysql_ds),
    LOAD_BALANCE_ALGORITHM(TYPE(NAME="WEIGHT", properties("oracle_ds"="9", "mysql_ds"="1")))
);

Proxy 切换读写流量至 MySQL #

当 MySQL 100% 承载读流量并且经过一段时间的业务验证后,此时可以考虑将写流量切换到 MySQL 单写模式,执行如下 DistSQL 可以完成单写切换。

-- ALTER,修改双写规则
ALTER DUAL_WRITE RULE dual_write_ds (
    PRIMARY_STORAGE_UNIT=mysql_ds,
    WRITE_STORAGE_UNITS(mysql_ds),
    READ_STORAGE_UNITS(mysql_ds)
);

FAQ #

  1. 异构双写功能是否支持数据分片功能,将原有数据库表(Oracle、SQLServer 等)替换为分布式 MySQL 数据库?

支持,异构双写功能支持同时使用分片功能,并且通过数据迁移工具能够写入到分片后的数据源中。