带数据库的零停机部署

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

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

如果您想浏览下面的代码示例,您可以在GitHub中找到所需的一切。

简介

零停机部署

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

您如何实现这一点?有许多方法,其中一种方法是

  • 部署服务版本 1

  • 将您的数据库迁移到新版本

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

  • 一旦您看到版本 2 运行良好,只需关闭版本 1

  • 您完成了!

很简单,不是吗?不幸的是,并没有那么简单,我们稍后将重点介绍这一点。现在,让我们检查另一个常见的部署过程,即蓝绿部署。

您是否听说过蓝绿部署?使用 Cloud Foundry,这非常容易做到。只需查看这篇文章,我们将在其中更详细地描述它。快速回顾一下,执行蓝绿部署就像

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

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

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

  • 通过将 URL 映射到绿色并取消映射蓝色来“切换”。

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

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

好吧,它们有很多共同之处,因为维护同一环境的两个副本会导致支持它所需的精力加倍。这就是为什么一些团队,正如Martin Fowler 所述,倾向于执行这种方法的变体

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

使用此技术时,数据库通常会带来挑战,尤其是在您需要更改架构以支持软件的新版本时。

在这里,我们遇到了本文将要讨论的主要问题。数据库。让我们再看一眼这句话

将您的数据库迁移到新版本

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

因此,即使零停机/蓝绿部署的好处巨大,公司也倾向于遵循这样一个更安全的应用程序部署过程

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

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

  • 运行数据库迁移脚本

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

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

数据库问题

如果您有一个无状态应用程序,它不会在数据库中存储任何数据,那么您现在就可以开始进行零停机部署。不幸的是,大多数软件都必须将数据存储在某个地方。因此,在进行任何类型的架构更改之前,您必须三思而后行。在我们深入探讨如何以允许零停机部署的方式更改架构的细节之前,让我们首先关注架构版本控制。

架构版本控制

在本文中,我们将使用Flyway作为架构版本控制工具。当然,我们还编写了一个 Spring Boot 应用程序,该应用程序对 Flyway 具有原生支持,并在应用程序上下文设置时执行架构迁移。使用 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 应用程序,其中我们有一个Person,它在数据库中具有first_namelast_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. 在数据库的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列不为空,则从中读取姓氏值;如果surname未设置,则从last_name读取。您可以从代码中删除getLastName(),因为当您的应用程序从3.0.0回滚到2.0.0时,它将产生空值。

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

重要

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

重要

您应该删除getLastName()方法,因为在版本3.0.0中,代码中没有last_name列的概念。这意味着那里将设置空值。您可以保留该方法并添加空值检查,但更好的解决方案是确保在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不使用数据库的surname列,而版本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_namesurname中。此外,我们从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

注释

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

回滚应用程序

目前的情况是,我们的应用程序版本为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列(如果它不为空),如果为空,则选择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中。此外,我们从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:从数据库中删除last_name

应用程序版本: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列的副本。(注意:此列不得具有非空约束)

  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 Flyway 示例项目的克隆。

您可以查看[https://127.0.0.1:8080/flyway](https://127.0.0.1:8080/flyway)以查看脚本列表。

该示例还启用了 H2 控制台(在[https://127.0.0.1:8080/h2-console](https://127.0.0.1:8080/h2-console)),以便您可以查看数据库的状态(默认 jdbc url 为jdbc:h2:mem:testdb)。

扩展阅读

获取 Spring 时事通讯

与 Spring 时事通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

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

了解更多

即将举行的活动

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

查看全部