零停机部署与数据库

工程 | Marcin Grzejszczak | 2016年5月31日 | ...

本文将深入探讨如何解决数据库兼容性和部署过程中的相关问题。我们将展示如果您在准备不足的情况下尝试进行此类部署,生产应用程序可能会发生什么。然后,我们将逐步介绍实现零停机所需的应用程序生命周期中的各个步骤。我们操作的结果将是以向后兼容的方式应用向后不兼容的数据库更改。

如果您想学习下面的代码示例,可以在 GitHub 上找到所需的一切。

简介

零停机部署

什么是这种神秘的零停机部署?如果您能成功地将应用程序的新版本引入生产环境,而用户在此期间没有发现应用程序停机,那么您可以说您的应用程序是以这种方式部署的。从用户和公司的角度来看,这是最佳的部署方案,因为可以在没有任何中断的情况下引入新功能和消除错误。

如何实现呢?有很多方法,其中一种是:

  • 部署服务版本 1

  • 将数据库迁移到新版本

  • 与版本 1 并行部署服务版本 2

  • 一旦您发现版本 2 运行良好,就关闭版本 1

  • 完成!

很容易,对吧?不幸的是,事情并非如此简单,我们稍后会重点讨论。现在,让我们看看另一种常见的部署流程:蓝绿部署。

您听说过蓝绿部署吗?使用 Cloud Foundry 可以非常轻松地实现这一点。只需查看这篇文章,我们在其中进行了更深入的描述。快速回顾一下,进行蓝绿部署就像这样简单:

  • 维护两个生产环境副本(“蓝色”和“绿色”);

  • 通过将生产 URL 映射到蓝色环境来路由所有流量;

  • 在绿色环境中部署和测试应用程序的任何更改;

  • 通过将 URL 映射到绿色环境并将其从蓝色环境解除映射来“切换”。

蓝绿部署是一种方法,它可以让您轻松地引入新功能,而无需担心生产环境会完全崩溃。这是因为即使出现这种情况,您也可以通过“切换”轻松地将路由器回滚到指向先前的环境。

阅读完以上所有内容后,您可能会问自己一个问题:零停机部署与蓝绿部署有什么关系?

嗯,它们有很多共同之处,因为维护两个相同的环境副本会导致所需的维护工作翻倍。这就是为什么一些团队,正如 Martin Fowler 所说,倾向于采用这种方法的变体:

另一种变体是使用相同的数据库,对 Web 层和领域层进行蓝绿切换。

数据库通常是这种技术的挑战,尤其是在需要更改模式以支持新版本软件时。

至此,我们遇到了本文将涉及的主要问题。数据库。让我们再仔细看看这句话:

将数据库迁移到新版本

现在你应该问自己一个问题——如果数据库更改是向后不兼容的怎么办?我的应用程序版本 1 不会崩溃吗?实际上,它会崩溃……​

因此,尽管零停机/蓝绿部署的好处是巨大的,但公司往往遵循更安全的应用程序部署流程:

  • 准备包含新版本应用程序的软件包

  • 关闭正在运行的应用程序

  • 运行数据库迁移脚本

  • 部署并运行新版本的应用程序

在本文中,我们将更深入地描述如何处理数据库和代码,以便您可以从零停机部署的好处中受益。

数据库问题

如果您的应用程序是无状态的,并且不在数据库中存储任何数据,那么您现在就可以开始进行零停机部署。不幸的是,大多数软件都需要将数据存储在某个地方。这就是为什么在进行任何模式更改之前必须三思而后行。在我们详细介绍如何更改模式以实现零停机部署之前,让我们首先关注模式版本控制。

模式版本控制

在本文中,我们将使用 Flyway 作为模式版本控制工具。我们当然也在编写一个对 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 Boot 文档

将模式版本控制工具与 Spring Boot 结合使用,您可以获得两大好处。

  • 您将数据库更改与代码更改解耦

  • 数据库迁移与您的应用程序部署一起进行 - 您的部署流程得到简化

解决数据库问题

在本文的以下部分,我们将重点介绍数据库更改的两种方法。

  • 向后不兼容

  • 向后兼容

第一个将作为警告,说明如果在没有准备的情况下尝试进行零停机部署的后果。第二个将提供一个建议的解决方案,说明如何在进行零停机部署的同时保持向后兼容性。

我们将要研究的项目将是一个简单的 Spring Boot Flyway 应用程序,其中我们在数据库中有一个包含 first_namelast_namePerson。我们希望将 last_name 列重命名为 surname

假设

在深入细节之前,我们需要定义一些关于我们应用程序的假设。我们希望获得的关键结果是拥有一个相当简单的流程。

提示

商业专业提示:简化流程可以为您节省大量支持成本(公司员工越多,节省的成本越多)!

我们不希望进行数据库回滚

不进行数据库回滚可以简化部署过程(有些数据库回滚几乎是不可能的,例如回滚删除)。我们更倾向于只回滚应用程序。这样,即使您有不同的数据库(例如 SQL 和 NoSQL),您的部署流水线看起来也会是一样的。

