零停机部署和数据库

零停机部署和数据库

本文详细介绍了如何解决部署中的数据库兼容性问题。 我们将告诉您,如果您在没有预先准备的情况下尝试部署,您的生产应用程序可能会发生什么情况。 然后,我们将经历零停机所需的应用程序生命周期阶段(约。 车道:进一步 - 零停机时间)。 我们操作的结果将以向后兼容的方式应用向后不兼容的数据库更改。

如果您想了解本文中的代码示例,可以在以下位置找到它们 GitHub上.

介绍

零停机部署

多么神秘 零停机部署? 您可以说,这是当您的应用程序以这样的方式部署时,您可以成功地将应用程序的新版本引入生产,而用户不会注意到它的不可用。 从用户和公司的角度来看,这是最好的部署方案,因为它允许在不中断的情况下引入新功能并修复错误。

如何实现这一目标? 有多种方法,以下是其中一种:

  • 部署您的服务的版本 1
  • 执行数据库迁移
  • 与版本 #2 并行部署服务的版本 #1
  • 一旦您看到版本 2 可以正常工作,请删除版本 1
  • 准备好了!

很容易,不是吗? 不幸的是,事情并没有那么简单,我们稍后会详细讨论。 现在我们来看看另一个相当常见的部署流程——蓝绿部署。

你可曾听说 蓝绿部署? Cloud Foundry 让这一切变得极其简单。 看看 本文,我们在这里更详细地描述这一点。 简单总结一下,让我们提醒您如何进行蓝绿部署:

  • 确保您的生产代码的两个副本(“蓝色”和“绿色”)有效;
  • 将所有流量引导至蓝色环境,即以便生产 URL 指向那里;
  • 在绿色环境中部署和测试所有应用程序更改;
  • 将 url 从蓝色环境切换为绿色环境

蓝绿部署是一种允许您轻松引入新功能而不必担心生产中断的方法。 这是因为,即使出现问题,您也可以通过简单的“轻按开关”轻松回滚到之前的环境。

读完以上内容后,您可能会问这样的问题:零停机与蓝绿部署有什么关系?

嗯,它们有很多共同点,因为维护同一环境的两个副本需要付出双倍的努力来维护它们。 这就是为什么有些团队声称 马丁·福勒(Martin Fowler),遵循此方法的变体:

另一种选择是使用相同的数据库,为 Web 层和域层创建蓝绿交换机。 在这种方法中,数据库通常会成为问题,特别是当您需要更改其架构以支持新版本的软件时。

我们来到了本文的主要问题。 数据库。 我们再看一下这句话。

执行数据库迁移。

现在您必须问自己一个问题 - 如果数据库更改不向后兼容怎么办? 我的应用程序的第一个版本不会崩溃吗? 事实上,这正是将会发生的事情......

因此,尽管零停机/蓝绿部署具有巨大的好处,但公司仍倾向于遵循以下更安全的流程来部署其应用程序:

  • 准备一个包含新版本应用程序的包
  • 关闭正在运行的应用程序
  • 运行脚本来迁移数据库
  • 部署并启动应用程序的新版本

在本文中,我们将详细介绍如何使用数据库和代码来利用零停机部署。

数据库问题

如果您有一个不在数据库中存储任何数据的无状态应用程序,您可以立即获得零停机部署。 不幸的是,大多数软件都需要在某个地方存储数据。 这就是为什么在对电路进行任何更改之前应该三思而后行。 在详细了解如何更改架构以便实现无停机部署之前,我们首先关注版本控制架构。

版本控制方案

在本文中我们将使用 飞路 作为版本控制工具(约。 翻译:我们正在谈论数据库迁移)。 当然,我们还将编写一个具有内置 Flyway 支持的 Spring Boot 应用程序,并在设置应用程序上下文时执行模式迁移。 使用 Flyway 时,您可以将迁移脚本存储在项目文件夹中(默认情况下位于 classpath:db/migration)。 在这里您可以看到此类迁移文件的示例

└── db
 └── migration
     ├── V1__init.sql
     ├── V2__Add_surname.sql
     ├── V3__Final_migration.sql
     └── V4__Remove_lastname.sql

在此示例中,我们看到 4 个迁移脚本,如果之前未执行,则在应用程序启动时将依次执行。 让我们看一下其中一个文件(V1__init.sql) 举个例子。

