Hướng dẫn giao dịch qua các dịch vụ nhỏ

1. Giới thiệu

Trong bài viết này, chúng ta sẽ thảo luận về các tùy chọn để triển khai giao dịch trên các dịch vụ nhỏ.

Chúng tôi cũng sẽ kiểm tra một số lựa chọn thay thế cho các giao dịch trong một kịch bản dịch vụ vi mô phân tán.

2. Tránh giao dịch qua các dịch vụ nhỏ

Một giao dịch phân tán là một quá trình rất phức tạp với rất nhiều bộ phận chuyển động có thể bị lỗi. Ngoài ra, nếu các phần này chạy trên các máy khác nhau hoặc thậm chí trong các trung tâm dữ liệu khác nhau, thì quá trình thực hiện giao dịch có thể trở nên rất lâu và không đáng tin cậy.

Điều này có thể ảnh hưởng nghiêm trọng đến trải nghiệm người dùng và băng thông tổng thể của hệ thống. Vì vậy, một trong những cách tốt nhất để giải quyết vấn đề của các giao dịch phân tán là tránh chúng hoàn toàn.

2.1. Ví dụ về các giao dịch yêu cầu kiến ​​trúc

Thông thường, một microservice được thiết kế sao cho độc lập và hữu ích. Nó sẽ có thể giải quyết một số nhiệm vụ kinh doanh nguyên tử.

Nếu chúng tôi có thể phân chia hệ thống của mình thành các dịch vụ nhỏ như vậy, thì rất có thể chúng tôi sẽ không cần thực hiện các giao dịch giữa chúng.

Ví dụ, chúng ta hãy xem xét một hệ thống nhắn tin quảng bá giữa những người dùng.

Dịch vụ vi mô của người dùng sẽ quan tâm đến hồ sơ người dùng (tạo người dùng mới, chỉnh sửa dữ liệu hồ sơ, v.v.) với lớp miền cơ bản sau:

@Entity public class User implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; @Basic private String name; @Basic private String surname; @Basic private Instant lastMessageTime; }

Tin nhắn microservice sẽ liên quan đến việc phát sóng. Nó đóng gói Thông điệp thực thể và mọi thứ xung quanh nó:

@Entity public class Message implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO) private long id; @Basic private long userId; @Basic private String contents; @Basic private Instant messageTimestamp; }

Mỗi microservice có cơ sở dữ liệu riêng. Lưu ý rằng chúng tôi không tham chiếu đến thực thể Người dùng từ Thông báo thực thể , vì các lớp người dùng không thể truy cập từ microservice thông báo . Chúng tôi chỉ đề cập đến người dùng bằng id.

Bây giờ thực thể Người dùng chứa trường lastMessageTime vì chúng tôi muốn hiển thị thông tin về thời gian hoạt động cuối cùng của người dùng trong hồ sơ của cô ấy.

Tuy nhiên, để thêm thông báo mới cho người dùng và cập nhật LastMessageTime của họ , bây giờ chúng ta phải triển khai một giao dịch trên các microservices.

2.2. Phương pháp tiếp cận thay thế không có giao dịch

Chúng tôi có thể thay đổi kiến ​​trúc microservice của mình và xóa trường lastMessageTime khỏi thực thể Người dùng .

Sau đó, chúng tôi có thể hiển thị thời gian này trong hồ sơ người dùng bằng cách đưa ra một yêu cầu riêng cho microservice thông báo và tìm giá trị messageTimestamp tối đa cho tất cả các thông báo của người dùng này.

Có thể, nếu microservice tin nhắn đang được tải cao hoặc thậm chí không hoạt động, chúng tôi sẽ không thể hiển thị thời gian của tin nhắn cuối cùng của người dùng trong hồ sơ của cô ấy.

Nhưng điều đó có thể dễ chấp nhận hơn việc không thực hiện một giao dịch phân tán để lưu thư chỉ vì microservice của người dùng không phản hồi kịp thời.

Tất nhiên, có nhiều tình huống phức tạp hơn khi chúng tôi phải thực hiện một quy trình kinh doanh trên nhiều dịch vụ nhỏ và chúng tôi không muốn để xảy ra sự mâu thuẫn giữa các dịch vụ nhỏ đó.

3. Giao thức cam kết hai giai đoạn

Giao thức cam kết hai pha (hoặc 2PC) là một cơ chế để thực hiện một giao dịch trên các thành phần phần mềm khác nhau (nhiều cơ sở dữ liệu, hàng đợi tin nhắn, v.v.)

3.1. Kiến trúc của 2PC

Một trong những người tham gia quan trọng trong giao dịch phân tán là điều phối viên giao dịch. Giao dịch phân tán bao gồm hai bước:

  • Giai đoạn chuẩn bị - trong giai đoạn này, tất cả những người tham gia giao dịch chuẩn bị cho cam kết và thông báo cho điều phối viên rằng họ đã sẵn sàng để hoàn thành giao dịch
  • Giai đoạn cam kết hoặc khôi phục - trong giai đoạn này, một cam kết hoặc một lệnh khôi phục được điều phối viên giao dịch đưa ra cho tất cả những người tham gia

Vấn đề với 2PC là nó khá chậm so với thời gian hoạt động của một microservice.

