Zero Downtime Deployment และฐานข้อมูล

Zero Downtime Deployment และฐานข้อมูล

บทความนี้จะอธิบายรายละเอียดวิธีการแก้ไขปัญหาความเข้ากันได้ของฐานข้อมูลในการปรับใช้ เราจะบอกคุณว่าจะเกิดอะไรขึ้นกับแอปพลิเคชันการผลิตของคุณหากคุณพยายามปรับใช้โดยไม่ได้เตรียมการเบื้องต้น จากนั้นเราจะผ่านขั้นตอนวงจรการใช้งานของแอปพลิเคชันที่ต้องมีการหยุดทำงานเป็นศูนย์ (ประมาณ เลน: เพิ่มเติม - การหยุดทำงานเป็นศูนย์). ผลลัพธ์ของการดำเนินงานของเราคือการใช้การเปลี่ยนแปลงฐานข้อมูลที่เข้ากันไม่ได้แบบย้อนหลังในลักษณะที่เข้ากันได้แบบย้อนหลัง

หากคุณต้องการทำความเข้าใจตัวอย่างโค้ดจากบทความ สามารถดูได้ที่ GitHub.

การแนะนำ

การปรับใช้การหยุดทำงานเป็นศูนย์

ช่างลึกลับจริงๆ การปรับใช้การหยุดทำงานเป็นศูนย์? คุณสามารถพูดได้ว่านี่คือเมื่อแอปพลิเคชันของคุณถูกปรับใช้ในลักษณะที่คุณสามารถแนะนำแอปพลิเคชันเวอร์ชันใหม่สู่การใช้งานจริงได้สำเร็จ ในขณะที่ผู้ใช้ไม่ได้สังเกตเห็นความไม่พร้อมใช้งาน จากมุมมองของผู้ใช้และบริษัท นี่เป็นสถานการณ์การปรับใช้ที่ดีที่สุดเท่าที่จะเป็นไปได้ เนื่องจากทำให้สามารถแนะนำคุณสมบัติใหม่ๆ และแก้ไขจุดบกพร่องได้โดยไม่หยุดชะงัก

จะบรรลุเป้าหมายนี้ได้อย่างไร? มีหลายวิธี นี่คือหนึ่งในนั้น:

  • ปรับใช้เวอร์ชันหมายเลข 1 ของบริการของคุณ
  • ดำเนินการย้ายฐานข้อมูล
  • ปรับใช้บริการเวอร์ชัน #2 ของคุณควบคู่ไปกับเวอร์ชัน #1
  • ทันทีที่คุณเห็นเวอร์ชันหมายเลข 2 ทำงานตามที่ควรจะเป็น ให้ลบเวอร์ชันหมายเลข 1 ออก
  • พร้อม!

ง่ายใช่มั้ย? น่าเสียดายที่มันไม่ง่ายอย่างนั้น และเราจะดูรายละเอียดในภายหลัง ตอนนี้เรามาดูกระบวนการปรับใช้ทั่วไปอีกขั้นตอนหนึ่ง - การปรับใช้สีน้ำเงินเขียว

เคยได้ยินไหม การปรับใช้สีน้ำเงินสีเขียว? Cloud Foundry ทำให้สิ่งนี้เป็นเรื่องง่ายมาก เพียงแค่มองไปที่ บทความนี้ซึ่งเราจะอธิบายเรื่องนี้โดยละเอียดยิ่งขึ้น เพื่อสรุปสั้นๆ ให้เราเตือนคุณถึงวิธีการปรับใช้สีน้ำเงินเขียว:

  • ตรวจสอบให้แน่ใจว่าสำเนารหัสการผลิตของคุณสองชุด ("สีน้ำเงิน" และ "สีเขียว") ใช้งานได้
  • นำการรับส่งข้อมูลทั้งหมดไปยังสภาพแวดล้อมสีน้ำเงิน เช่น เพื่อให้ URL ที่ใช้งานจริงชี้ไปตรงนั้น
  • ปรับใช้และทดสอบการเปลี่ยนแปลงแอปพลิเคชันทั้งหมดในสภาพแวดล้อมสีเขียว
  • เปลี่ยน URL จากสภาพแวดล้อมสีน้ำเงินเป็นสีเขียว

การใช้งาน Blue Green เป็นแนวทางที่ช่วยให้คุณสามารถแนะนำคุณสมบัติใหม่ได้อย่างง่ายดายโดยไม่ต้องกังวลว่าการใช้งานจริงจะหยุดชะงัก นี่เป็นเพราะความจริงที่ว่าแม้ว่าจะมีบางอย่างเกิดขึ้น คุณสามารถย้อนกลับไปยังสภาพแวดล้อมก่อนหน้าได้อย่างง่ายดายเพียงแค่ "สะบัดสวิตช์"

หลังจากอ่านทั้งหมดข้างต้นแล้ว คุณอาจถามคำถาม: การหยุดทำงานเป็นศูนย์เกี่ยวข้องกับการปรับใช้ Blue green อย่างไร

