数据库代码化(Database-as-Code)实战

本文涉及的产品
对象存储 OSS,20GB 3个月
文件存储 NAS,50GB 3个月
日志服务 SLS,月写入数据量 50GB 1个月
简介: 最近在做专有云输出时遇到了一个棘手的问题,客户需要将我们两年前发布的版本升级到最新版。由于跨度较长,产品代码和数据库 schema 都发生了巨大变化。产品代码部分因为采用了版本管理策略,拥有明确的升级路径,但数据库部分由于未采用代码化方案,导致升级路径缺失,整个升级过程非常艰难。

前言

最近在做专有云输出时遇到了一个棘手的问题,客户需要将我们两年前发布的版本升级到最新版。由于跨度较长,产品代码和数据库 schema 都发生了巨大变化。产品代码部分因为采用了版本管理策略,拥有明确的升级路径,但数据库部分由于未采用代码化方案,导致升级路径缺失,整个升级过程非常艰难。为了让以后的版本升级能顺利进行,需要制定出一套统一的数据库代码化方案。

What we needed to do was to change our mindset of how we treated our database. We had to stop treating it like some special artifact or some unique scenario, and we started looking at it through the same perspective that we were treating our web code.

State based VS Migrations based

State based 和 Migrations based 是来实现数据库代码化的两种常用方法,下面分别进行介绍。

State based

state_based

在 state based 模式下,我们仅需要维护数据库的目标状态。每个表、存储过程、视图、触发器都将保存为单独的 SQL 文件,这些文件将是数据库对象状态的真实表示。而升级数据库所需的脚本会由工具自动生成,从而大大减轻维护成本。

但这种模式并不能很好地处理数据迁移场景,例如,将 user 表的 name 列拆分成 first name 和 last name 两个字段。这是因为数据表里的数据往往是上下文相关的,这意味着工具无法对数据进行可靠的假设以生成升级脚本。

Migrations based

migrations_based

在 migrations based 模式下,我们需要自行维护数据库从一个版本到另一个版本的变更脚本。相比 state based,该模式增加了维护的成本和复杂性,但它能让我们更直接地控制迁移过程,从而能够处理诸如数据迁移这样上下文相关的场景。并且由于变更通过命令式的方式描述,我们可以更早地对其进行评审。实现了 migration based 的代表性工具有 LiquibaseFlyway 等。

Flyway 简介

本章将介绍 migrations based 模式下的代表性工具之一 Flyway,文中使用的版本为6.0.8

Flyway 是什么

Flyway 是一款开源的数据库迁移工具,它可以方便地帮我们完成数据库的全新部署和增量升级。它有如下功能特点:

  1. 可以嵌入到应用程序里或作为单独的工具执行。
  2. 追踪已执行的迁移。
  3. 执行新的迁移。
  4. 验证数据库状态。

Flyway 原理

sls_branch

Flyway 的工作原理如下:

  1. 初始时,在数据库中创建一个名为flyway_schema_history的元数据表,该表用于记录 migration 的执行情况。
  2. 扫描用户指定目录下的 migration 脚本,并将它们按版本号由低到高排序。
  3. 将 migration 脚本依次应用到数据库上。同时,元数据表也会相应更新。
  4. 下次增量升级数据库时,flyway 会根据元数据表中记录的执行情况,找出本次新增的迁移脚本并依次执行。

数据库代码化方案

由于我们不仅需要处理 schema 的变更,还面临一些数据迁移的场景,所以最终选择了 migrations based 模式,并使用 Flyway 帮助我们实现数据库代码化。

迁移脚本组织结构

为了兼顾公有云和专有云,同时考虑老版专有云的升级场景,我们设计了如下目录结构用于管理 SQL 迁移脚本。

|--{db1}
     |--flyway.conf
     |--base_sql
          |--V0.000__a.sql
          |--V0.001__b.sql
          |...
          `--V0.025__z.sql
     |--upgrade_legacy_private_cloud_sql
          |--V2.000__create_TB_t1.sql
          |--V2.001__alter_TB_a.sql
          `--V2.002__TB_b_update.sql
     |--upgrade_sql
          |--V2019.11.11.000__alter_TB_b_add_column.sql
          |--V2019.11.11.001__TB_c_insert.properties
          |--V2019.11.11.001__TB_c_insert.sql
          `--V2019.11.13.000__mix.sql
 |--{db2}
      |--flyway.conf
      ...
 |--common
      `--procedure.sql

