Spring REST API & Data JPA
44/46
Bài 44 / 46~12 phútMigration & CapstoneLộ trình · chặng 17/21Miễn phí… lượt xem

Chiến Lược Flyway: Zero-Downtime và Expand-Contract

Khám phá chiến lược forward-only, cấu trúc migration đa môi trường và kỹ thuật Expand-Contract di chuyển schema zero-downtime cho production.

TL;DR: Flyway community áp dụng chiến lược forward-only — khi phát sinh lỗi, giải pháp duy nhất là tạo migration mới sửa tiếp về phía trước thay vì undo làm mất mát dữ liệu sản xuất. Khi cần tách biệt dữ liệu môi trường, phân tách thư mục dev/ và prod/ với dải version V100+. View và stored procedure dùng migration lặp lại R__ để tự chạy lại khi file đổi checksum. Để zero-downtime trên cụm rolling deployment, áp dụng kỹ thuật Expand-Contract 5 bước tách biệt.

Tại sao production cần chiến lược forward-only thay vì down-migration?

Flyway Community (bản mã nguồn mở phổ biến) không cung cấp lệnh undo. Đây là quyết định kiến trúc có chủ đích nhằm bảo vệ hệ thống phân tán và toàn vẹn dữ liệu.

Rủi ro mất mát dữ liệu của Down-migration

Giả sử đội ngũ phát triển áp dụng down-migration để hoàn tác một cột mới:

SQL
-- V5__add_priority.sql
ALTER TABLE projects ADD COLUMN priority VARCHAR(20) NOT NULL DEFAULT 'MEDIUM';

-- U5__add_priority.sql (undo scenario)
ALTER TABLE projects DROP COLUMN priority;

Khi kịch bản U5 được kích hoạt, toàn bộ dữ liệu người dùng đã ghi vào cột priority từ lúc áp dụng V5 đến khi xảy ra sự cố sẽ bị xóa sạch vĩnh viễn. Trên môi trường production có lượng giao dịch lớn liên tục, tổn thất này là không thể cứu vãn.

Dòng thời gian từ lúc apply V5 lúc 8 giờ, 200 request ghi giá trị thật vào cột priority, tới lúc phát hiện bug lúc 8 rưỡi, rồi rẽ hai nhánh: nhánh undo drop cột làm mất vĩnh viễn 200 giá trị và xoá dấu vết V5 khỏi history, nhánh đi tiếp bằng V6 cho phép chọn dữ liệu nào giữ lại và history giữ đủ cả V5 lẫn V6

Cơ chế Forward-only bảo vệ hệ thống

Thay vì đảo ngược trạng thái, đội ngũ kỹ thuật tạo một migration mới để tiếp tục tiến lên:

SQL
-- V5__add_priority.sql (applied, immutable)
ALTER TABLE projects ADD COLUMN priority VARCHAR(20) NOT NULL DEFAULT 'MEDIUM';

-- V6__fix_priority_type.sql (forward fix)
ALTER TABLE projects DROP COLUMN priority;
ALTER TABLE projects ADD COLUMN priority_level INT NOT NULL DEFAULT 3;

Chiến lược forward-only mang lại 4 ưu điểm vượt trội:

  1. Lịch sử kiểm toán vẹn toàn: Bảng flyway_schema_history phản ánh chân thực từng mốc thời gian thay đổi schema.
  2. Kiểm soát di chuyển dữ liệu: Script mới có thể trích xuất, chuyển đổi và tái sử dụng dữ liệu từ cột cũ trước khi loại bỏ.
  3. Quy trình triển khai thống nhất: Hành động sửa lỗi sử dụng chung pipeline CI/CD với tính năng thông thường.
  4. Kiểm thử độc lập: Migration sửa đổi có thể chạy kiểm thử trên staging trước khi lên production.
Quy tắc bất biến

