1. Bối cảnh
Trong một hệ thống nội bộ dành cho nhân viên hỗ trợ, khi họ tạo đơn hàng quảng cáo trên màn hình LED, nếu không xác nhận triển khai trong vòng 30 phút, hệ thống cần tự động hủy đơn hàng đó. Số lượng người dùng và mức độ đồng thời rất thấp.
2. Các giải pháp khả thi
Để tự động đóng đơn hàng sau 30 phút, có thể áp dụng một số phương án sau:
- Dùng tác vụ định kỳ (Task Scheduling): Quét cơ sở dữ liệu định kỳ. Giải pháp này kém hiệu quả và tiêu tốn tài nguyên máy chủ.
- Dùng Redis với khóa hết hạn: Tạo một token với thời gian sống 30 phút khi đặt hàng, lưu vào Redis. Khi token hết hạn, lắng nghe sự kiện để lấy orderId, kiểm tra trạng thái và đóng đơn hàng. Tuy nhiên, cách này có nhược điểm là cần thêm một trường dữ liệu phụ trong bảng.
- Dùng Redis Delayed Queue: Xử lý timeout cho đơn hàng.
- Dùng Message Queue (MQ) với cơ chế trì hoãn: Đây là giải pháp tối ưu nhất, thường dùng kết hợp Dead Letter Queue (DLQ) và TTL (Time-To-Live).
Phần tiếp theo sẽ tập trung vào giải pháp dùng RabbitMQ với Dead Letter Queue.
3. Nguyên lý hoạt động
- Khi một đơn hàng được tạo, hệ thống gửi một tin nhắn đến Exchange chính (A) với thời gian sống (TTL) là 30 phút.
- Tin nhắn được định tuyến đến Queue A. Queue này được cấu hình với một Dead Letter Exchange (DLX).
- Queue A không có consumer nào lắng nghe. Do đó, tin nhắn sẽ nằm chờ đến khi hết hạn.
- Sau 30 phút (hoặc theo TTL đã cài), tin nhắn tự động chuyển sang Dead Letter Exchange, rồi đến Dead Letter Queue (DLQ).
- Consumer của DLQ nhận được tin nhắn, lấy orderId và kiểm tra trạng thái thanh toán:
- Nếu đã thanh toán → kết thúc (return).
- Nếu chưa thanh toán → cập nhật trạng thái đơn hàng thành "Đã hủy" và hoàn trả tồn kho.
4. Demo chi tiết
Tạo class DeadLetterMQConfig và đánh dấu bằng @Component.
4.1 Định nghĩa các biến cấu hình
@Value("${mayikt.order.exchange}")
private String orderExchange;
@Value("${mayikt.order.queue}")
private String orderQueue;
@Value("${mayikt.order.routingKey}")
private String orderRoutingKey;
@Value("${mayikt.dlx.exchange}")
private String dlxExchange;
@Value("${mayikt.dlx.queue}")
private String dlxQueue;
@Value("${mayikt.dlx.routingKey}")
private String dlxRoutingKey;
4.2 Khai báo các Bean trong Spring
Dead Letter Exchange:
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange(dlxExchange);
}
Dead Letter Queue:
@Bean
public Queue dlxQueue() {
return new Queue(dlxQueue);
}
Order Exchange chính:
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(orderExchange);
}
Order Queue (quan trọng):
@Bean
public Queue orderQueue() {
Map<String, Object> arguments = new HashMap<>(2);
// Chỉ định Dead Letter Exchange
arguments.put("x-dead-letter-exchange", dlxExchange);
// Chỉ định routing key cho DLX
arguments.put("x-dead-letter-routing-key", dlxRoutingKey);
return new Queue(orderQueue, true, false, false, arguments);
}
Binding Order Queue với Order Exchange:
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(orderRoutingKey);
}
Binding Dead Letter Queue với Dead Letter Exchange:
@Bean
public Binding binding() {
return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(dlxRoutingKey);
}
Code hoàn chỉnh cho DeadLetterMQConfig.java:
@Component
public class DeadLetterMQConfig {
@Value("${mayikt.order.exchange}")
private String orderExchange;
@Value("${mayikt.order.queue}")
private String orderQueue;
@Value("${mayikt.order.routingKey}")
private String orderRoutingKey;
@Value("${mayikt.dlx.exchange}")
private String dlxExchange;
@Value("${mayikt.dlx.queue}")
private String dlxQueue;
@Value("${mayikt.dlx.routingKey}")
private String dlxRoutingKey;
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange(dlxExchange);
}
@Bean
public Queue dlxQueue() {
return new Queue(dlxQueue);
}
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(orderExchange);
}
@Bean
public Queue orderQueue() {
Map<String, Object> args = new HashMap<>(2);
args.put("x-dead-letter-exchange", dlxExchange);
args.put("x-dead-letter-routing-key", dlxRoutingKey);
return new Queue(orderQueue, true, false, false, args);
}
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue()).to(orderExchange()).with(orderRoutingKey);
}
@Bean
public Binding dlxBinding() {
return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with(dlxRoutingKey);
}
}
4.3 Cấu hình application.properties
server.port=8082
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# Camel case mapping cho MyBatis
mybatis.configuration.map-underscore-to-camel-case=true
# RabbitMQ config
spring.rabbitmq.virtual-host=/zhang_rabbit
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.username=zhang
spring.rabbitmq.password=zhang
spring.rabbitmq.port=5672
# Cấu hình cho Dead Letter Queue
mayikt.dlx.exchange=mayikt_order_dlx_exchange
mayikt.dlx.queue=mayikt_order_dlx_queue
mayikt.dlx.routingKey=dlx
# Cấu hình cho Order Exchange chính
mayikt.order.exchange=mayikt_order_exchange
mayikt.order.queue=mayikt_order_queue
mayikt.order.routingKey=mayikt.order
4.4 Producer – Controller đặt hàng
Trong ví dụ này, TTL được đặt là 10 giây để dễ kiểm tra.
@RestController
public class OrderController {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
@Value("${mayikt.order.exchange}")
private String orderExchange;
@Value("${mayikt.order.routingKey}")
private String orderRoutingKey;
@GetMapping("/addOrder")
public String addOrder() {
String orderId = System.currentTimeMillis() + "";
OrderEntity order = new OrderEntity("Order 30min timeout demo", orderId, 0);
int result = orderMapper.addOrder(order);
if (result <= 0) {
return "fail";
}
rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, orderId, msg -> {
// Đặt TTL 10 giây để test
msg.getMessageProperties().setExpiration("10000");
return msg;
});
return "success";
}
}
4.5 Consumer cho Dead Letter Queue
@Component
public class OrderDlxConsumer {
@Autowired
private OrderMapper orderMapper;
@RabbitListener(queues = "mayikt_order_dlx_queue")
public void process(String orderId) {
System.out.println("DLQ received: " + orderId);
if (orderId == null || orderId.trim().isEmpty()) return;
OrderEntity order = orderMapper.getOrder(orderId);
if (order == null) return;
// Nếu chưa thanh toán (status = 0)
if (order.getOrderStatus() == 0) {
orderMapper.updateStatus(orderId, 2); // Đóng đơn
// Thêm logic hoàn trả tồn kho tại đây
}
}
}
4.6 Kiểm tra
- Gọi API:
http://localhost:8082/addOrder - Đơn hàng được tạo với trạng thái
0 (Chưa thanh toán) - Sau 10 giây, consumer của DLQ nhận tin nhắn, kiểm tra và cập nhật trạng thái thành
2 (Đã đóng).