下面对其进行说明:
1.每个数据库对应一个独立的目录,包含了该数据库的迁移脚本和配置信息。
2.数据库目录下的文件 flyway.conf 包含了该数据库的连接、认证、baseline 等信息。
3.子目录 base_sql 用于存放存量 schema,版本号格式为0.xxx。该目录中的内容确定后将不允许变更。
4.子目录 upgrade_legacy_private_cloud_sql 用于存放专有云老版本到新版本的迁移脚本,版本号格式为2.xxx
5.子目录 upgrade_sql 统一存放公有云和专有云的后续迁移脚本。由于公有云并未编制版本,且专有云不同版本的编号方式也存在差异,这里选用日期作为迁移脚本的版本前缀,格式为yyyy.mm.dd.index
6.不同环境的 DML 可能存在差异。针对这种场景,需要在迁移脚本中将差异部分用占位符表示,占位符的实际值由同名 properties 文件渲染得到,例如V{yyyy.mm.dd.index}__xxx.properties。同名 properties 文件存放在基线里,不同环境可设置不同的值,运行时会将对应环境里的文件拷贝至 upgrade_sql 目录。
7.文件common/procedure.sql包含了变更 index、column、key 的存储过程,这些存储过程实现了变更的幂等性。

迁移脚本粒度

一般情况下,建议一个脚本只包含针对一个对象的一类操作。例如,Vxxx_TB_car_add_column.sql代表为数据表car增加列,Vyyy_TB_car_insert.sql代表向数据表car中插入数据。这种模式符合单一职责的设计思想,可以大大减少合并冲突的数量和复杂性。

执行流程

基于上述 SQL 迁移脚本的管理模式,公有云和专有云不同场景的执行流程如下表所示:

环境 场景 执行流程
公有云 全新部署 1. 执行 base_sql 中的 migration 脚本。
2. 执行 upgrade_sql 中的 migration 脚本。
公有云 新版升级 1. 获取当前版本 x。
2. 依次执行 upgrade_sql 中版本号大于 x 的 migration 脚本。
公有云 已有数据库升级 1. 手动将已有数据库和 base_sql 对齐。
2. 将 baseline 设为 2000。
3. 依次执行 upgrade_sql 中版本号大于 2000 的 migration 脚本。
专有云 全新部署 1. 执行 base_sql 中的 migration 脚本。
2. 执行 upgrade_sql 中的 migration 脚本。
专有云 新版升级 1. 获取当前版本 x。
2. 依次执行 upgrade_sql 中版本号大于 x 的 migration 脚本。
专有云 已有数据库升级 1. 整理专有云老版到新版的 migration 脚本,并放于 upgrade_legacy_private_cloud_sql 目录下。
2. 将 baseline 设为 1。
3. 依次执行 upgrade_legacy_private_cloud_sql 和 upgrade_sql 中版本号大于 1 的 migration 脚本。

幂等性实践

理想情况下,每个迁移脚本只会在每个数据库上运行一次。但如果某次迁移执行失败,很可能需要重新执行成功迁移的步骤才能使数据库恢复到理想状态。这时以等幂方式编写迁移脚本将非常有帮助。这里我们总结了不同 DDL 和 DML 幂等性实现的最佳实践。

SQL Type Object Action SQL script naming convention Best Practices
DDL Table Create Table V{yyyy.mm.dd.index}__create_TB_{table_name}.sql

Example:
V2019.11.08.000__create_TB_car.sql
CREATE TABLE IF NOT EXISTS {table_name};
DDL Table Drop Table V{yyyy.mm.dd.index}__drop_TB_{table_name}.sql

Example:
V2019.11.08.000__drop_TB_car.sql
DROP TABLE IF EXISTS {table_name};
DDL Column Add Column V{yyyy.mm.dd.index}__alter_TB_{table_name}_add_column.sql

Example:
V2019.11.08.000__alter_TB_car_add_column.sql
目标 column 不存在,则进行操作。(通过存储过程封装)
DDL Column Drop Column V{yyyy.mm.dd.index}__alter_TB_{table_name}_drop_column.sql

Example:
V2019.11.08.000__alter_TB_car_drop_column.sql
目标 column 存在,则进行操作。(通过存储过程封装)
DDL Column Change Column V{yyyy.mm.dd.index}__alter_TB_{table_name}_change_column.sql

