Microservices là gì?
Microservices là một kiến trúc phần mềm chia ứng dụng thành các service nhỏ, độc lập, có thể deploy và scale riêng biệt. Mỗi service tập trung vào một chức năng nghiệp vụ cụ thể và giao tiếp với nhau qua API.
Ưu điểm của Microservices
- Độc lập deploy: Cập nhật một service mà không ảnh hưởng các service khác
- Công nghệ đa dạng: Mỗi service có thể dùng ngôn ngữ/database riêng
- Scale linh hoạt: Scale từng service theo nhu cầu thực tế
- Fault isolation: Lỗi một service không làm sập toàn bộ hệ thống
- Phát triển song song: Các team có thể làm việc độc lập
Thách thức
- Phức tạp hơn về mặt vận hành
- Cần xử lý distributed transactions
- Network latency giữa các services
- Debugging và monitoring khó hơn
1. Cấu trúc Project
microservices-demo/
├── api-gateway/
│ ├── src/
│ ├── pom.xml
│ └── Dockerfile
├── user-service/
│ ├── src/
│ ├── pom.xml
│ └── Dockerfile
├── order-service/
│ ├── src/
│ ├── pom.xml
│ └── Dockerfile
├── product-service/
│ ├── src/
│ ├── pom.xml
│ └── Dockerfile
├── discovery-server/
│ ├── src/
│ ├── pom.xml
│ └── Dockerfile
├── docker-compose.yml
└── README.md
2. Discovery Server với Eureka
Service Discovery cho phép các service tự động tìm thấy nhau mà không cần hardcode địa chỉ IP.
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
Application Class
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
public static void main(String[] args) {
SpringApplication.run(DiscoveryServerApplication.class, args);
}
}
application.yml
server:
port: 8761
spring:
application:
name: discovery-server
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
3. User Service - Service mẫu
Entity
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String fullName;
@Column
private String phone;
@Enumerated(EnumType.STRING)
private UserRole role = UserRole.USER;
@Column(updatable = false)
@CreationTimestamp
private LocalDateTime createdAt;
@UpdateTimestamp
private LocalDateTime updatedAt;
}
Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
@Query("SELECT u FROM User u WHERE u.role = :role")
List<User> findByRole(@Param("role") UserRole role);
}
Service Layer
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserMapper userMapper;
public UserResponse createUser(CreateUserRequest request) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateEmailException("Email đã tồn tại");
}
User user = User.builder()
.email(request.getEmail())
.password(passwordEncoder.encode(request.getPassword()))
.fullName(request.getFullName())
.phone(request.getPhone())
.role(UserRole.USER)
.build();
User savedUser = userRepository.save(user);
log.info("Created new user with id: {}", savedUser.getId());
return userMapper.toResponse(savedUser);
}
public UserResponse getUserById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User không tồn tại"));
return userMapper.toResponse(user);
}
public Page<UserResponse> getAllUsers(Pageable pageable) {
return userRepository.findAll(pageable)
.map(userMapper::toResponse);
}
@Transactional
public UserResponse updateUser(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException("User không tồn tại"));
if (request.getFullName() != null) {
user.setFullName(request.getFullName());
}
if (request.getPhone() != null) {
user.setPhone(request.getPhone());
}
User updatedUser = userRepository.save(user);
log.info("Updated user with id: {}", id);
return userMapper.toResponse(updatedUser);
}
@Transactional
public void deleteUser(Long id) {
if (!userRepository.existsById(id)) {
throw new UserNotFoundException("User không tồn tại");
}
userRepository.deleteById(id);
log.info("Deleted user with id: {}", id);
}
}
Controller
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
@Tag(name = "User API", description = "Quản lý người dùng")
public class UserController {
private final UserService userService;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "Tạo user mới")
public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) {
return userService.createUser(request);
}
@GetMapping("/{id}")
@Operation(summary = "Lấy thông tin user theo ID")
public UserResponse getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
@GetMapping
@Operation(summary = "Lấy danh sách users có phân trang")
public Page<UserResponse> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "id") String sortBy
) {
return userService.getAllUsers(PageRequest.of(page, size, Sort.by(sortBy)));
}
@PutMapping("/{id}")
@Operation(summary = "Cập nhật user")
public UserResponse updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request
) {
return userService.updateUser(id, request);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Xóa user")
public void deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
}
}
4. API Gateway với Spring Cloud Gateway
API Gateway là điểm vào duy nhất cho client, routing requests đến các services tương ứng.
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
application.yml
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- RewritePath=/api/users/(?<segment>.*), /api/users/${segment}
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- RewritePath=/api/orders/(?<segment>.*), /api/orders/${segment}
eureka:
client:
service-url:
defaultZone: http://discovery-server:8761/eureka/
5. Dockerfile tối ưu
Multi-stage Build
# Build stage
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests
# Runtime stage
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# Tạo user non-root
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
# Copy JAR file
COPY --from=build /app/target/*.jar app.jar
# Chuyển sang user non-root
USER appuser
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
# JVM tuning cho container
ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
6. Docker Compose
docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: mysql
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: microservices_db
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
networks:
- microservices-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
container_name: redis
ports:
- "6379:6379"
networks:
- microservices-network
discovery-server:
build: ./discovery-server
container_name: discovery-server
ports:
- "8761:8761"
networks:
- microservices-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8761/actuator/health"]
interval: 10s
timeout: 5s
retries: 5
api-gateway:
build: ./api-gateway
container_name: api-gateway
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=docker
depends_on:
discovery-server:
condition: service_healthy
networks:
- microservices-network
user-service:
build: ./user-service
environment:
- SPRING_PROFILES_ACTIVE=docker
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/microservices_db
- SPRING_DATASOURCE_USERNAME=root
- SPRING_DATASOURCE_PASSWORD=root123
depends_on:
mysql:
condition: service_healthy
discovery-server:
condition: service_healthy
networks:
- microservices-network
deploy:
replicas: 2
order-service:
build: ./order-service
environment:
- SPRING_PROFILES_ACTIVE=docker
depends_on:
discovery-server:
condition: service_healthy
user-service:
condition: service_started
networks:
- microservices-network
networks:
microservices-network:
driver: bridge
volumes:
mysql_data:
7. Inter-Service Communication
Sử dụng OpenFeign
@FeignClient(name = "user-service", fallback = UserClientFallback.class)
public interface UserClient {
@GetMapping("/api/users/{id}")
UserResponse getUserById(@PathVariable("id") Long id);
@GetMapping("/api/users")
List<UserResponse> getAllUsers();
}
@Component
@Slf4j
public class UserClientFallback implements UserClient {
@Override
public UserResponse getUserById(Long id) {
log.warn("Fallback: Cannot get user with id: {}", id);
return UserResponse.builder()
.id(id)
.fullName("Unknown User")
.build();
}
@Override
public List<UserResponse> getAllUsers() {
log.warn("Fallback: Cannot get users list");
return Collections.emptyList();
}
}
Sử dụng trong Order Service
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderService {
private final OrderRepository orderRepository;
private final UserClient userClient;
public OrderResponse createOrder(CreateOrderRequest request) {
// Verify user exists
UserResponse user = userClient.getUserById(request.getUserId());
Order order = Order.builder()
.userId(request.getUserId())
.totalAmount(request.getTotalAmount())
.status(OrderStatus.PENDING)
.build();
Order savedOrder = orderRepository.save(order);
log.info("Created order {} for user {}", savedOrder.getId(), user.getFullName());
return orderMapper.toResponse(savedOrder);
}
}
8. Configuration Management
application.yml cho từng profile
application.yml (default)
spring:
application:
name: user-service
jpa:
hibernate:
ddl-auto: validate
show-sql: false
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
application-docker.yml
spring:
datasource:
url: jdbc:mysql://mysql:3306/microservices_db
username: root
password: root123
jpa:
show-sql: true
eureka:
client:
service-url:
defaultZone: http://discovery-server:8761/eureka/
instance:
prefer-ip-address: true
9. Chạy và Test
Build và chạy
# Build tất cả services
docker-compose build
# Khởi động
docker-compose up -d
# Xem logs
docker-compose logs -f user-service
# Scale user-service
docker-compose up -d --scale user-service=3
# Kiểm tra services đang chạy
docker-compose ps
# Dừng
docker-compose down
# Dừng và xóa volumes
docker-compose down -v
Test API qua Gateway
# Tạo user
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "password123",
"fullName": "Test User",
"phone": "0123456789"
}'
# Lấy user theo ID
curl http://localhost:8080/api/users/1
# Lấy danh sách users
curl "http://localhost:8080/api/users?page=0&size=10"
# Cập nhật user
curl -X PUT http://localhost:8080/api/users/1 \
-H "Content-Type: application/json" \
-d '{
"fullName": "Updated Name",
"phone": "0987654321"
}'
# Xóa user
curl -X DELETE http://localhost:8080/api/users/1
Kiểm tra Eureka Dashboard
Truy cập http://localhost:8761 để xem các services đã đăng ký.
10. Best Practices
Logging với Correlation ID
@Component
public class CorrelationIdFilter extends OncePerRequestFilter {
private static final String CORRELATION_ID_HEADER = "X-Correlation-ID";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String correlationId = request.getHeader(CORRELATION_ID_HEADER);
if (correlationId == null) {
correlationId = UUID.randomUUID().toString();
}
MDC.put("correlationId", correlationId);
response.setHeader(CORRELATION_ID_HEADER, correlationId);
try {
filterChain.doFilter(request, response);
} finally {
MDC.remove("correlationId");
}
}
}
Circuit Breaker với Resilience4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final UserClient userClient;
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
@Retry(name = "userService")
public UserResponse getUser(Long userId) {
return userClient.getUserById(userId);
}
private UserResponse getUserFallback(Long userId, Exception ex) {
log.error("Fallback for user {}: {}", userId, ex.getMessage());
return UserResponse.builder()
.id(userId)
.fullName("Unknown")
.build();
}
}
Health Check cho từng service
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: always
health:
circuitbreakers:
enabled: true
Kết luận
Microservices với Spring Boot và Docker mang lại khả năng mở rộng và bảo trì tốt cho hệ thống lớn. Các điểm quan trọng cần nhớ:
- Bắt đầu đơn giản, không cần quá nhiều services ngay từ đầu
- Implement service discovery để services tự động tìm thấy nhau
- Sử dụng API Gateway làm điểm vào duy nhất
- Áp dụng circuit breaker và retry để tăng độ ổn định
- Logging với correlation ID để trace requests qua nhiều services
- Health checks và monitoring là bắt buộc cho production
- Sử dụng Docker Compose cho development, Kubernetes cho production