CREATE TABLE PERSON (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
first_name varchar(255) not null,
last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

一切都是完全不言自明的:您可以使用 SQL 来定义应如何修改数据库。 有关 Spring Boot 和 Flyway 的更多信息,请查看 Spring引导文档.

通过将源代码控制工具与 Spring Boot 结合使用,您可以获得两大好处:

  • 将数据库更改与代码更改分开
  • 数据库迁移随着应用程序的推出而发生,即您的部署过程得到简化

排除数据库问题

在本文的下一部分中,我们将重点关注两种数据库更改方法。

  • 向后不兼容
  • 向后兼容性

第一个将被视为警告,在没有初步准备的情况下不应执行零停机部署...第二个提供了如何在不停机的情况下执行部署并同时保持向后兼容性的解决方案。

我们将要开发的项目将是一个简单的 Spring Boot Flyway 应用程序,它具有 Person с first_name и last_name 在数据库中(约。 翻译: Person 是一个表和 first_name и last_name - 这些是其中的字段)。 我们想重命名 last_name в surname.

假设

在我们深入了解细节之前,我们需要对我们的应用程序做出一些假设。 我们想要实现的主要结果将是一个相当简单的过程。

笔记。 商务专业提示。 简化流程可以为您节省大量支持费用(为您的公司工作的人员越多,您可以节省的钱就越多)!

无需回滚数据库

这简化了部署过程(有些数据库回滚几乎是不可能的,比如删除回滚)。 我们更愿意仅回滚应用程序。 这样,即使您有不同的数据库(例如 SQL 和 NoSQL),您的部署管道也会看起来相同。

必须始终能够将应用程序回滚到一个版本(不能再回滚)

仅应在必要时进行回滚。 如果当前版本中存在不易修复的错误,我们应该能够恢复到最新的工作版本。 我们假设这个最新的工作版本是前一个版本。 维护多次部署的代码和数据库兼容性将是极其困难和昂贵的。

注意。 为了提高可读性,在本文中我们将更改应用程序的主要版本。

第 1 步:初始状态

应用版本: 1.0.0
数据库版本: v1

备注

这将是应用程序的初始状态。

数据库变更

数据库包含 last_name.

CREATE TABLE PERSON (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
first_name varchar(255) not null,
last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

代码变更

该应用程序将人员数据存储在 last_name:

/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package sample.flyway;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String lastName;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return this.lastName;
    }

    public void setLastName(String lastname) {
        this.lastName = lastname;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + this.firstName + ", lastName=" + this.lastName
                + "]";
    }
}

向后不兼容的列重命名

让我们看一个如何更改列名称的示例:

警告。 下面的例子会故意破坏一些东西。 我们展示这个是为了演示数据库兼容性问题。

应用版本: 2.0.0.BAD

数据库版本: v2bad

备注

当前的更改不允许我们同时运行两个实例(旧的和新的)。 因此,零停机部署将很难实现(如果考虑到假设,实际上是不可能的)。

A/B 测试

目前的情况是我们有一个应用程序版本 1.0.0, 部署在生产和数据库中 v1。 我们需要部署应用程序的第二个实例,版本 2.0.0.BAD,并将数据库更新为 v2bad.

步骤:

  1. 部署版本应用程序的新实例 2.0.0.BAD它将数据库更新为 v2bad
  2. 在数据库中 v2badlast_name 不再存在 - 它已更改为 surname
  3. 数据库和应用程序更新成功,部分实例正在运行 1.0.0, 其他人 - 在 2.0.0.BAD。 一切都连接到数据库 v2bad
  4. 该版本的所有实例 1.0.0 将开始抛出错误,因为他们将尝试将数据插入到列中 last_name谁不再存在
  5. 该版本的所有实例 2.0.0.BAD 可以正常工作

正如您所看到的,如果我们对数据库和应用程序进行向后不兼容的更改,A/B 测试是不可能的。

应用回滚

我们假设在尝试进行 A/B 部署之后(约。 per.:作者在这里可能指的是 A/B 测试)我们决定需要将应用程序回滚到该版本 1.0.0. 假设我们不想回滚数据库。