Example:
V2019.11.08.000__alter_TB_car_change_column.sql
目标 column 存在,则进行操作。(通过存储过程封装)
DDL Keys and Indexes Add V{yyyy.mm.dd.index}__alter_TB_{table_name}_add_index.sql

Example:
V2019.11.08.000__alter_TB_car_add_index.sql
目标 key 或 index 不存在,则进行操作。(通过存储过程封装)
DDL Keys and Indexes Drop V{yyyy.mm.dd.index}__alter_TB_{table_name}_drop_index.sql

Example:
V2019.11.08.000__alter_TB_car_drop_index.sql
目标 key 或 index 存在,则进行操作。(通过存储过程封装)
DML Row Insert V{yyyy.mm.dd.index}__TB_{table_name}_insert.sql

Example:
V2019.11.08.001__TB_car_insert.sql
  • 带唯一索引 Insert - 具备幂等性,重复插入会失败,推荐使用。如果希望重复时不抛异常,可以使用insert ignore intoreplaceon duplicate key update等命令。
  • 不带唯一索引 Insert - 不具备幂等性。在数据量不大的情况下,可考虑改写成如下 SQL 语句INSERT INTO table(field1, field2, fieldn) SELECT 'field1', 'field2', 'fieldn' FROM DUAL WHERE NOT EXISTS(SELECT field FROM table WHERE field = ?)
DML Row Update V{yyyy.mm.dd.index}__TB_{table_name}_update.sql

Example:
V2019.11.08.001__TB_car_update.sql
  • 计算式 Update - 不具备幂等性,尽量避免使用,例如UPDATE table SET number=number-1 WHERE id=1
  • 非计算式 Update - 天然具备幂等性,推荐使用,例如UPDATE table SET number=3 WHERE id=1
DML Row Delete V{yyyy.mm.dd.index}__TB_{table_name}_delete.sql

Example:
V2019.11.08.001__TB_car_delete.sql
天然具备幂等性。
DML Row Multiple tables
Multiple actions
V{yyyy.mm.dd.index}__mix.sql

Example:
V2019.11.08.001__mix.sql
如果针对下列情况有事务性要求,可以将 SQL 放到一个 mix 脚本里。
  • 单表多类 DML
  • 多表单类 DML
  • 多表多类 DML

以修改表字段为例,这里将修改过程封装成了如下的存储过程。

DELIMITER $$
CREATE PROCEDURE `SAFE_CHANGE_COLUMN`(IN i_table_name VARCHAR(128),IN i_col_name VARCHAR(128), IN i_col_def VARCHAR(256))
BEGIN
    SET @tableName = i_table_name;
    SET @colName = i_col_name;
    SET @colDef = i_col_def;
    SET @colExists = 0;
    SELECT 1 INTO @colExists FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = @tableName AND COLUMN_NAME = @colName LIMIT 1;
    IF @colExists THEN
        SET @query = CONCAT('ALTER TABLE ',@tableName,' CHANGE COLUMN ', @colName,' ',@colDef);
        PREPARE stmt FROM @query;
        EXECUTE stmt;
        DEALLOCATE PREPARE stmt;
    END IF;
END$$

CICD 流水线

cicd

这里我们将数据库迁移脚本和应用代码存放在相同的代码仓库里,并共享同一个 CICD 流程。这种模式最符合 DevOps 所倡导的协作、试验、快速反馈、持续改进等思想,能实现产品更快、更频繁、更稳定的交付。

总结

采用数据库代码化方案后,数据库的维护升级工作变得轻松简单。

  1. 不论多长跨度的升级都能从容应对,因为任意版本之间都有了明确的升级路径。
  2. 数据库 schema 变更、数据迁移行为变得可审计。
  3. 数据库迁移、应用代码共享 CICD 流水线,提升产品交付发布速度。

参考资料

扩展阅读