Điều phối giao dịch giữa các microservices, ngay cả khi chúng trên cùng một mạng, thực sự có thể làm chậm hệ thống , vì vậy cách tiếp cận này thường không được sử dụng trong trường hợp tải cao.

3.2. Tiêu chuẩn XA

Tiêu chuẩn XA là một đặc điểm kỹ thuật để thực hiện các giao dịch phân tán 2PC trên các tài nguyên hỗ trợ. Bất kỳ máy chủ ứng dụng nào tương thích với JTA (JBoss, GlassFish, v.v.) đều hỗ trợ nó ngay lập tức.

Ví dụ, các tài nguyên tham gia vào một giao dịch phân tán có thể là hai cơ sở dữ liệu của hai microservices khác nhau.

Tuy nhiên, để tận dụng cơ chế này, các tài nguyên phải được triển khai trên một nền tảng JTA duy nhất. Điều này không phải lúc nào cũng khả thi đối với kiến ​​trúc microservice.

3.3. Bản nháp tiêu chuẩn REST-AT

Một tiêu chuẩn được đề xuất khác là REST-AT đã được RedHat trải qua một số phát triển nhưng vẫn không thoát ra khỏi giai đoạn dự thảo. Tuy nhiên, nó được hỗ trợ bởi máy chủ ứng dụng WildFly.

Tiêu chuẩn này cho phép sử dụng máy chủ ứng dụng làm điều phối viên giao dịch với API REST cụ thể để tạo và tham gia các giao dịch phân tán.

Các dịch vụ web RESTful muốn tham gia vào giao dịch hai giai đoạn cũng phải hỗ trợ một API REST cụ thể.

Thật không may, để kết nối một giao dịch phân tán với các tài nguyên cục bộ của microservice, chúng tôi vẫn phải triển khai các tài nguyên này cho một nền tảng JTA duy nhất hoặc tự giải quyết một nhiệm vụ không nhỏ là viết cầu nối này.

4. Cuối cùng nhất quán và đền bù

By far, one of the most feasible models of handling consistency across microservices is eventual consistency.

This model doesn't enforce distributed ACID transactions across microservices. Instead, it proposes to use some mechanisms of ensuring that the system would be eventually consistent at some point in the future.

4.1. A Case for Eventual Consistency

For example, suppose we need to solve the following task:

  • register a user profile
  • do some automated background check that the user can actually access the system

The second task is to ensure, for example, that this user wasn't banned from our servers for some reason.

But it could take time, and we'd like to extract it to a separate microservice. It wouldn't be reasonable to keep the user waiting for so long just to know that she was registered successfully.

One way to solve it would be with a message-driven approach including compensation. Let's consider the following architecture:

  • the user microservice tasked with registering a user profile
  • the validation microservice tasked with doing a background check
  • the messaging platform that supports persistent queues

The messaging platform could ensure that the messages sent by the microservices are persisted. Then they would be delivered at a later time if the receiver weren't currently available

4.2. Happy Scenario

In this architecture, a happy scenario would be:

  • the user microservice registers a user, saving information about her in its local database
  • the user microservice marks this user with a flag. It could signify that this user hasn't yet been validated and doesn't have access to full system functionality
  • a confirmation of registration is sent to the user with a warning that not all functionality of the system is accessible right away
  • the user microservice sends a message to the validation microservice to do the background check of a user
  • the validation microservice runs the background check and sends a message to the user microservice with the results of the check
    • if the results are positive, the user microservice unblocks the user
    • if the results are negative, the user microservice deletes the user account

After we've gone through all these steps, the system should be in a consistent state. However, for some period of time, the user entity appeared to be in an incomplete state.

The last step, when the user microservice removes the invalid account, is a compensation phase.

4.3. Failure Scenarios

Now let's consider some failure scenarios:

  • if the validation microservice is not accessible, then the messaging platform with its persistent queue functionality ensures that the validation microservice would receive this message at some later time
  • suppose the messaging platform fails, then the user microservice tries to send the message again at some later time, for example, by scheduled batch-processing of all users that were not yet validated
  • if the validation microservice receives the message, validates the user but can't send the answer back due to the messaging platform failure, the validation microservice also retries sending the message at some later time
  • if one of the messages got lost, or some other failure happened, the user microservice finds all non-validated users by scheduled batch-processing and sends requests for validation again

Ngay cả khi một số thông báo được phát hành nhiều lần, điều này sẽ không ảnh hưởng đến tính nhất quán của dữ liệu trong cơ sở dữ liệu của microservices.

Bằng cách xem xét cẩn thận tất cả các trường hợp lỗi có thể xảy ra, chúng tôi có thể đảm bảo rằng hệ thống của chúng tôi sẽ đáp ứng các điều kiện về tính nhất quán cuối cùng. Đồng thời, chúng tôi sẽ không cần phải xử lý các giao dịch phân tán tốn kém.

Nhưng chúng ta phải lưu ý rằng đảm bảo tính nhất quán cuối cùng là một nhiệm vụ phức tạp. Nó không có một giải pháp duy nhất cho mọi trường hợp.

5. Kết luận

Trong bài viết này, chúng tôi đã thảo luận về một số cơ chế thực hiện giao dịch trên các dịch vụ nhỏ.

Và, chúng tôi cũng đã khám phá một số lựa chọn thay thế để thực hiện kiểu giao dịch này ngay từ đầu.