พวกมันมีอะไรที่เหมือนกันค่อนข้างมาก เนื่องจากการรักษาสำเนาสองชุดในสภาพแวดล้อมเดียวกันต้องใช้ความพยายามสองเท่าในการบำรุงรักษา นี่คือสาเหตุที่บางทีมอ้างสิทธิ์ มาร์ติน ฟาวเลอร์ให้ปฏิบัติตามรูปแบบต่างๆ ของแนวทางนี้:

อีกทางเลือกหนึ่งคือการใช้ฐานข้อมูลเดียวกัน โดยสร้างสวิตช์สีน้ำเงิน-เขียวสำหรับเลเยอร์เว็บและโดเมน ในแนวทางนี้ ฐานข้อมูลมักจะมีปัญหา โดยเฉพาะอย่างยิ่งเมื่อคุณต้องการเปลี่ยนสคีมาเพื่อรองรับซอฟต์แวร์เวอร์ชันใหม่

และที่นี่เรามาถึงปัญหาหลักในบทความนี้ ฐานข้อมูล. ลองมาดูวลีนี้อีกครั้ง

ดำเนินการย้ายฐานข้อมูล

ตอนนี้คุณต้องถามตัวเองด้วยคำถาม - จะเกิดอะไรขึ้นถ้าการเปลี่ยนแปลงฐานข้อมูลเข้ากันไม่ได้แบบย้อนหลัง? แอปเวอร์ชันแรกของฉันจะไม่พังใช่ไหม จริงๆ แล้วนี่คือสิ่งที่จะเกิดขึ้น...

ดังนั้น แม้จะได้รับประโยชน์มากมายจากการหยุดทำงานเป็นศูนย์/การปรับใช้สีน้ำเงินเขียว แต่บริษัทต่างๆ ก็มักจะปฏิบัติตามกระบวนการที่ปลอดภัยกว่าต่อไปนี้ในการปรับใช้แอปพลิเคชันของตน:

  • เตรียมแพ็คเกจพร้อมแอพพลิเคชั่นเวอร์ชันใหม่
  • ปิดแอปพลิเคชันที่ทำงานอยู่
  • เรียกใช้สคริปต์เพื่อย้ายฐานข้อมูล
  • ปรับใช้และเปิดตัวแอปพลิเคชันเวอร์ชันใหม่

ในบทความนี้ เราจะให้รายละเอียดเกี่ยวกับวิธีที่คุณสามารถทำงานกับฐานข้อมูลและโค้ดของคุณเพื่อใช้ประโยชน์จากการปรับใช้การหยุดทำงานเป็นศูนย์

ปัญหาเกี่ยวกับฐานข้อมูล

หากคุณมีแอปพลิเคชันไร้สถานะที่ไม่ได้จัดเก็บข้อมูลใดๆ ไว้ในฐานข้อมูล คุณสามารถปรับใช้ระบบหยุดทำงานเป็นศูนย์ได้ทันที น่าเสียดายที่ซอฟต์แวร์ส่วนใหญ่จำเป็นต้องจัดเก็บข้อมูลไว้ที่ใดที่หนึ่ง นี่คือเหตุผลที่คุณควรคิดให้รอบคอบก่อนทำการเปลี่ยนแปลงวงจร ก่อนที่เราจะลงรายละเอียดเกี่ยวกับวิธีการเปลี่ยนสคีมาเพื่อให้สามารถปรับใช้ได้โดยไม่มีการหยุดทำงาน เรามาเน้นที่สคีมาการกำหนดเวอร์ชันก่อน

รูปแบบการกำหนดเวอร์ชัน

ในบทความนี้เราจะใช้ ฟลายเวย์ เป็นเครื่องมือควบคุมเวอร์ชัน (ประมาณ การแปล: เรากำลังพูดถึงการย้ายฐานข้อมูล). โดยปกติแล้ว เราจะเขียนแอปพลิเคชัน 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 คุณจะได้รับประโยชน์ใหญ่ 2 ประการ:

  • คุณแยกการเปลี่ยนแปลงฐานข้อมูลออกจากการเปลี่ยนแปลงรหัส
  • การย้ายฐานข้อมูลเกิดขึ้นพร้อมกับการเปิดตัวแอปพลิเคชันของคุณ เช่น กระบวนการปรับใช้ของคุณง่ายขึ้น

การแก้ไขปัญหาฐานข้อมูล

ในส่วนถัดไปของบทความ เราจะเน้นที่การดูสองแนวทางในการเปลี่ยนแปลงฐานข้อมูล

  • ความไม่เข้ากันแบบย้อนกลับ
  • ความเข้ากันได้แบบย้อนหลัง

อันแรกจะถือเป็นคำเตือนว่าคุณไม่ควรทำการปรับใช้การหยุดทำงานเป็นศูนย์โดยไม่มีการเตรียมการเบื้องต้น... อันที่สองเสนอวิธีแก้ปัญหาเกี่ยวกับวิธีที่คุณสามารถดำเนินการปรับใช้โดยไม่ต้องหยุดทำงานและในขณะเดียวกันก็รักษาความเข้ากันได้แบบย้อนหลัง

