الفصل 5: الاتصال بين الخدمات
🎯 أهداف الفصل
- فهم أوضاع الاتصال المتزامن مقابل غير المتزامن
- تنفيذ استدعاءات REST باستخدام OpenFeign
- تطبيق أنماط المرونة باستخدام Resilience4j
- إتقان Circuit Breaker، Retry، Rate Limiter
- تنفيذ fallbacks فعالة
1. أوضاع الاتصال
1.1 الاتصال المتزامن
الخصائص:
- العميل ينتظر الرد
- سهل التنفيذ
- اقتران زمني (اعتماد على التوافر)
- زمن استجابة متراكم
1.2 الاتصال غير المتزامن
الخصائص:
- إرسال ونسيان
- فك الارتباط الزمني
- أكثر مرونة
- أكثر تعقيدًا (اتساق نهائي)
1.3 متى تستخدم ماذا؟
| حالة الاستخدام | متزامن | غير متزامن |
|---|---|---|
| التحقق من المستخدم | ✅ | |
| الحصول على تفاصيل المنتج | ✅ | |
| إنشاء طلب | ✅ | |
| إرسال إشعار | ✅ | |
| تحديث التحليلات | ✅ | |
| مزامنة البيانات | ✅ |
2. OpenFeign - عميل HTTP إعلاني
2.1 ما هو Feign؟
Feign (الآن OpenFeign) هو عميل HTTP إعلاني يسمح بتعريف استدعاءات REST عبر واجهات Java.
// بدون Feign (RestTemplate)
String url = "http://user-service/api/users/" + userId;
User user = restTemplate.getForObject(url, User.class);
// مع Feign (إعلاني)
User user = userClient.getUserById(userId);
2.2 تبعيات Maven
<dependencies>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Eureka Client (للاكتشاف) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Resilience4j -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
</dependencies>
2.3 تفعيل Feign
package com.example.orderservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients // تفعيل Feign
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
2.4 تعريف عميل Feign
package com.example.orderservice.client;
import com.example.orderservice.dto.UserDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@FeignClient(name = "user-service") // اسم الخدمة في Eureka
public interface UserClient {
@GetMapping("/api/users/{id}")
UserDto getUserById(@PathVariable("id") Long id);
@GetMapping("/api/users")
List<UserDto> getAllUsers();
@GetMapping("/api/users/{id}/exists")
Boolean userExists(@PathVariable("id") Long id);
}
2.5 DTO
package com.example.orderservice.dto;
public class UserDto {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;
// Constructors
public UserDto() {}
public UserDto(Long id, String username, String email,
String firstName, String lastName) {
this.id = id;
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
2.6 الاستخدام
package com.example.orderservice.service;
import com.example.orderservice.client.UserClient;
import com.example.orderservice.client.ProductClient;
import com.example.orderservice.dto.UserDto;
import com.example.orderservice.dto.ProductDto;
import com.example.orderservice.model.Order;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final UserClient userClient;
private final ProductClient productClient;
private final OrderRepository orderRepository;
public OrderService(UserClient userClient,
ProductClient productClient,
OrderRepository orderRepository) {
this.userClient = userClient;
this.productClient = productClient;
this.orderRepository = orderRepository;
}
public Order createOrder(Long userId, Long productId, int quantity) {
// استدعاء متزامن لخدمة المستخدم
UserDto user = userClient.getUserById(userId);
if (user == null) {
throw new RuntimeException("User not found: " + userId);
}
// استدعاء متزامن لخدمة المنتج
ProductDto product = productClient.getProductById(productId);
if (product == null) {
throw new RuntimeException("Product not found: " + productId);
}
// إنشاء الطلب
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
order.setTotalPrice(product.getPrice() * quantity);
order.setUserEmail(user.getEmail());
return orderRepository.save(order);
}
}
2.7 تكوين Feign
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
loggerLevel: BASIC
user-service:
connectTimeout: 3000
readTimeout: 5000
loggerLevel: FULL
# Logging Feign
logging:
level:
com.example.orderservice.client: DEBUG
2.8 مستويات تسجيل Feign
| المستوى | الوصف |
|---|---|
NONE | لا يوجد تسجيل |
BASIC | الطريقة، URL، الحالة، الوقت |
HEADERS | + الرؤوس |
FULL | + جسم الطلب/الاستجابة |
3. Resilience4j - أنماط المرونة
3.1 لماذا المرونة؟
3.2 التبعيات
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- إذا كنت تريد التعليقات التوضيحية @CircuitBreaker، @Retry، إلخ. -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<!-- AOP للتعليقات التوضيحية -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4. Circuit Breaker Pattern
4.1 حالات الدائرة
4.2 تكوين YAML
resilience4j:
circuitbreaker:
configs:
default:
# حجم النافذة المنزلقة
slidingWindowSize: 10
# نوع النافذة (COUNT_BASED أو TIME_BASED)
slidingWindowType: COUNT_BASED
# الحد الأدنى لعدد المكالمات قبل الحساب
minimumNumberOfCalls: 5
# عتبة الفشل لفتح (%)
failureRateThreshold: 50
# مدة في حالة OPEN (ms)
waitDurationInOpenState: 10000
# المكالمات المسموح بها في HALF_OPEN
permittedNumberOfCallsInHalfOpenState: 3
# الانتقال التلقائي من OPEN إلى HALF_OPEN
automaticTransitionFromOpenToHalfOpenEnabled: true
# الاستثناءات التي تعتبر فشلًا
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.FeignException
# الاستثناءات التي يتم تجاهلها
ignoreExceptions:
- com.example.BusinessException
instances:
userService:
baseConfig: default
productService:
baseConfig: default
failureRateThreshold: 30
4.3 مع التعليقات التوضيحية
package com.example.orderservice.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
@Service
public class UserServiceClient {
private final UserClient userClient;
public UserServiceClient(UserClient userClient) {
this.userClient = userClient;
}
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}
// Fallback - يجب أن يكون له نفس التوقيع + استثناء
public UserDto getUserFallback(Long userId, Exception ex) {
// سجل الخطأ
log.warn("Circuit breaker triggered for user {}. Error: {}",
userId, ex.getMessage());
// إرجاع قيمة افتراضية أو مخزنة
return new UserDto(userId, "unknown", "unknown@example.com",
"Unknown", "User");
}
}
4.4 مع Feign (Fallback Factory)
package com.example.orderservice.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(
name = "user-service",
fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient {
@GetMapping("/api/users/{id}")
UserDto getUserById(@PathVariable("id") Long id);
@GetMapping("/api/users/{id}/exists")
Boolean userExists(@PathVariable("id") Long id);
}
package com.example.orderservice.client;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
private static final Logger log = LoggerFactory.getLogger(UserClientFallbackFactory.class);
@Override
public UserClient create(Throwable cause) {
log.error("UserClient fallback triggered. Cause: {}", cause.getMessage());
return new UserClient() {
@Override
public UserDto getUserById(Long id) {
return new UserDto(id, "fallback-user", "fallback@example.com",
"Fallback", "User");
}
@Override
public Boolean userExists(Long id) {
return false; // متشائم في حالة الخطأ
}
};
}
}
تفعيل fallbacks Feign:
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
5. Retry Pattern
5.1 التكوين
resilience4j:
retry:
configs:
default:
maxAttempts: 3
waitDuration: 1000
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.RetryableException
ignoreExceptions:
- com.example.BusinessException
instances:
userService:
baseConfig: default
maxAttempts: 5
5.2 مع التعليقات التوضيحية
@Service
public class ProductServiceClient {
@Retry(name = "productService", fallbackMethod = "getProductFallback")
public ProductDto getProduct(Long productId) {
return productClient.getProductById(productId);
}
public ProductDto getProductFallback(Long productId, Exception ex) {
log.warn("Retry exhausted for product {}. Error: {}",
productId, ex.getMessage());
throw new ServiceUnavailableException("Product service unavailable");
}
}
5.3 دمج Retry و Circuit Breaker
@Service
public class UserServiceClient {
// الترتيب مهم! Retry ثم CircuitBreaker
@Retry(name = "userService")
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}
public UserDto getUserFallback(Long userId, Exception ex) {
return getCachedUser(userId);
}
}
ترتيب التنفيذ:
- الاستدعاء الأولي
- إذا فشل → Retry (حتى maxAttempts)
- إذا فشلت جميع المحاولات → Circuit Breaker
- إذا كان Circuit OPEN → Fallback فوري
6. Rate Limiter Pattern
6.1 التكوين
resilience4j:
ratelimiter:
configs:
default:
# عدد الأذونات لكل فترة
limitForPeriod: 10
# مدة الفترة
limitRefreshPeriod: 1s
# مهلة الانتظار للحصول على إذن
timeoutDuration: 500ms
instances:
userService:
baseConfig: default
limitForPeriod: 100
limitRefreshPeriod: 1s
6.2 مع التعليقات التوضيحية
@Service
public class ExternalApiClient {
@RateLimiter(name = "externalApi", fallbackMethod = "rateLimitFallback")
public Response callExternalApi(Request request) {
return externalClient.call(request);
}
public Response rateLimitFallback(Request request, RequestNotPermitted ex) {
log.warn("Rate limit exceeded");
throw new TooManyRequestsException("Rate limit exceeded. Try again later.");
}
}
7. Bulkhead Pattern
7.1 المفهوم
يقوم Bulkhead بعزل الموارد لمنع خدمة بطيئة من حظر الكل.
7.2 التكوين
resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 10
maxWaitDuration: 500ms
instances:
userService:
baseConfig: default
maxConcurrentCalls: 5
thread-pool-bulkhead:
configs:
default:
maxThreadPoolSize: 10
coreThreadPoolSize: 5
queueCapacity: 20
instances:
productService:
baseConfig: default
7.3 مع التعليقات التوضيحية
@Service
public class OrderService {
@Bulkhead(name = "userService", type = Bulkhead.Type.SEMAPHORE)
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}
@Bulkhead(name = "productService", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<ProductDto> getProductAsync(Long productId) {
return CompletableFuture.supplyAsync(() ->
productClient.getProductById(productId)
);
}
}
8. Time Limiter Pattern
8.1 التكوين
resilience4j:
timelimiter:
configs:
default:
timeoutDuration: 5s
cancelRunningFuture: true
instances:
userService:
baseConfig: default
timeoutDuration: 3s
8.2 مع التعليقات التوضيحية
@Service
public class SlowServiceClient {
@TimeLimiter(name = "slowService", fallbackMethod = "timeoutFallback")
public CompletableFuture<Response> callSlowService() {
return CompletableFuture.supplyAsync(() -> {
// استدعاء بطيء محتمل
return slowClient.call();
});
}
public CompletableFuture<Response> timeoutFallback(TimeoutException ex) {
return CompletableFuture.completedFuture(Response.timeout());
}
}
9. دمج جميع الأنماط
9.1 التكوين الكامل
resilience4j:
circuitbreaker:
instances:
userService:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 3
retry:
instances:
userService:
maxAttempts: 3
waitDuration: 500ms
enableExponentialBackoff: true
ratelimiter:
instances:
userService:
limitForPeriod: 100
limitRefreshPeriod: 1s
bulkhead:
instances:
userService:
maxConcurrentCalls: 10
timelimiter:
instances:
userService:
timeoutDuration: 3s
9.2 خدمة مع جميع الأنماط
@Service
@Slf4j
public class ResilientUserService {
private final UserClient userClient;
public ResilientUserService(UserClient userClient) {
this.userClient = userClient;
}
// ترتيب التنفيذ: Bulkhead → TimeLimiter → RateLimiter → CircuitBreaker → Retry
@Bulkhead(name = "userService")
@TimeLimiter(name = "userService")
@RateLimiter(name = "userService")
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
@Retry(name = "userService")
public CompletableFuture<UserDto> getUser(Long userId) {
return CompletableFuture.supplyAsync(() -> {
log.info("Calling user service for user: {}", userId);
return userClient.getUserById(userId);
});
}
public CompletableFuture<UserDto> getUserFallback(Long userId, Exception ex) {
log.warn("Fallback triggered for user: {}. Error: {}",
userId, ex.getMessage());
return CompletableFuture.completedFuture(
new UserDto(userId, "fallback", "fallback@example.com", "Fallback", "User")
);
}
}
10. مراقبة Resilience4j
10.1 نقاط النهاية Actuator
management:
endpoints:
web:
exposure:
include: health,circuitbreakers,circuitbreakerevents,retries,ratelimiters
endpoint:
health:
show-details: always
health:
circuitbreakers:
enabled: true
ratelimiters:
enabled: true
10.2 نقاط النهاية المتاحة
# حالة circuit breakers
curl http://localhost:8083/actuator/circuitbreakers
# أحداث circuit breakers
curl http://localhost:8083/actuator/circuitbreakerevents
# مقاييس Prometheus
curl http://localhost:8083/actuator/prometheus | grep resilience4j
10.3 مثال على الاستجابة
{
"circuitBreakers": {
"userService": {
"failureRate": "20.0%",
"slowCallRate": "0.0%",
"state": "CLOSED",
"bufferedCalls": 10,
"failedCalls": 2,
"slowCalls": 0,
"notPermittedCalls": 0
}
}
}
11. ملخص
جدول ملخص
| النمط | المشكلة المحلولة | متى تستخدمه |
|---|---|---|
| Circuit Breaker | سلسلة من الأعطال | استدعاءات خارجية غير مستقرة |
| Retry | أخطاء مؤقتة | مهلات الشبكة، 503 |
| Rate Limiter | التحميل الزائد | واجهات برمجة التطبيقات الخارجية مع الحصص |
| Bulkhead | استنزاف الموارد | عزل الخدمات البطيئة |
| Time Limiter | استدعاءات محظورة | خدمات بطيئة محتملة |
ترتيب الأنماط
Request → Bulkhead → TimeLimiter → RateLimiter → CircuitBreaker → Retry → Service
التكوين الموصى به للإنتاج
resilience4j:
circuitbreaker:
instances:
default:
slidingWindowSize: 100
failureRateThreshold: 50
waitDurationInOpenState: 30000
permittedNumberOfCallsInHalfOpenState: 10
retry:
instances:
default:
maxAttempts: 3
waitDuration: 1000ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
ratelimiter:
instances:
default:
limitForPeriod: 1000
limitRefreshPeriod: 1s
12. اختبار التحقق
السؤال 1
ما الفرق بين @FeignClient(fallback) و @FeignClient(fallbackFactory)؟
عرض الإجابة
- fallback: فئة ثابتة، لا يمكن الوصول إلى الاستثناء
- fallbackFactory: يتلقى الاستثناء كمعامل، يسمح بتسجيل أو تغيير fallback حسب الخطأ
السؤال 2
ما هو ترتيب تنفيذ Retry و CircuitBreaker؟
عرض الإجابة
Retry يتم تنفيذه أولاً (أقرب إلى الخدمة)، ثم CircuitBreaker.
إذا فشلت جميع المحاولات، فإن CircuitBreaker يسجل فشلًا. إذا كان CircuitBreaker مفتوحًا، فلن يتم حتى محاولة retries.