步骤:

  1. 我们停止版本应用程序实例 2.0.0.BAD
  2. 数据库仍然是 v2bad
  3. 自版本以来 1.0.0 不明白那是什么 surname,我们会看到错误
  4. 地狱已经崩溃,我们再也回不去了

正如您所看到的,如果我们对数据库和应用程序进行向后不兼容的更改,我们将无法回滚到以前的版本。

脚本执行日志

Backward incompatible scenario:

01) Run 1.0.0
02) Wait for the app (1.0.0) to boot
03) Generate a person by calling POST localhost:9991/person to version 1.0.0
04) Run 2.0.0.BAD
05) Wait for the app (2.0.0.BAD) to boot
06) Generate a person by calling POST localhost:9991/person to version 1.0.0 <-- this should fail
07) Generate a person by calling POST localhost:9992/person to version 2.0.0.BAD <-- this should pass

Starting app in version 1.0.0
Generate a person in version 1.0.0
Sending a post to 127.0.0.1:9991/person. This is the response:

{"firstName":"b73f639f-e176-4463-bf26-1135aace2f57","lastName":"b73f639f-e176-4463-bf26-1135aace2f57"}

Starting app in version 2.0.0.BAD
Generate a person in version 1.0.0
Sending a post to 127.0.0.1:9991/person. This is the response:

curl: (22) The requested URL returned error: 500 Internal Server Error

Generate a person in version 2.0.0.BAD
Sending a post to 127.0.0.1:9995/person. This is the response:

{"firstName":"e156be2e-06b6-4730-9c43-6e14cfcda125","surname":"e156be2e-06b6-4730-9c43-6e14cfcda125"}

数据库变更

重命名的迁移脚本 last_name в surname

源Flyway脚本:

CREATE TABLE PERSON (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
first_name varchar(255) not null,
last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

重命名的脚本 last_name.

-- This change is backward incompatible - you can't do A/B testing
ALTER TABLE PERSON CHANGE last_name surname VARCHAR;

代码变更

我们更改了字段名称 lastNamesurname.

以向后兼容的方式重命名列

这是我们可能遇到的最常见的情况。 我们需要进行向后不兼容的更改。 我们已经证明,对于零停机部署,我们不应该简单地应用数据库迁移而不需要额外的步骤。 在本文的这一部分中,我们将执行 3 次应用程序部署以及数据库迁移,以实现所需的结果,同时保持向后兼容性。

笔记。 回想一下,我们有一个版本数据库 v1。 它包含列 first_name и last_name。 我们必须改变 last_namesurname。 我们还有应用程序版本 1.0.0, 尚未使用 surname.

第 2 步:添加姓氏

应用版本: 2.0.0
数据库版本: v2

备注

通过添加新列并复制其内容,我们创建了向后兼容的数据库更改。 同时,如果我们回滚 JAR 或运行旧的 JAR,它在执行过程中也不会中断。

我们正在推出新版本

步骤:

  1. 执行数据库迁移以创建新列 surname。 现在你的数据库版本 v2
  2. 从复制数据 last_name в surname. 注意如果你有很多这样的数据,你应该考虑批量迁移!
  3. 在使用它们的地方编写代码 BOTH и 柱子。 现在您的应用程序版本 2.0.0
  4. 从列中读取值 surname,如果不是 null,或从 last_name如果 surname 未指定。 您可以删除 getLastName() 从代码中,因为它会输出 null 当回滚你的应用程序时 3.0.02.0.0.

如果您使用的是Spring Boot Flyway,这两个步骤将在版本启动时执行 2.0.0 应用程序。 如果手动运行数据库版本控制工具,则必须执行两项不同的操作(首先手动更新数据库版本,然后部署新应用程序)。

这一点很重要。 请记住,新创建的列 不应该不是NULL。 如果您进行回滚,旧应用程序不知道新列,并且不会在安装期间安装它 Insert. 但是如果你添加这个约束,你的数据库将是 v2,这将需要设置新列的值。 这将导致违反限制。

这一点很重要。 你应该删除该方法 getLastName(),因为在版本中 3.0.0 代码中没有列的概念 last_name。 这意味着将在那里设置 null。 您可以保留该方法并添加检查 null,但更好的解决方案是确保在逻辑中 getSurname() 您选择了正确的非零值。

A/B 测试

目前的情况是我们有一个应用程序版本 1.0.0,部署在生产环境中,数据库位于 v1。 我们需要部署版本应用程序的第二个实例 2.0.0这会将数据库更新为 v2.

步骤:

  1. 部署版本应用程序的新实例 2.0.0它将数据库更新为 v2
  2. 与此同时,版本实例处理了一些请求 1.0.0
  3. 更新成功并且您有多个正在运行的版本应用程序实例 1.0.0 和其他版本 2.0.0. 每个人都与数据库进行通信 v2
  4. 版本 1.0.0 不使用数据库中的姓氏列,而是使用版本 2.0.0 用途。 它们互不干扰,也不应该出现错误。
  5. 版本 2.0.0 将数据存储在新旧列中,确保向后兼容性

这一点很重要。 如果您有任何根据旧/新列中的值对项目进行计数的查询,您应该记住您现在有重复的值(很可能它们仍在迁移)。 例如,如果您想要计算姓氏(无论该列被称为什么)以字母开头的用户数量 A,然后直到数据迁移完成(oldnew 列),如果查询新列,可能会出现不一致的数据。

应用回滚

现在我们有应用程序版本 2.0.0 和数据库在 v2.

步骤:

  1. 将您的应用程序回滚到版本 1.0.0.
  2. 版本 1.0.0 不使用数据库中的列 surname,所以回滚应该会成功

数据库更改

数据库包含一个名为 last_name.

Flyway源脚本:

CREATE TABLE PERSON (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
first_name varchar(255) not null,
last_name varchar(255) not null
);

insert into PERSON (first_name, last_name) values ('Dave', 'Syer');

添加脚本 surname.

警告。 请记住,您不能向要添加的列添加任何 NOT NULL 约束。 如果回滚 JAR,旧版本将不知道添加的列,并会自动将其设置为 NULL。 如果存在这样的限制,旧的应用程序就会崩溃。

-- NOTE: This field can't have the NOT NULL constraint cause if you rollback, the old version won't know about this field
-- and will always set it to NULL
ALTER TABLE PERSON ADD surname varchar(255);

-- WE'RE ASSUMING THAT IT'S A FAST MIGRATION - OTHERWISE WE WOULD HAVE TO MIGRATE IN BATCHES
UPDATE PERSON SET PERSON.surname = PERSON.last_name

代码变更

我们将数据存储为 last_name和在 surname。 同时我们读到 last_name,因为此列是最相关的。 在部署过程中,某些请求可能已由尚未更新的应用程序实例处理。

/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package sample.flyway;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String lastName;
    private String surname;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    /**
     * Reading from the new column if it's set. If not the from the old one.
     *
     * When migrating from version 1.0.0 -> 2.0.0 this can lead to a possibility that some data in
     * the surname column is not up to date (during the migration process lastName could have been updated).
     * In this case one can run yet another migration script after all applications have been deployed in the
     * new version to ensure that the surname field is updated.
     *
     * However it makes sense since when looking at the migration from 2.0.0 -> 3.0.0. In 3.0.0 we no longer
     * have a notion of lastName at all - so we don't update that column. If we rollback from 3.0.0 -> 2.0.0 if we
     * would be reading from lastName, then we would have very old data (since not a single datum was inserted
     * to lastName in version 3.0.0).
     */
    public String getSurname() {
        return this.surname != null ? this.surname : this.lastName;
    }

    /**
     * Storing both FIRST_NAME and SURNAME entries
     */
    public void setSurname(String surname) {
        this.lastName = surname;
        this.surname = surname;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + this.firstName + ", lastName=" + this.lastName + ", surname=" + this.surname
                + "]";
    }
}

步骤 3:从代码中删除 last_name

应用版本: 3.0.0

数据库版本:v3

备注

笔记per.:显然,在原始文章中,作者错误地复制了步骤 2 中该块的文本。在此步骤中,应在应用程序代码中进行更改,旨在删除使用该列的功能 last_name.

通过添加新列并复制其内容,我们创建了向后兼容的数据库更改。 另外,如果我们回滚 JAR 或运行旧的 JAR,它在执行过程中不会中断。

应用回滚

目前我们有应用程序版本 3.0.0 和数据库 v3. 版本 3.0.0 不保存数据到 last_name。 这意味着在 surname 存储最新信息。

