Đối với các chương trình nằm ở phía server-side, chúng ta sẽ gặp phải vấn đề downtime khi cần triển khai upgrade code mới. Để giải quyết vấn đề này, ta thường nhân rộng service thành nhiều replica (>= 3) và sử dụng kỹ thuật rolling upgrade
: từ từ triển khai upgrade trên từng replica trước; kiểm tra nó có hoạt động trơn tru, có bị lỗi gì không; rồi từ từ tiến hành tiếp trên các replica còn lại.
Điều này dẫn tới phiên bản code cũ và mới, định dạng (format) dữ liệu cũ và mới tồn tại song song trong hệ thống cùng một lúc. Để đảm bảo cho service chạy trơn tru trên cả phiên bản cũ và mới, ta cần đảm bảo sự tương thích (compatibility) trên cả 2 chiều:
- Backward compatibility: code mới có thể đọc được dữ liệu viết bởi code cũ.
- Forward compatibility: code cũ có thể đọc được dữ liệu viết bởi code mới.
Backward compatibility thường không khó: biết được format của code cũ là gì, ta hoàn toàn có thể xử lý được nó (hoặc nếu đường cùng quá thì ta cứ giữ lại đoạn code đang đọc dữ liệu cũ, đừng xóa nó đi). Tuy nhiên, Forward compatibility sẽ phức tạp hơn, bởi vì nó đòi hỏi ta phải phòng ngừa trước những thứ sẽ được thêm bởi code mới.
Thay đổi data type của field
Ta cần tránh đổi data type, vì việc này sẽ dẫn tới không thỏa mãn cả 2 chiều compatibility: code mới không đọc được định dạng cũ, và code cũ cũng không đọc được định dạng mới.
Xóa field
Ta cũng cần tránh xóa field, vì điều này có thể làm cho code cũ bị không tương thích. Trước khi quyết định xóa field, hãy đảm bảo rằng là field đó đã được khai báo giá trị mặc định (default value) ở code cũ.
Thêm field
Code mới ghi 1 giá trị nào đó vào DB, và sau đấy bản ghi đó lại được đọc ra từ replica vẫn đang chạy code cũ. Dữ liệu được load vào object model trong bộ nhớ, rồi cập nhật giá trị và ghi lại vào DB. Trong quá trình đó, field được thêm của code mới sẽ bị mất.
Trong trường hợp này, giải pháp đó là code cũ phải luôn giữ các field mới nguyên vẹn, mặc dù ta không dùng tới nó. Tùy vào từng ngôn ngữ lập trình và thư viện khác nhau, sẽ có những cách xử lý khác nhau để thực hiện giải pháp đó. Ở phần tiếp theo, ta sẽ minh họa với Java và Jackson ObjectMapper.
Dependency
Thêm dòng sau vào trong file pom.xml:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.3</version>
</dependency>
Tìm các phiên bản mới nhất của thư viện jackson-databind
tại đây.
Chương trình minh họa:
Ta sẽ tạo 2 model riêng: 1 cái tượng trưng cho model cũ, còn model mới sẽ extends từ cái cũ
class OldUser {
private String userName;
private Long favoriteNumber;
private List<String> interests;
// getters and setters...
}
class User extends OldUser {
private String photoURL;
private String foo;
// getters and setters...
}
Chương trình sẽ tái hiện lại quá trình được minh họa bên trên:
public static void main(String[] args) throws IOException {
final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
User user = new User();
user.setUserName("Martin");
user.setFavoriteNumber(1337L);
user.setInterests(Collections.singletonList("hacking"));
user.setPhotoURL("http://...");
// Data written by new version of code
String newVersionJSON = OBJECT_MAPPER.writeValueAsString(user);
System.out.println("before: " + newVersionJSON);
// Read and decode into old model object
OldUser oldUser = OBJECT_MAPPER.readValue(newVersionJSON, OldUser.class);
oldUser.setFavoriteNumber(42L); // Update
// Re-encode and Write back
System.out.println("after: " + OBJECT_MAPPER.writeValueAsString(oldUser));
}
Kết quả – giá trị photoURL
đã bị mất:
before: {"userName":"Martin","favoriteNumber":1337,"interests":["hacking"],"photoURL":"http://…"} after: {"userName":"Martin","favoriteNumber":42,"interests":["hacking"]}
Giải pháp xử lý:
Để giữ nguyên vẹn các field mới mà không xóa nó đi, ta sẽ giữ nó trong 1 field riêng tên là others
:
class OldUser {
...
@JsonIgnore
private final Map<String, Object> others = new HashMap<>();
@JsonAnyGetter
private Map<String, Object> any() {
return others;
}
@JsonAnySetter
private void set(String name, Object value) {
others.put(name, value);
}
}
Giờ thì ta chạy thử lại chương trình nhé:
before: {"userName":"Martin","favoriteNumber":1337,"interests":["hacking"],"photoURL":"http://…"} after: {"userName":"Martin","favoriteNumber":42,"interests":["hacking"],"photoURL":"http://…"}
Leave a Reply