高级持续交付实践指南
立即解锁
发布时间: 2025-08-24 02:19:39 阅读量: 1 订阅数: 4 

# 高级持续交付实践指南
## 1. 非向后兼容变更处理
非向后兼容的数据库变更处理起来要困难得多。例如,如果数据库变更 v11 是非向后兼容的,那么将服务回滚到 1.2.7 版本就会变得不可能。为了解决这个问题,我们可以将非向后兼容的变更转换为在一定时间内向后兼容的变更,具体做法是将模式迁移拆分为两个部分:
- **立即执行的向后兼容更新**:通常意味着保留一些冗余数据。
- **在回滚期之后执行的非向后兼容更新**:回滚期定义了我们可以将代码回退的范围。
### 1.1 删除列的示例
以删除列为例,具体步骤如下:
1. **停止在源代码中使用该列**(v1.2.5,向后兼容更新,首先执行)。
2. **从数据库中删除该列**(v11,非向后兼容更新,在回滚期之后执行)。
在数据库 v11 之前的所有服务版本都可以回滚到任何先前版本,而从服务 v1.2.8 开始的服务只能在回滚期内回滚。这种方法虽然看似只是延迟了列的删除,但它解决了回滚和零停机部署的问题,降低了发布风险。如果将回滚期设置为合理的时间,例如每天多次发布的情况下设置为两周,那么风险可以忽略不计。
### 1.2 重命名列的示例
以计算器服务中重命名 `result` 列到 `sum` 为例,具体步骤如下:
1. **向数据库中添加新列**:创建 `src/main/resources/db/migration/V3__Add_sum_column.sql` 迁移文件,内容如下:
```sql
alter table CALCULATION
add SUM varchar(100);
```
执行迁移后,数据库中将有 `result` 和 `sum` 两列。
2. **修改代码以使用两列**:在 `Calculation` 类中进行修改,代码如下:
```java
public class Calculation {
...
private String sum;
...
public Calculation(String a, String b, String sum, Timestamp createdAt) {
this.a = a;
this.b = b;
this.sum = sum;
this.result = sum;
this.createdAt = createdAt;
}
public String getSum() {
return sum != null ? sum : result;
}
}
```
从现在起,每次向数据库中添加一行时,相同的值会同时写入 `result` 和 `sum` 列。读取 `sum` 时,首先检查新列中是否存在该值,如果不存在则从旧列中读取。也可以使用数据库触发器来实现相同的效果。到目前为止,所有的更改都是向后兼容的,因此我们可以随时将服务回滚到任何版本。
3. **合并两列中的数据**:在发布稳定一段时间后执行,创建 `V4__Copy_result_into_sum_column.sql` 迁移文件,内容如下:
```sql
update CALCULATION
set CALCULATION.sum = CALCULATION.result
where CALCULATION.sum is null;
```
此时回滚仍然没有限制,但如果需要部署步骤 2 更改之前的版本,则需要重复此数据库迁移。
4. **从代码中移除旧列**:由于新列中已经包含了所有数据,因此可以在数据模型中不再使用旧列。修改 `Calculation` 类,移除与 `result` 相关的代码,如下所示:
```java
public class Calculation {
...
private String sum;
...
public Calculation(String a, String b, String sum, Timestamp createdAt) {
this.a = a;
this.b = b;
this.sum = sum;
this.createdAt = createdAt;
}
public String getSum() {
return sum;
}
}
```
此操作仅在步骤 2 之前是向后兼容的,如果需要回滚到步骤 1,可能会丢失此步骤之后存储的数据。
5. **从数据库中删除旧列**:在回滚期之后执行,确保不会回滚到步骤 4 之前。创建 `V5__Drop_result_column.sql` 迁移文件,内容如下:
```sql
alter table CALCULATION
drop column RESULT;
```
完成此步骤后,列重命名过程结束。这种方法虽然使操作稍微复杂,但降低了非向后兼容数据库变更的风险,实现了零停机部署。
## 2. 分离数据库更新和代码变更
之前我们通常将数据库迁移与服务发布一起执行,即每次提交(意味着每次发布)都包含数据库变更和代码变更。但推荐的做法是明确分离,即提交到仓库的要么是数据库更新,要么是代码变更。
### 2.1 分离的好处
数据库 - 服务变更分离的好处是可以免费进行向后兼容性检查。例如,假设变更 v11 和 v1.2.7 涉及一个逻辑变更,如向数据库中添加新列。我们首先提交数据库 v11,持续交付管道中的测试会检查数据库 v11 是否与服务 v.1.2.6 兼容,即检查数据库更新 v11 是否向后兼容。然后提交 v1.2.7 变更,管道会检查数据库 v11 是否与服务 v1.2.7 正常工作。
### 2.2 分离的实践
数据库 - 代码分离并不意味着必须有两个单独的 Jenkins 管道,管道可以同时执行两者,但应保持每次提交要么是数据库更新要么是代码变更的良好实践。总之,数据库模式变更不应手动进行,而应使用迁移工具自动化执行,并作为持续交付管道的一部分。同时,应避免非向后兼容的数据库更新,最好的方法是将数据库和代码变更分别提交到仓库。
## 3. 避免共享数据库
在许多系统中,数据库成为多个服务共享的中心点。这种情况下,对数据库的任何更新都会变得更具挑战性,因为需要在所有服务之间进行协调。
### 3.1 共享数据库的问题
以在线商店为例,`Customers` 表包含 `first name`、`last name`、`username`、`password`、`email` 和 `discount` 列,有三个服务对客户数据感兴趣:
- **Profile manager**:用于编辑用户数据。
- **Checkout processor**:处理结账(读取用户名和电子邮件)。
- **Discount manager**:分析客户订单并设置合适的折扣。
这些服务依赖于相同的数据库模式,存在以下问题:
- **更新兼容性**:更新模式时,必须确保与所有三个服务兼容。虽然所有向后兼容的变更没问题,但任何非向后兼容的更新都会变得非常困难甚至不可能。
- **管道选择**:每个服务都有单独的交付周期和持续交付管道,那么应该使用哪个管道进行数据库模式迁移呢?这个问题没有很好的答案。
### 3.2 解决方案
为了解决这些问题,每个服务应该有自己的数据库,并且服务之间通过 API 进行通信。例如,`Checkout processor` 应该通过 `Profile manager` 的 API 获取客户数据,`discount` 列应该提取到单独的数据库(或模式)中,由 `Discount manager` 负责管理。这种方法符合微服务架构原则,通信通过 API 比直接访问数据库更灵活。在单体系统中,数据库通常是集成点,但这种方法会
0
0
复制全文
相关推荐