步骤:

  1. 将您的应用程序回滚到版本 2.0.0.
  2. 版本 2.0.0 用途和 last_name и surname.
  3. 版本 2.0.0 会采取 surname,如果不为零,否则 -last_name

数据库变更

数据库中没有结构变化。 执行以下脚本来进行旧数据的最终迁移:

-- WE'RE ASSUMING THAT IT'S A FAST MIGRATION - OTHERWISE WE WOULD HAVE TO MIGRATE IN BATCHES
-- ALSO WE'RE NOT CHECKING IF WE'RE NOT OVERRIDING EXISTING ENTRIES. WE WOULD HAVE TO COMPARE
-- ENTRY VERSIONS TO ENSURE THAT IF THERE IS ALREADY AN ENTRY WITH A HIGHER VERSION NUMBER
-- WE WILL NOT OVERRIDE IT.
UPDATE PERSON SET PERSON.surname = PERSON.last_name;

-- DROPPING THE NOT NULL CONSTRAINT; OTHERWISE YOU WILL TRY TO INSERT NULL VALUE OF THE LAST_NAME
-- WITH A NOT_NULL CONSTRAINT.
ALTER TABLE PERSON MODIFY COLUMN last_name varchar(255) NULL DEFAULT NULL;

代码变更

笔记per.:该块的描述也是作者从步骤2中错误复制的。按照文章的逻辑,这一步代码的更改应该旨在从中删除与该列一起使用的元素 last_name.

我们将数据存储为 last_name和在 surname. 此外,我们从专栏中读到 last_name,因为它是最相关的。 在部署过程中,某些请求可能由尚未升级的实例处理。