相关文章
|
2月前
|
存储 关系型数据库 数据库
附部署代码|云数据库RDS 全托管 Supabase服务:小白轻松搞定开发AI应用
本文通过一个 Agentic RAG 应用的完整构建流程,展示了如何借助 RDS Supabase 快速搭建具备知识处理与智能决策能力的 AI 应用,展示从数据准备到应用部署的全流程,相较于传统开发模式效率大幅提升。
附部署代码|云数据库RDS 全托管 Supabase服务:小白轻松搞定开发AI应用
|
3月前
|
人工智能 安全 机器人
无代码革命:10分钟打造企业专属数据库查询AI机器人
随着数字化转型加速,企业对高效智能交互解决方案的需求日益增长。阿里云AppFlow推出的AI助手产品,借助创新网页集成技术,助力企业打造专业数据库查询助手。本文详细介绍通过三步流程将AI助手转化为数据库交互工具的核心优势与操作指南,包括全场景适配、智能渲染引擎及零代码配置等三大技术突破。同时提供Web集成与企业微信集成方案,帮助企业实现便捷部署与安全管理,提升内外部用户体验。
415 12
无代码革命:10分钟打造企业专属数据库查询AI机器人
|
3月前
|
安全 druid Nacos
0 代码改造实现应用运行时数据库密码无损轮转
本文探讨了敏感数据的安全风险及降低账密泄漏风险的策略。国家颁布的《网络安全二级等保2.0标准》强调了企业数据安全的重要性。文章介绍了Nacos作为配置中心在提升数据库访问安全性方面的应用,并结合阿里云KMS、Druid连接池和Spring Cloud Alibaba社区推出的数据源动态轮转方案。该方案实现了加密配置统一托管、帐密全托管、双层权限管控等功能,将帐密切换时间从数小时优化到一秒,显著提升了安全性和效率。未来,MSE Nacos和KMS将扩展至更多组件如NoSQL、MQ等,提供一站式安全服务,助力AI时代的应用安全。
261 15
|
3月前
|
SQL 数据建模 关系型数据库
别光知道存数据库了,数据建模才是王道!(入门指南+实战代码)
别光知道存数据库了,数据建模才是王道!(入门指南+实战代码)
328 4
|
4月前
|
负载均衡 算法 关系型数据库
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
本文聚焦 MySQL 集群架构中的负载均衡算法,阐述其重要性。详细介绍轮询、加权轮询、最少连接、加权最少连接、随机、源地址哈希等常用算法,分析各自优缺点及适用场景。并提供 Java 语言代码实现示例,助力直观理解。文章结构清晰,语言通俗易懂,对理解和应用负载均衡算法具有实用价值和参考价值。
大数据大厂之MySQL数据库课程设计:揭秘MySQL集群架构负载均衡核心算法:从理论到Java代码实战,让你的数据库性能飙升!
|
6月前
|
人工智能 前端开发 JavaScript
代码采纳率从 22% 到 33%,通义灵码辅助数据库智能编码实践
通义灵码本质上是一个AI agent,它已经进行了大量的优化。然而,为了更完美或有效地调用模型的潜在能力,我们在使用时仍需掌握一些技巧。通常,大多数人在使用通义灵码时会直接上手,这是 AI agent 的一个优势,即 zero shot 使用,无需任何上下文即可直接使用通义灵码的能力。
|
2月前
|
安全 Java Nacos
0代码改动实现Spring应用数据库帐密自动轮转
Nacos作为国内被广泛使用的配置中心,已经成为应用侧的基础设施产品,近年来安全问题被更多关注,这是中国国内软件行业逐渐迈向成熟的标志,也是必经之路,Nacos提供配置加密存储-运行时轮转的核心安全能力,将在应用安全领域承担更多职责。
|
6月前
|
存储 运维 监控
百万指标,秒级查询,零宕机——时序数据库 TDengine 在 AIOps 中的硬核实战
本篇文章详细讲述了七云团队在运维平台中如何利用 TDengine 解决海量时序数据存储与查询的实际业务需求。内容涵盖了从数据库选型、方案落地到业务挑战及解决办法的完整过程,特别是分享了升级 TDengine 3.x 时的实战经验,给到有需要的小伙伴参考阅读。
199 1
|
7月前
|
关系型数据库 数据库 数据安全/隐私保护
云数据库实战:基于阿里云RDS的Python应用开发与优化
在互联网时代,数据驱动的应用已成为企业竞争力的核心。阿里云RDS为开发者提供稳定高效的数据库托管服务,支持多种数据库引擎,具备自动化管理、高可用性和弹性扩展等优势。本文通过Python应用案例,从零开始搭建基于阿里云RDS的数据库应用,详细演示连接、CRUD操作及性能优化与安全管理实践,帮助读者快速上手并提升应用性能。