โครงการของเราที่เราจะดำเนินการคือแอปพลิเคชัน 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. ในฐานข้อมูล 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 (ประมาณ ต่อ: ผู้เขียนอาจหมายถึงการทดสอบ 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_name и last_name. เราต้องเปลี่ยน last_name บน surname. เรายังมีเวอร์ชันแอปด้วย 1.0.0, ซึ่งยังไม่ได้ใช้ surname.

ขั้นตอนที่ 2: เพิ่มนามสกุล

เวอร์ชันแอป: 2.0.0
เวอร์ชันฐานข้อมูล: v2

ความเห็น

ด้วยการเพิ่มคอลัมน์ใหม่และคัดลอกเนื้อหา เราจะสร้างการเปลี่ยนแปลงฐานข้อมูลที่เข้ากันได้แบบย้อนหลัง ในเวลาเดียวกัน ถ้าเราย้อนกลับ JAR หรือมี JAR เก่าทำงานอยู่ มันจะไม่พังระหว่างการดำเนินการ

เรากำลังเปิดตัวเวอร์ชันใหม่

ขั้นตอน:

  1. ดำเนินการย้ายฐานข้อมูลเพื่อสร้างคอลัมน์ใหม่ surname. ตอนนี้เป็นเวอร์ชัน DB ของคุณ v2
  2. คัดลอกข้อมูลจาก last_name в surname. หมายเหตุหากคุณมีข้อมูลนี้จำนวนมาก คุณควรพิจารณาการโยกย้ายแบบแบตช์!
  3. เขียนโค้ดที่ใช้ ทั้งคู่ и ใหม่และ เก่า คอลัมน์. ตอนนี้เป็นเวอร์ชันแอปของคุณแล้ว 2.0.0
  4. อ่านค่าจากคอลัมน์ surnameถ้ามันไม่ใช่ nullหรือจากลast_nameถ้า surname ไม่ได้ระบุ คุณสามารถลบได้ getLastName() จากโค้ดเนื่องจากมันจะส่งออก null เมื่อย้อนกลับแอปพลิเคชันของคุณจาก 3.0.0 ไปยัง 2.0.0.

หากคุณใช้ Spring Boot Flyway สองขั้นตอนนี้จะดำเนินการระหว่างการเริ่มต้นเวอร์ชัน 2.0.0 การใช้งาน หากคุณเรียกใช้เครื่องมือกำหนดเวอร์ชันฐานข้อมูลด้วยตนเอง คุณจะต้องทำสองสิ่งที่ต่างกันในการดำเนินการนี้ (ขั้นแรกให้อัปเดตเวอร์ชัน db ด้วยตนเอง จากนั้นจึงปรับใช้แอปพลิเคชันใหม่)

มีความสำคัญ โปรดจำไว้ว่าคอลัมน์ที่สร้างขึ้นใหม่ ไม่ควร เป็น ไม่เป็นโมฆะ. หากคุณทำการย้อนกลับ แอปพลิเคชันเก่าจะไม่ทราบเกี่ยวกับคอลัมน์ใหม่ และจะไม่ติดตั้งในระหว่างนั้น 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

ความเห็น

บันทึก ต่อ: เห็นได้ชัดว่าในบทความต้นฉบับผู้เขียนคัดลอกข้อความของบล็อกนี้จากขั้นตอนที่ 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;

การเปลี่ยนแปลงรหัส

บันทึก ต่อ: ผู้เขียนคัดลอกคำอธิบายของบล็อกนี้ผิดพลาดจากขั้นตอนที่ 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: การลบ 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_name и surname. แอปพลิเคชันอ่านจาก last_name. ฐานข้อมูลอยู่ในเวอร์ชัน v2ประกอบด้วยคอลัมน์ต่างๆ เช่น last_nameและ surname. surname เป็นสำเนาของ last_name. (หมายเหตุ: คอลัมน์นี้ต้องไม่มีข้อจำกัดที่ไม่เป็นโมฆะ)
  3. การปรับใช้เวอร์ชันแอปพลิเคชัน 3.0.0ซึ่งเก็บข้อมูลไว้เท่านั้น surname และอ่านจากนามสกุล สำหรับฐานข้อมูลนั้น การโยกย้ายครั้งสุดท้ายกำลังเกิดขึ้น last_name в surname. ยังเป็นข้อจำกัด ไม่เป็นโมฆะ ลบออกจาก 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

ตัวอย่าง Flyway ของ Spring Boot

ตัวอย่างทั้งหมดนำมาจาก Spring Boot Sample Flyway.

คุณสามารถดูที่ http://localhost:8080/flywayมีรายการสคริปต์อยู่

ตัวอย่างนี้ยังรวมถึงคอนโซล H2 (at http://localhost:8080/h2-console) เพื่อให้คุณสามารถดูสถานะฐานข้อมูลได้ (URL jdbc เริ่มต้นคือ jdbc:h2:mem:testdb).

นอกจากนี้

อ่านบทความอื่น ๆ ในบล็อกของเราด้วย:

ที่มา: will.com

เพิ่มความคิดเห็น