Một khi file migration đã merge vào nhánh chính và áp dụng lên database, file đó trở thành bất biến. Bất kỳ sửa đổi nội dung nào đều khiến Flyway phát hiện lệch checksum và dừng khởi động ứng dụng ngay lập tức.

Làm thế nào để quản lý seed data đa môi trường với Flyway?

Cơ sở dữ liệu ở môi trường phát triển (Dev) cần dữ liệu mẫu phong phú để kiểm thử, trong khi Production chỉ cần các bảng danh mục khởi tạo ban đầu hoặc tài khoản quản trị tối thiểu.

Tổ chức thư mục theo môi trường

Tách bạch thư mục lưu trữ theo từng môi trường triển khai:

Plain Text
src/main/resources/db/migration/
├── V1__init_schema.sql            # Mọi môi trường
├── V2__add_indexes.sql            # Mọi môi trường
├── dev/
│   └── V100__seed_dev_data.sql    # Chỉ nạp ở Dev / Test
└── prod/
    └── V100__create_admin.sql     # Chỉ nạp ở Production

Quy ước đặt tiền tố V100+ tạo một khoảng trống phiên bản (version gap) lớn, ngăn ngừa hoàn toàn nguy cơ xung đột số thứ tự khi các nhà phát triển tạo thêm migration schema thông thường (V3, V4...).

Thiết lập vị trí migration theo Spring Profile

YAML
# application-dev.yml
spring:
  flyway:
    locations:
      - classpath:db/migration
      - classpath:db/migration/dev

# application-prod.yml
spring:
  flyway:
    locations:
      - classpath:db/migration
      - classpath:db/migration/prod
SQL
-- db/migration/dev/V100__seed_dev_data.sql
INSERT INTO projects (name, description, status, priority_level) VALUES
    ('TaskFlow Demo', 'Demo project dev', 'ACTIVE', 3),
    ('Archived Task', 'Legacy archive', 'ARCHIVED', 1);

INSERT INTO tasks (project_id, title, status) VALUES
    (1, 'Setup Database', 'DONE'),
    (1, 'Implement API', 'IN_PROGRESS');

Khi nào nên sử dụng Java-based migration thay vì SQL thuần?

Hầu hết các tác vụ DDL/DML nên được diễn đạt bằng SQL chuẩn để dễ đọc và kiểm toán. Tuy nhiên, khi cần chuyển đổi dữ liệu phi cấu trúc phức tạp hoặc mã hóa bảo mật, Java-based migration là giải pháp tối ưu.

Java
package db.migration;

import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;

public class V3__MigrateLegacyFormat extends BaseJavaMigration {

    @Override
    public void migrate(Context context) throws Exception {
        Connection conn = context.getConnection();
        String selectSql = "SELECT id, legacy_json FROM projects WHERE migrated = false";
        String updateSql = "UPDATE projects SET new_data = ?, migrated = true WHERE id = ?";

        try (Statement st = conn.createStatement();
             ResultSet rs = st.executeQuery(selectSql);
             PreparedStatement upd = conn.prepareStatement(updateSql)) {

            while (rs.next()) {
                long id = rs.getLong("id");
                String legacyJson = rs.getString("legacy_json");
                String transformed = legacyJson.replace("\"v1\"", "\"v2\"");

                upd.setString(1, transformed);
                upd.setLong(2, id);
                upd.executeUpdate();
            }
        }
    }
}

Quy ước đặt tên class: V<Version>__<Description> kế thừa BaseJavaMigration. Flyway sẽ tự động quét classpath và nạp lớp Java này theo đúng thứ tự phiên bản.

Repeatable migration (R__) giải quyết bài toán view và procedure ra sao?

Các đối tượng cơ sở dữ liệu phi trạng thái (Stateless schema objects) như View, Stored Procedure hay Trigger thường xuyên thay đổi logic lọc hay tính toán. Nếu dùng versioned migration (V__), lịch sử sẽ bị phân mảnh bởi hàng chục file nhỏ như V10__update_view.sql, V15__update_view_again.sql.