/*
 * Copyright 2012-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package sample.flyway;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Person {
    @Id
    @GeneratedValue
    private Long id;
    private String firstName;
    private String surname;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getSurname() {
        return this.surname;
    }

    public void setSurname(String lastname) {
        this.surname = lastname;
    }

    @Override
    public String toString() {
        return "Person [firstName=" + this.firstName + ", surname=" + this.surname
                + "]";
    }
}

步骤 4:从数据库中删除姓氏

应用版本: 4.0.0

数据库版本: v4

备注

由于版本代码 3.0.0 没有使用该列 last_name,如果我们回滚到执行期间不会发生任何不好的事情 3.0.0 从数据库中删除列后。

脚本执行日志

We will do it in the following way:

01) Run 1.0.0
02) Wait for the app (1.0.0) to boot
03) Generate a person by calling POST localhost:9991/person to version 1.0.0
04) Run 2.0.0
05) Wait for the app (2.0.0) to boot
06) Generate a person by calling POST localhost:9991/person to version 1.0.0
07) Generate a person by calling POST localhost:9992/person to version 2.0.0
08) Kill app (1.0.0)
09) Run 3.0.0
10) Wait for the app (3.0.0) to boot
11) Generate a person by calling POST localhost:9992/person to version 2.0.0
12) Generate a person by calling POST localhost:9993/person to version 3.0.0
13) Kill app (3.0.0)
14) Run 4.0.0
15) Wait for the app (4.0.0) to boot
16) Generate a person by calling POST localhost:9993/person to version 3.0.0
17) Generate a person by calling POST localhost:9994/person to version 4.0.0

Starting app in version 1.0.0
Generate a person in version 1.0.0
Sending a post to 127.0.0.1:9991/person. This is the response:

{"firstName":"52b6e125-4a5c-429b-a47a-ef18bbc639d2","lastName":"52b6e125-4a5c-429b-a47a-ef18bbc639d2"}

Starting app in version 2.0.0

Generate a person in version 1.0.0
Sending a post to 127.0.0.1:9991/person. This is the response:

{"firstName":"e41ee756-4fa7-4737-b832-e28827a00deb","lastName":"e41ee756-4fa7-4737-b832-e28827a00deb"}

Generate a person in version 2.0.0
Sending a post to 127.0.0.1:9992/person. This is the response:

{"firstName":"0c1240f5-649a-4bc5-8aa9-cff855f3927f","lastName":"0c1240f5-649a-4bc5-8aa9-cff855f3927f","surname":"0c1240f5-649a-4bc5-8aa9-cff855f3927f"}

Killing app 1.0.0

Starting app in version 3.0.0

Generate a person in version 2.0.0
Sending a post to 127.0.0.1:9992/person. This is the response:
{"firstName":"74d84a9e-5f44-43b8-907c-148c6d26a71b","lastName":"74d84a9e-5f44-43b8-907c-148c6d26a71b","surname":"74d84a9e-5f44-43b8-907c-148c6d26a71b"}

Generate a person in version 3.0.0
Sending a post to 127.0.0.1:9993/person. This is the response:
{"firstName":"c6564dbe-9ab5-40ae-9077-8ae6668d5862","surname":"c6564dbe-9ab5-40ae-9077-8ae6668d5862"}

Killing app 2.0.0

Starting app in version 4.0.0

Generate a person in version 3.0.0
Sending a post to 127.0.0.1:9993/person. This is the response:

{"firstName":"cbe942fc-832e-45e9-a838-0fae25c10a51","surname":"cbe942fc-832e-45e9-a838-0fae25c10a51"}

Generate a person in version 4.0.0
Sending a post to 127.0.0.1:9994/person. This is the response:

{"firstName":"ff6857ce-9c41-413a-863e-358e2719bf88","surname":"ff6857ce-9c41-413a-863e-358e2719bf88"}

数据库更改

关于 v3 我们只是删除该列 last_name 并添加缺少的限制。

-- REMOVE THE COLUMN
ALTER TABLE PERSON DROP last_name;

-- ADD CONSTRAINTS
UPDATE PERSON SET surname='' WHERE surname IS NULL;
ALTER TABLE PERSON ALTER COLUMN surname VARCHAR NOT NULL;

代码变更

代码没有任何变化。

结论

我们通过执行多个向后兼容的部署成功应用了向后不兼容的列名称更改。 以下是所执行操作的摘要:

  1. 应用程序版本部署 1.0.0 с v1 数据库架构(列名称 = last_name)
  2. 应用程序版本部署 2.0.0, 它存储数据在 last_name и surname。 应用程序读取自 last_name。 数据库处于版本状态 v2包含像这样的列 last_namesurname. surname 是 l 的副本ast_name。 (注意:该列不能有非空约束)
  3. 应用程序版本部署 3.0.0,它只存储数据 surname 并从姓氏中读取。 至于数据库,正在进行最后一次迁移 last_name в surname。 也是一个限制 不是NULL 已经从...删除 last_name。 数据库现在已经是版本了 v3
  4. 应用程序版本部署 4.0.0 - 未对代码进行任何更改。 数据库部署 v4,这会删除 last_name。 您可以在此处向数据库添加任何缺少的约束。

通过采用这种方法,您始终可以回滚一个版本,而不会破坏数据库/应用程序兼容性。

代码

本文中使用的所有代码均可在以下位置获取 Github上。 下面是附加说明。

项目

克隆存储库后,您将看到以下文件夹结构。

├── boot-flyway-v1              - 1.0.0 version of the app with v1 of the schema
├── boot-flyway-v2              - 2.0.0 version of the app with v2 of the schema (backward-compatible - app can be rolled back)
├── boot-flyway-v2-bad          - 2.0.0.BAD version of the app with v2bad of the schema (backward-incompatible - app cannot be rolled back)
├── boot-flyway-v3              - 3.0.0 version of the app with v3 of the schema (app can be rolled back)
└── boot-flyway-v4              - 4.0.0 version of the app with v4 of the schema (app can be rolled back)

脚本

您可以运行下面脚本中描述的脚本,这将演示对数据库的向后兼容和不兼容的更改。

查看 具有向后兼容更改的情况, 跑步:

./scripts/scenario_backward_compatible.sh

并看到 具有向后不兼容更改的情况, 跑步:

./scripts/scenario_backward_incompatible.sh

Spring Boot 示例 Flyway

所有示例均取自 Spring Boot Sample Flyway.

你可以看一下 http://localhost:8080/flyway,有一个脚本列表。

此示例还包括 H2 控制台(位于 http://localhost:8080/h2-console)这样你就可以查看数据库状态(默认jdbc URL是 jdbc:h2:mem:testdb).

另外

另请阅读我们博客上的其他文章:

来源: habr.com

添加评论