Backend

99 Ngày Spring — Ngày 75: Giao tiếp giữa service

SSite Admin
9 tháng 10, 2026 7 phút đọc 9 lượt xem
99 Ngày Spring — Ngày 75: Giao tiếp giữa service

Ba ngày qua ta có cấu hình tập trung, danh bạ và cửa vào. Giờ tới việc xảy ra nhiều nhất trong một hệ microservices: service gọi service. Câu hỏi không phải “dùng thư viện nào” mà là làm sao để lời gọi có timeout, tìm đúng instance, thử lại khi hợp lý và không thử lại khi nguy hiểm. Bài này so sánh ba cách viết client trên Spring Boot 3.5 (RestClient, HTTP interface, OpenFeign) rồi bàn về retry với Spring Cloud LoadBalancer. Java 21 và Spring Cloud 2025.0.

Giao tiếp giữa service: RestClient cân bằng tải, HTTP interface, OpenFeign và retry an toàn

1. Đồng bộ hay bất đồng bộ trước đã

  • Gọi đồng bộ (HTTP) khi bạn cần câu trả lời ngay để tiếp tục: kiểm tra tồn kho trước khi tạo đơn, lấy giá hiện tại. Người gọi chờ, và chịu độ trễ cộng dồn của cả chuỗi.

  • Nhắn tin bất đồng bộ (Kafka, RabbitMQ; Ngày 78–79) khi việc có thể xảy ra sau: gửi email, cập nhật báo cáo, đồng bộ sang hệ thống khác. Người gọi không chờ, và service nhận có thể tạm chết mà không kéo theo người gửi.

  • Chuỗi gọi đồng bộ dài (A gọi B gọi C gọi D) nhân xác suất lỗi và cộng độ trễ; nếu thấy chuỗi như vậy, trước khi tối ưu client hãy hỏi có bước nào nên thành sự kiện không.

2. RestClient cân bằng tải: nền của mọi thứ

package vn.motdev.order;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

import java.time.Duration;

@Configuration
class PaymentClientConfig {
    @Bean
    @LoadBalanced
    RestClient.Builder loadBalancedBuilder() {
        var factory = new JdkClientHttpRequestFactory();   // HttpClient của JDK 21 phía dưới
        factory.setReadTimeout(Duration.ofSeconds(2));       // KHÔNG có timeout = chờ vô hạn khi đối tác treo
        return RestClient.builder().requestFactory(factory);
    }

    @Bean
    RestClient paymentClient(RestClient.Builder builder) {
        return builder.baseUrl("http://payment-service").build();   // tên trong danh bạ (Ngày 73)
    }
}
  • RestClient (Spring 6.1) là API đồng bộ hiện đại thay RestTemplate, đã học ở Ngày 66. @LoadBalanced trên Builder thêm interceptor đổi tên service thành địa chỉ từ danh bạ (Ngày 73).

  • Timeout là bắt buộc, không phải tùy chọn: JdkClientHttpRequestFactory cho đặt read timeout; connect timeout đặt trên HttpClient của JDK truyền vào factory. Không có timeout, một đối tác treo giữ luồng của bạn mãi mãi, và với thread pool cố định, service của bạn chết theo.

  • Một RestClient bean cho mỗi đối tác với baseUrl riêng: timeout, header xác thực và interceptor khác nhau theo đối tác, và test dễ thay thế.

3. HTTP interface: hợp đồng dưới dạng interface

package vn.motdev.order;

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;

// HTTP interface (Spring Framework 6): hợp đồng gọi xa dưới dạng interface, không cần thư viện ngoài
@HttpExchange("/api/payments")
public interface PaymentApi {
    @GetExchange("/{id}")
    PaymentView get(@PathVariable String id);

    @PostExchange
    PaymentView create(@RequestBody CreatePayment body);
}