Repeatable migration (R__) giải quyết triệt để vấn đề này: file migration không gắn phiên bản cố định, Flyway tính checksum và chỉ chạy lại khi nội dung file có sự thay đổi.

SQL
-- R__active_projects_view.sql
DROP VIEW IF EXISTS active_projects;

CREATE VIEW active_projects AS
SELECT
    p.id,
    p.name,
    p.status,
    COUNT(t.id) AS task_count,
    SUM(CASE WHEN t.status = 'DONE' THEN 1 ELSE 0 END) AS done_count
FROM projects p
LEFT JOIN tasks t ON t.project_id = p.id
WHERE p.status IN ('ACTIVE', 'PLANNING')
GROUP BY p.id, p.name, p.status;
SQL
-- R__log_project_change_fn.sql
CREATE OR REPLACE FUNCTION log_project_change()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO audit_log (table_name, operation, record_id, changed_at)
    VALUES ('projects', TG_OP, NEW.id, NOW());
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Thứ tự thực thi: Flyway luôn chạy toàn bộ versioned migration (V1 → V2 → ...) trước, sau đó mới duyệt và chạy các repeatable migration theo thứ tự chữ cái (R__a... → R__b...). Điều này bảo đảm các bảng dữ liệu cơ sở đã sẵn sàng trước khi khởi tạo view.

Yêu cầu Idempotent bắt buộc

Mọi file R__ phải viết theo phong cách idempotent (chạy bao nhiêu lần cũng cho ra cùng kết quả) bằng cách dùng cú pháp DROP VIEW IF EXISTS kết hợp CREATE VIEW, hoặc CREATE OR REPLACE FUNCTION.

Làm sao để thiết lập CI/CD pipeline kiểm thử migration an toàn?

Một migration chỉ thực sự an toàn khi được thẩm định trên cơ sở dữ liệu trống hoàn toàn trước khi hợp nhất vào nhánh chính.

Quy trình chuẩn trong CI/CD:

  1. Khởi tạo dịch vụ: Dựng một instance PostgreSQL sạch trên container độc lập.
  2. Áp dụng toàn bộ migration: Chạy Flyway từ V1 đến phiên bản mới nhất để bảo đảm không có script cũ nào bị gãy cú pháp.
  3. Hibernate Validation: Kích hoạt ddl-auto: validate để Hibernate đối soát các @Entity trong mã nguồn với schema vật lý vừa tạo.
YAML
# .github/workflows/ci.yml
jobs:
  test-migration:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: taskflow_ci
          POSTGRES_USER: runner
          POSTGRES_PASSWORD: secret
        ports: ["5432:5432"]
        options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Verify Flyway migrations & entity schema
        run: mvn verify
        env:
          SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/taskflow_ci
          SPRING_DATASOURCE_USERNAME: runner
          SPRING_DATASOURCE_PASSWORD: secret
          SPRING_PROFILES_ACTIVE: test

Thiết lập cấu hình kiểm thử chặt chẽ trong application-test.yml:

YAML
spring:
  flyway:
    enabled: true
    validate-on-migrate: true   # Fail if applied migration changed
  jpa:
    hibernate:
      ddl-auto: validate        # Fail if entity does not match schema

Quy trình Expand-Contract giúp đổi schema zero-downtime như thế nào?

Trong môi trường rolling deployment hiện đại (Kubernetes hay AWS ECS), trong suốt quá trình triển khai luôn tồn tại thời điểm cả hai phiên bản mã nguồn (Pod cũ và Pod mới) cùng kết nối vào một database. Nếu thay đổi schema đột ngột, Pod cũ sẽ gặp lỗi 500 Internal Server Error.

Bảng năm bước expand-contract đổi tên cột status sang state: mỗi hàng ghi migration hoặc deploy tương ứng, các cột đang có trong bảng projects, và trạng thái của pod cũ với pod mới; không hàng nào có pod bị gãy

