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
Hãy thực hành triển khai từng phần một và test kỹ trước khi đưa vào môi trường production!