record PaymentView(String id, String status, long amount) {}
record CreatePayment(String orderId, long amount, String idempotencyKey) {}
package vn.motdev.order;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@Configuration
class PaymentApiConfig {
    @Bean
    PaymentApi paymentApi(RestClient paymentClient) {
        // Sinh implementation của interface, chạy trên RestClient đã cân bằng tải + có timeout
        return HttpServiceProxyFactory
                .builderFor(RestClientAdapter.create(paymentClient))
                .build()
                .createClient(PaymentApi.class);
    }
}

@org.springframework.stereotype.Service
class CheckoutService {
    private final PaymentApi payments;
    CheckoutService(PaymentApi payments) { this.payments = payments; }

    PaymentView pay(String orderId, long amount) {
        // Khóa idempotency: gọi lại (retry) không tạo giao dịch thứ hai
        return payments.create(new CreatePayment(orderId, amount, "order-" + orderId));
    }
}
  • Interface với @HttpExchange mô tả API xa; HttpServiceProxyFactory sinh implementation chạy trên RestClient bạn đưa vào, nên mọi cấu hình cân bằng tải, timeout, interceptor đều được kế thừa. Không thêm thư viện ngoài.

  • Lợi ích lớn nhất là hợp đồng nằm trong code: đổi đường dẫn hay kiểu trả về là lỗi biên dịch ở người gọi, và service cung cấp có thể publish interface này trong một module payment-api để người dùng tái sử dụng.

  • Trong ví dụ, CreatePayment mang idempotencyKey: phía payment lưu khóa và trả lại kết quả cũ nếu thấy khóa trùng. Đây là điều kiện để mục 5 có thể retry POST an toàn.

  • Spring Boot 4 thêm tự động cấu hình cho HTTP interface (@ImportHttpServices); trên Boot 3.5 ta khai báo factory bằng tay như trên.

// Lựa chọn khác: OpenFeign — spring-cloud-starter-openfeign, @EnableFeignClients trên lớp @SpringBootApplication
@org.springframework.cloud.openfeign.FeignClient(name = "payment-service", path = "/api/payments")
interface PaymentFeign {
    @org.springframework.web.bind.annotation.GetMapping("/{id}")
    PaymentView get(@org.springframework.web.bind.annotation.PathVariable String id);
}
// Cùng ý tưởng "interface = hợp đồng", tích hợp sẵn LoadBalancer và CircuitBreaker.
// Spring Cloud OpenFeign ở trạng thái feature-complete: chỉ sửa lỗi, không thêm tính năng;
// Spring khuyến nghị HTTP interface + RestClient cho dự án mới.

4. Chọn cách nào

  • RestClient trực tiếp: một hai lời gọi, cần kiểm soát từng chi tiết của request, hoặc API xa không ổn định để làm interface.

  • HTTP interface: mặc định cho dự án mới. Cùng mô hình interface như Feign, nhưng nằm trong Spring Framework, chạy trên client bạn tự cấu hình, hỗ trợ cả RestClient, WebClient và RestTemplate.

  • OpenFeign: đội đã dùng quen, có sẵn bộ interceptor/decoder tùy chỉnh, hoặc dựa vào tích hợp Feign với CircuitBreaker. Nhớ rằng Spring Cloud OpenFeign chỉ nhận sửa lỗi; đừng chọn nó cho phần mới chỉ vì thói quen.

  • Dù chọn gì, ba thứ không đổi: timeout tường minh, gọi bằng tên qua danh bạ, và một client riêng cho mỗi đối tác.

5. Retry: thử lại đúng cách