Để đổi tên cột status thành state mà không gây gián đoạn hệ thống, ta áp dụng mẫu Expand-Contract qua 5 bước:

1. Bước 1: Expand (Mở rộng schema)

Thêm cột mới dưới dạng NULLABLE để mã nguồn cũ không bị ảnh hưởng:

SQL
-- V10__add_state_column.sql
ALTER TABLE projects ADD COLUMN state VARCHAR(20);

2. Bước 2: Deploy ứng dụng ghi đồng thời (Dual-write)

Triển khai phiên bản ứng dụng ghi dữ liệu vào cả hai cột cũ và mới:

Java
public void updateStatus(Long id, String val) {
    jdbcTemplate.update("UPDATE projects SET status = ?, state = ? WHERE id = ?", val, val, id);
}

3. Bước 3: Backfill dữ liệu lịch sử

Tạo migration sao chép toàn bộ dữ liệu lịch sử từ cột cũ sang cột mới, sau đó gắn ràng buộc NOT NULL:

SQL
-- V11__backfill_state_from_status.sql
UPDATE projects SET state = status WHERE state IS NULL;
ALTER TABLE projects ALTER COLUMN state SET NOT NULL;

4. Bước 4: Deploy ứng dụng đọc cột mới (Read new)

Cập nhật mã nguồn chuyển toàn bộ thao tác đọc/ghi sang cột state. Khi tất cả các Pod hoàn tất cập nhật, không còn bất kỳ truy vấn nào chạm vào cột status.

5. Bước 5: Contract (Thu hẹp schema)

Sau khi chu kỳ release ổn định, tạo migration cuối cùng để loại bỏ cột cũ:

SQL
-- V12__drop_status_column.sql
ALTER TABLE projects DROP COLUMN status;

Những điểm mấu chốt cần ghi nhớ là gì?

  • Forward-only: Sửa lỗi schema bằng cách tiến về phía trước qua script mới. Không dùng down-migration vì nguy cơ làm mất dữ liệu sản xuất.
  • Tách biệt môi trường: Phân chia rõ ràng giữa schema lõi và seed data kiểm thử bằng thư mục con và tiền tố V100+.
  • Repeatable R__: Lựa chọn hoàn hảo cho View và Procedure; bắt buộc tuân thủ tính chất idempotent.
  • Thẩm định CI/CD: Kết hợp validate-on-migrate: true và Hibernate ddl-auto: validate trên DB kiểm thử sạch để bắt lỗi lệch schema từ sớm.
  • Mẫu Expand-Contract: Công thức chuẩn mực cho zero-downtime deployment, tách rời từng pha DDL và mã nguồn nhằm bảo đảm tính tương thích ngược.

Bạn đã nắm vững các chiến lược Flyway nâng cao chưa?

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao Flyway Community không hỗ trợ down-migration và khuyến khích forward-only?
  2. Q2
    Quy ước đặt tên file seed data cho môi trường dev như V100__seed_dev.sql mang lại lợi ích gì?
  3. Q3
    Đặc điểm cốt lõi của repeatable migration (R__) trong Flyway là gì?
  4. Q4
    Trong quy trình Expand-Contract để đổi tên cột, tại sao không thể gộp bước thêm cột mới và drop cột cũ vào một lần triển khai?
  5. Q5
    Cấu hình ddl-auto: validate của Hibernate trong CI pipeline có vai trò gì quan trọng nhất?

Bài tiếp theo: Mini-challenge: TaskFlow v2 — Postgres + JPA capstone

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

✓ Bạn vừa đi hết chặng Database migration (Flyway) (17/21)

Chặng tiếp: Authentication & Authorization — “Security Architecture & Filter Chain — tổng quan”

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Mini-challenge: TaskFlow v2 — Postgres + JPA capstone