我们希望始终能够将应用程序回滚一个版本(不多于一个)

我们只在必要时回滚。如果当前版本存在无法轻松解决的 bug,我们希望能够恢复到最后一个正常工作的版本。我们假设这个最后一个正常工作的版本就是前一个版本。维持代码和数据库兼容性超过单次部署将极其困难且成本高昂。

提示

为了便于阅读,本文中我们将使用主版本号递增的方式来标记应用程序版本。

步骤 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');

代码更改

应用程序将 Person 数据存储在名为 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
 *
 *      https://apache.ac.cn/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. 在数据库的 v2bad 版本中,列 last_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 部署后,我们决定将应用程序回滚到版本 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;

代码更改

我们已将字段名从 lastName 更改为 surname

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

这是我们最常遇到的情况。我们需要执行向后不兼容的更改。我们已经证明,要实现零停机部署,我们不能简单地在没有额外工作的情况下应用数据库迁移。在本文的这一部分,我们将通过 3 次应用程序部署以及数据库迁移来实现所需的效果,同时保持向后兼容。

提示

温馨提示 - 假设我们的数据库版本为 v1。它包含 first_namelast_name 列。我们想将 last_name 更改为 surname。我们的应用程序版本为 1.0.0,尚未开始使用 surname 列。

步骤 2:添加 surname

应用程序版本:2.0.0

数据库版本:v2

注释

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

推出新版本

步骤

  1. 迁移数据库以创建名为 surname 的新列。现在您的数据库版本为 v2

  2. 将数据从 last_name 列复制到 surname注意:如果数据量很大,您应该考虑批量迁移!

  3. 编写代码同时使用列和列。现在您的应用程序版本为 2.0.0

  4. 如果 surname 列不为 null,则从 surname 列读取 surname 值;如果 surname 未设置,则从 last_name 读取。您可以从代码中移除 getLastName() 方法,因为它在应用程序从 3.0.0 回滚到 2.0.0 时会产生 null。

如果您使用 Spring Boot Flyway,这两个步骤将在启动应用程序的 2.0.0 版本时执行。如果您手动运行数据库版本控制工具,则必须在单独的流程中进行(首先手动升级数据库版本,然后部署新应用程序)。

重要提示

请记住,新创建的列决不能设置为 NOT NULL。如果您回滚,旧应用程序不知道新列,并且在 Insert 时不会设置它。但是如果您添加了该约束并且数据库版本为 v2,则会要求设置新列的值。这将导致约束冲突。

重要提示

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

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 不使用数据库的 surname 列,而版本 2.0.0 使用。它们互不干扰,不应该抛出任何异常。

  5. 版本 2.0.0 将数据保存到旧列和新列,因此它是向后兼容的

重要提示

如果您有任何基于旧/新列的值计数项目的查询,您必须记住现在您拥有重复的值(很可能仍在迁移中)。例如,如果您想计算姓氏(无论您怎么称呼)以字母 A 开头的用户数量,那么在数据迁移( 列)完成之前,如果您对新列执行查询,数据可能会不一致。

回滚应用程序

当前情况是,应用程序版本为 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_namesurname 两列中。我们从 surname 列读取数据,如果 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
 *
 *      https://apache.ac.cn/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

注释

在此步骤之后,我们已完全切换到新的 surname 列,并从代码中移除了旧的 last_name 列。v3 的数据库迁移将删除 last_name 列。

回滚应用程序

当前情况是,应用程序版本为 3.0.0,数据库版本为 v3。版本 3.0.0 不再将数据存储到 last_name 列中。这意味着最新的信息存储在 surname 列中。

步骤

  1. 将您的应用程序回滚到版本 2.0.0

  2. 版本 2.0.0 同时使用 last_namesurname 列。

  3. 版本 2.0.0 如果 surname 列不为 null,将首先选择 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;

代码变更

我们正在将数据存储到 last_namesurname 两列中。我们从 surname 列读取数据,如果 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
 *
 *      https://apache.ac.cn/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_namesurname两列。应用程序从last_name列读取。数据库版本为v2,包含last_namesurname两列。surname列是last_name列的副本。(注意:此列不能有NOT NULL约束)

  3. 部署应用程序版本3.0.0,该版本仅将数据保存到surname并从surname读取。至于数据库,完成了last_namesurname的最终迁移。同时,从last_name中去掉了NOT NULL约束。数据库版本现在是v3

  4. 部署应用程序版本4.0.0 - 代码没有变化。部署数据库版本v4,首先执行last_namesurname的最终迁移并移除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](http://localhost:8080/flyway)查看脚本列表。

该示例还启用了H2控制台(位于[http://localhost:8080/h2-console](http://localhost:8080/h2-console)),您可以通过它查看数据库状态(默认jdbc url是jdbc:h2:mem:testdb)。

延伸阅读

获取 Spring 新闻简报

订阅 Spring 新闻简报,保持联系

订阅

抢先一步

VMware 提供培训和认证,为您的进步提供强大助力。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅,提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举办的活动

查看 Spring 社区所有即将举办的活动。

查看全部