# order-service/src/main/resources/application.yml
spring:
  cloud:
    loadbalancer:
      retry:
        enabled: true                          # cần spring-retry trên classpath
        max-retries-on-same-service-instance: 0
        max-retries-on-next-service-instance: 2   # đổi sang instance KHÁC, tối đa 2 lần
        retry-on-all-operations: false            # mặc định: chỉ GET — POST không tự retry
        retryable-status-codes: 502,503,504
        backoff:
          enabled: true
          min-backoff: 100ms
          max-backoff: 1s
      health-check:
        interval: 10s                             # loại instance đang DOWN khỏi danh sách chọn
  • Spring Cloud LoadBalancer có thể retry sang instance khác khi kết nối thất bại hoặc nhận mã 502/503/504: đúng loại lỗi mà retry có ích, vì instance bên cạnh có thể đang khỏe. Với RestClient và RestTemplate được @LoadBalanced, cấu hình trên tự bật RetryLoadBalancerInterceptor.

  • Mặc định chỉ retry GET (retry-on-all-operations: false). POST tạo thanh toán mà retry mù quáng là tạo hai giao dịch: request đầu có thể đã thành công nhưng câu trả lời bị mất trên đường về. Chỉ bật retry cho thao tác ghi khi phía nhận idempotent nhờ khóa như mục 3.

  • Backoff có jitter để nhiều client không đồng loạt thử lại cùng lúc (retry storm). Giới hạn tổng số lần thử: 3 lần × 2 giây timeout là 6 giây người dùng phải chờ; retry không thay được timeout hợp lý.

  • Retry giải quyết lỗi thoáng qua. Lỗi kéo dài (service chết hẳn, quá tải) cần ngừng gọi một thời gian để nó hồi phục: đó là circuit breaker và bulkhead của Resilience4j, Ngày 76. Retry chồng lên circuit breaker chồng lên timeout là bộ ba tiêu chuẩn.

  • Lỗi 4xx (400, 404, 422) không bao giờ retry: request sai thì gửi lại vẫn sai. Ngày 69 đã chuẩn hóa ProblemDetail; client nên đọc nó để phân biệt lỗi của mình với lỗi của đối tác.

6. Bài tập thực hành

  • Cấu hình PaymentApi qua RestClient cân bằng tải, chạy hai instance payment và log server.port phía nhận để thấy round-robin qua HTTP interface.

  • Cho một instance payment ngủ 10 giây; với read timeout 2 giây, đo thời gian người gọi chờ khi có retry sang instance khác và khi không.

  • Bật retry-on-all-operations: true chưa có idempotency key, gửi POST và làm rơi câu trả lời (tắt mạng giữa chừng bằng Toxiproxy hoặc ngủ trước khi trả về): đếm số giao dịch được tạo. Thêm khóa idempotency và lặp lại.

  • Chuyển một client Feign hiện có sang HTTP interface; so sánh lượng cấu hình và số dependency.

Tóm lại: chọn đồng bộ hay bất đồng bộ trước khi chọn thư viện; @LoadBalanced RestClient với timeout tường minh là nền; HTTP interface đưa hợp đồng vào code và là mặc định cho dự án mới, OpenFeign cho đội đã dùng; retry chỉ sang instance khác, chỉ cho lỗi thoáng qua, và chỉ cho thao tác idempotent. Ngày 76 hoàn thiện bộ ba: circuit breaker, bulkhead và rate limiter với Resilience4j.

Tài liệu đối chiếu

RestClient: https://docs.spring.io/spring-framework/reference/integration/rest-clients.html ; HTTP interface: https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-interface ; Spring Cloud LoadBalancer retry: https://docs.spring.io/spring-cloud-commons/reference/spring-cloud-commons/loadbalancer.html ; Spring Cloud OpenFeign: https://docs.spring.io/spring-cloud-openfeign/reference/

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Spring — Ngày 76: Circuit Breaker với Resilience4j

CLOSED, OPEN, HALF_OPEN, fallback, bulkhead và rate limiter để giới hạn lỗi dây chuyền.

10 thg 10, 20262 phút5
99 Ngày Java — Ngày 76: Soft, Weak và Phantom Reference

Bốn mức tham chiếu, WeakHashMap, ReferenceQueue và cách chọn cache có giới hạn trên Java 21.

10 thg 10, 20262 phút6
K

Học System Design qua 21 bài: yêu cầu, capacity, database, cache, hệ phân tán và 6 bài thực hành, kèm bài tập, sơ đồ luồng và mục lục đầy đủ.

10 thg 10, 20266 phút3