Phỏng vấn Spring Boot: Lan truyền Giao dịch
Làm chủ lan truyền giao dịch Spring Boot: REQUIRED, REQUIRES_NEW, NESTED và hơn thế. 12 câu hỏi phỏng vấn với mã ví dụ và bẫy thường gặp.

Lan truyền giao dịch là một khái niệm nền tảng trong Spring Boot, được đánh giá thường xuyên trong các buổi phỏng vấn kỹ thuật. Hiểu cách các giao dịch tương tác giữa các phương thức được chú thích @Transactional giúp tránh các lỗi tinh vi trong môi trường production và cho phép thiết kế kiến trúc bền vững.
Người phỏng vấn kiểm tra khả năng chọn mức độ lan truyền phù hợp dựa trên ngữ cảnh nghiệp vụ. Có thể giải thích tại sao dùng REQUIRES_NEW thay vì REQUIRED trong một trường hợp cụ thể tạo nên sự khác biệt.
Cơ bản về lan truyền giao dịch
Câu hỏi 1: Lan truyền giao dịch trong Spring là gì?
Lan truyền định nghĩa hành vi của một phương thức giao dịch khi được gọi trong ngữ cảnh của một giao dịch hiện có. Nó trả lời câu hỏi: "Điều gì xảy ra khi một phương thức @Transactional gọi một phương thức khác cũng được chú thích?"
// Minh họa khái niệm lan truyền
@Service
public class OrderService {
private final PaymentService paymentService;
private final OrderRepository orderRepository;
// Giao dịch cha - bắt đầu giao dịch mới
@Transactional
public void createOrder(OrderRequest request) {
// Lưu đơn hàng trong giao dịch hiện tại
Order order = orderRepository.save(new Order(request));
// Gọi đến phương thức @Transactional khác
// Lan truyền quyết định: cùng giao dịch hay mới?
paymentService.processPayment(order.getId(), request.getAmount());
}
}
@Service
public class PaymentService {
// Lan truyền mặc định: REQUIRED
// Tham gia giao dịch hiện có từ createOrder()
@Transactional
public void processPayment(Long orderId, BigDecimal amount) {
// Thực thi trong CÙNG giao dịch với createOrder()
// Nếu phương thức này thất bại, đơn hàng cũng được rollback
}
}Spring cung cấp bảy mức lan truyền, mỗi mức phù hợp với nhu cầu nghiệp vụ cụ thể. Lựa chọn ảnh hưởng trực tiếp đến tính nhất quán dữ liệu và hiệu suất.
Câu hỏi 2: Mô tả hành vi của REQUIRED (lan truyền mặc định)
REQUIRED là lan truyền mặc định. Nếu giao dịch tồn tại, phương thức tham gia vào nó. Ngược lại, một giao dịch mới được tạo. Đây là hành vi phổ biến và trực quan nhất.
// REQUIRED: hành vi mặc định
@Service
public class UserService {
private final UserRepository userRepository;
private final AuditService auditService;
@Transactional(propagation = Propagation.REQUIRED)
public void updateUser(Long userId, UserUpdateRequest request) {
// Bắt đầu giao dịch nếu không có
User user = userRepository.findById(userId).orElseThrow();
user.setEmail(request.getEmail());
userRepository.save(user);
// Audit tham gia cùng giao dịch
auditService.logUpdate(userId, "EMAIL_CHANGED");
}
}
@Service
public class AuditService {
private final AuditLogRepository auditLogRepository;
@Transactional(propagation = Propagation.REQUIRED)
public void logUpdate(Long userId, String action) {
// Tham gia giao dịch của updateUser()
// Commit hoặc rollback cùng nhau
auditLogRepository.save(new AuditLog(userId, action, Instant.now()));
}
}Sơ đồ dưới đây minh họa luồng giao dịch:
updateUser() bắt đầu TX-1
├── save(user) → TX-1
└── logUpdate() → tham gia TX-1 (REQUIRED)
└── save(audit) → TX-1
Nếu logUpdate() thất bại → rollback TX-1 → user VÀ audit bị hủyCâu hỏi 3: Khi nào dùng REQUIRES_NEW thay vì REQUIRED?
REQUIRES_NEW tạm dừng giao dịch hiện có và tạo giao dịch độc lập mới. Hữu ích khi một thao tác phải được commit bất kể kết quả của giao dịch cha.
// REQUIRES_NEW: giao dịch độc lập
@Service
public class PaymentService {
private final PaymentRepository paymentRepository;
private final PaymentAuditService auditService;
@Transactional
public void processPayment(Long orderId, BigDecimal amount) {
Payment payment = new Payment(orderId, amount);
paymentRepository.save(payment);
// Audit PHẢI được lưu ngay cả khi thanh toán thất bại sau
auditService.logPaymentAttempt(orderId, amount);
// Mô phỏng lỗi sau audit
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("Negative amount");
}
}
}
@Service
public class PaymentAuditService {
private final PaymentAuditRepository auditRepository;
// REQUIRES_NEW: commit độc lập với giao dịch cha
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logPaymentAttempt(Long orderId, BigDecimal amount) {
// Giao dịch mới TX-2 được tạo
// TX-1 (processPayment) bị tạm dừng
auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED"));
// TX-2 commit ở đây, độc lập với TX-1
}
}Luồng giao dịch với REQUIRES_NEW:
processPayment() bắt đầu TX-1
├── save(payment) → TX-1
├── logPaymentAttempt() → TX-1 TẠM DỪNG
│ └── bắt đầu TX-2 → giao dịch mới
│ └── save(audit) → TX-2
│ └── COMMIT TX-2 → audit được lưu
│ └── TX-1 TIẾP TỤC
└── throw InvalidAmountException
└── ROLLBACK TX-1 → thanh toán bị hủy, nhưng audit được giữREQUIRES_NEW có thể gây deadlock nếu giao dịch mới truy cập vào cùng tài nguyên bị khóa bởi giao dịch tạm dừng. Tránh dùng REQUIRES_NEW để sửa đổi cùng các bảng với giao dịch cha.
Câu hỏi 4: Giải thích lan truyền NESTED và khác REQUIRES_NEW như thế nào
NESTED tạo một savepoint trong giao dịch hiện tại. Nếu phương thức thất bại, chỉ những thay đổi từ savepoint trở đi được rollback, không phải toàn bộ giao dịch cha.
// NESTED: savepoint trong giao dịch cha
@Service
public class BatchProcessingService {
private final ItemRepository itemRepository;
private final ItemProcessor itemProcessor;
@Transactional
public BatchResult processBatch(List<Item> items) {
BatchResult result = new BatchResult();
for (Item item : items) {
try {
// Mỗi item được xử lý với một savepoint
itemProcessor.processItem(item);
result.addSuccess(item.getId());
} catch (ProcessingException e) {
// Rollback chỉ item này, không phải toàn bộ batch
result.addFailure(item.getId(), e.getMessage());
}
}
return result; // Commit các item thành công
}
}
@Service
public class ItemProcessor {
private final ItemRepository itemRepository;
// NESTED: tạo savepoint, rollback một phần khả dụng
@Transactional(propagation = Propagation.NESTED)
public void processItem(Item item) {
item.setStatus("PROCESSING");
itemRepository.save(item);
// Kiểm tra nghiệp vụ
if (!isValid(item)) {
throw new ProcessingException("Invalid item");
// Rollback đến savepoint → chỉ item này
}
item.setStatus("COMPLETED");
itemRepository.save(item);
}
}So sánh NESTED và REQUIRES_NEW:
NESTED:
├── Dùng savepoint trong TX cha
├── Khi lỗi → rollback đến savepoint
├── Nếu TX cha rollback → NESTED cũng rollback
└── Hiệu suất hơn (không kết nối mới)
REQUIRES_NEW:
├── Tạo giao dịch hoàn toàn độc lập
├── Khi lỗi → rollback chỉ TX con
├── Nếu TX cha rollback → TX con ĐÃ COMMIT
└── Yêu cầu kết nối mớiLan truyền NESTED yêu cầu hỗ trợ savepoint JDBC. Hầu hết các cơ sở dữ liệu hiện đại (PostgreSQL, MySQL, Oracle) đều hỗ trợ. Kiểm tra tính tương thích trước khi dùng.
Các loại lan truyền nâng cao
Câu hỏi 5: Khi nào dùng SUPPORTS và NOT_SUPPORTED?
SUPPORTS thực thi trong giao dịch hiện có nếu có, ngược lại không có giao dịch. NOT_SUPPORTED tạm dừng bất kỳ giao dịch hiện có nào và thực thi mà không có giao dịch.
// SUPPORTS: giao dịch tùy chọn
@Service
public class ReportingService {
private final ReportRepository reportRepository;
// SUPPORTS: hoạt động có hoặc không có giao dịch
// Hữu ích cho các thao tác đọc không cần đảm bảo giao dịch
@Transactional(propagation = Propagation.SUPPORTS)
public Report generateReport(Long reportId) {
// Nếu được gọi từ phương thức @Transactional → dùng TX của nó
// Nếu được gọi trực tiếp → không có giao dịch (đọc OK)
return reportRepository.generateComplexReport(reportId);
}
}
@Service
public class ExternalNotificationService {
private final ExternalApiClient apiClient;
// NOT_SUPPORTED: không bao giờ trong giao dịch
// Tránh chặn TX trong cuộc gọi bên ngoài chậm
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendExternalNotification(String message) {
// TX cha tạm dừng trong cuộc gọi
apiClient.send(message); // Cuộc gọi HTTP có thể chậm
// TX cha tiếp tục sau
}
}// Ví dụ kết hợp
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final ReportingService reportingService;
private final ExternalNotificationService notificationService;
@Transactional
public void completeOrder(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
order.setStatus("COMPLETED");
orderRepository.save(order);
// Tạo báo cáo trong cùng TX (SUPPORTS)
Report report = reportingService.generateReport(orderId);
// Thông báo bên ngoài NGOÀI giao dịch (NOT_SUPPORTED)
// Tránh timeout TX nếu API bên ngoài chậm
notificationService.sendExternalNotification(
"Order " + orderId + " completed"
);
}
}Câu hỏi 6: Giải thích MANDATORY và NEVER
MANDATORY yêu cầu một giao dịch hiện có và ném exception nếu không có. NEVER yêu cầu không có giao dịch và ném exception nếu có.
// MANDATORY: phải được gọi từ trong giao dịch
@Service
public class AuditService {
private final AuditRepository auditRepository;
// MANDATORY: từ chối thực thi mà không có giao dịch hiện có
// Đảm bảo audit luôn nguyên tử với thao tác được audit
@Transactional(propagation = Propagation.MANDATORY)
public void logCriticalAction(String action, Long entityId) {
// Nếu được gọi không có giao dịch → IllegalTransactionStateException
auditRepository.save(new AuditLog(action, entityId, Instant.now()));
}
}
// CacheService.java
// NEVER: không bao giờ trong giao dịch
@Service
public class CacheService {
private final CacheManager cacheManager;
// NEVER: cache không nên tham gia giao dịch
// Tránh không nhất quán giữa cache và DB sau rollback
@Transactional(propagation = Propagation.NEVER)
public void invalidateCache(String cacheKey) {
// Nếu được gọi từ @Transactional → IllegalTransactionStateException
cacheManager.getCache("entities").evict(cacheKey);
}
}// Sử dụng MANDATORY đúng cách
@Service
public class SecurityService {
private final AuditService auditService;
@Transactional
public void changeUserPassword(Long userId, String newPassword) {
// Thao tác nhạy cảm...
updatePassword(userId, newPassword);
// Audit PHẢI trong cùng giao dịch
// MANDATORY thực thi ràng buộc kiến trúc này
auditService.logCriticalAction("PASSWORD_CHANGE", userId);
}
// LỖI: gọi trực tiếp không có giao dịch
public void badUsage() {
// Ném IllegalTransactionStateException vì không có TX
auditService.logCriticalAction("TEST", 1L);
}
}Sẵn sàng chinh phục phỏng vấn Spring Boot?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Bẫy phỏng vấn thường gặp
Câu hỏi 7: Tại sao @Transactional không hoạt động trên các cuộc gọi nội bộ?
Một trong những bẫy phổ biến nhất. Các cuộc gọi phương thức nội bộ (self-invocation) bỏ qua proxy Spring, tắt quản lý giao dịch.
// LỖI KINH ĐIỂN: self-invocation
@Service
public class BrokenService {
private final ItemRepository itemRepository;
public void processItems(List<Long> itemIds) {
for (Long id : itemIds) {
// BẪY: cuộc gọi nội bộ → bỏ qua proxy
// @Transactional trên processItem() bị BỎ QUA
processItem(id);
}
}
@Transactional
public void processItem(Long itemId) {
// Giao dịch này KHÔNG BAO GIỜ được tạo trong cuộc gọi nội bộ
Item item = itemRepository.findById(itemId).orElseThrow();
item.setStatus("PROCESSED");
itemRepository.save(item);
}
}Giải pháp tránh bẫy này:
// Giải pháp 1: Self-injection
@Service
public class FixedServiceWithSelfInjection {
private final ItemRepository itemRepository;
@Lazy
@Autowired
private FixedServiceWithSelfInjection self; // Self-injection
public void processItems(List<Long> itemIds) {
for (Long id : itemIds) {
// Gọi qua proxy → @Transactional hoạt động
self.processItem(id);
}
}
@Transactional
public void processItem(Long itemId) {
// Giao dịch được tạo đúng cách
Item item = itemRepository.findById(itemId).orElseThrow();
item.setStatus("PROCESSED");
itemRepository.save(item);
}
}
// Giải pháp 2: Tách thành hai service (khuyến nghị)
@Service
public class ItemOrchestrator {
private final ItemProcessor processor;
public void processItems(List<Long> itemIds) {
for (Long id : itemIds) {
// Gọi đến bean khác → proxy hoạt động
processor.processItem(id);
}
}
}
@Service
public class ItemProcessor {
private final ItemRepository itemRepository;
@Transactional
public void processItem(Long itemId) {
// Giao dịch được quản lý đúng cách
Item item = itemRepository.findById(itemId).orElseThrow();
item.setStatus("PROCESSED");
itemRepository.save(item);
}
}Câu hỏi 8: Cách xử lý exception và rollback?
Mặc định, Spring chỉ rollback trên RuntimeException và Error. Checked exception KHÔNG kích hoạt rollback tự động.
// Hành vi rollback theo loại exception
@Service
public class TransactionRollbackDemo {
private final OrderRepository orderRepository;
// RuntimeException → ROLLBACK tự động
@Transactional
public void methodWithRuntimeException() {
orderRepository.save(new Order());
throw new RuntimeException("Error"); // ROLLBACK
}
// Checked Exception → KHÔNG rollback mặc định
@Transactional
public void methodWithCheckedException() throws IOException {
orderRepository.save(new Order());
throw new IOException("File error"); // Vẫn COMMIT!
}
// Buộc rollback trên checked exception
@Transactional(rollbackFor = IOException.class)
public void methodWithRollbackFor() throws IOException {
orderRepository.save(new Order());
throw new IOException("Error"); // ROLLBACK nhờ rollbackFor
}
// Loại trừ RuntimeException khỏi rollback
@Transactional(noRollbackFor = BusinessException.class)
public void methodWithNoRollbackFor() {
orderRepository.save(new Order());
throw new BusinessException("Warning"); // COMMIT bất chấp exception
}
}Cấu hình khuyến nghị cho các trường hợp nghiệp vụ:
// Cấu hình giao dịch nhất quán
@Service
public abstract class BaseTransactionalService {
// Rollback trên tất cả exception (checked và unchecked)
@Transactional(rollbackFor = Exception.class)
protected void executeInTransaction(Runnable operation) {
operation.run();
}
}
// PaymentService.java
@Service
public class PaymentService extends BaseTransactionalService {
private final PaymentRepository paymentRepository;
@Transactional(
rollbackFor = Exception.class,
noRollbackFor = InsufficientFundsException.class
)
public PaymentResult processPayment(PaymentRequest request) {
Payment payment = new Payment(request);
paymentRepository.save(payment);
if (request.getAmount().compareTo(getBalance()) > 0) {
// KHÔNG rollback - muốn giữ bản ghi nỗ lực
throw new InsufficientFundsException("Insufficient balance");
}
return new PaymentResult(payment.getId(), "SUCCESS");
}
}Câu hỏi 9: Cách ly giao dịch hoạt động với lan truyền như thế nào?
Cách ly và lan truyền bổ sung cho nhau. Cách ly xác định khả năng hiển thị dữ liệu giữa các giao dịch đồng thời.
// Kết hợp cách ly + lan truyền
@Service
public class IsolationDemo {
private final AccountRepository accountRepository;
// READ_COMMITTED: thấy dữ liệu được commit bởi giao dịch khác
@Transactional(
isolation = Isolation.READ_COMMITTED,
propagation = Propagation.REQUIRED
)
public BigDecimal getAccountBalance(Long accountId) {
// Có thể thấy giá trị khác nhau nếu đọc lại trong giao dịch
return accountRepository.findById(accountId)
.map(Account::getBalance)
.orElse(BigDecimal.ZERO);
}
// REPEATABLE_READ: đảm bảo cùng đọc trong giao dịch
@Transactional(
isolation = Isolation.REPEATABLE_READ,
propagation = Propagation.REQUIRED
)
public void transferWithConsistentRead(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
// Ngay cả khi giao dịch khác sửa 'from' giữa chừng,
// luôn thấy giá trị ban đầu (snapshot)
from.debit(amount);
to.credit(amount);
accountRepository.save(from);
accountRepository.save(to);
}
// SERIALIZABLE: cách ly tối đa, không đồng thời
@Transactional(
isolation = Isolation.SERIALIZABLE,
propagation = Propagation.REQUIRES_NEW
)
public void criticalOperation(Long accountId) {
// Chặn mọi giao dịch khác trên dữ liệu này
// Dùng tiết kiệm - tác động hiệu suất
Account account = accountRepository.findById(accountId).orElseThrow();
account.performCriticalUpdate();
accountRepository.save(account);
}
}Bảng tóm tắt các mức cách ly:
| Cách ly | Dirty Read | Non-Repeatable | Phantom |
|------------------|------------|----------------|---------|
| READ_UNCOMMITTED | Có thể | Có thể | Có thể |
| READ_COMMITTED | Không | Có thể | Có thể |
| REPEATABLE_READ | Không | Không | Có thể |
| SERIALIZABLE | Không | Không | Không |Cách ly càng nghiêm ngặt, hiệu suất càng có thể bị ảnh hưởng bởi khóa. SERIALIZABLE có thể gây tranh chấp đáng kể trong production lưu lượng cao.
Mẫu nâng cao
Câu hỏi 10: Cách triển khai mẫu "transactional outbox"?
Mẫu outbox đảm bảo nhất quán giữa các sửa đổi cơ sở dữ liệu và việc gửi tin nhắn/sự kiện, ngay cả khi thất bại.
// Mẫu Transactional Outbox
@Service
public class OutboxService {
private final OutboxRepository outboxRepository;
// Lưu sự kiện trong cùng giao dịch với thực thể nghiệp vụ
@Transactional(propagation = Propagation.MANDATORY)
public void saveEvent(String aggregateType, Long aggregateId, String eventType, String payload) {
OutboxEvent event = OutboxEvent.builder()
.aggregateType(aggregateType)
.aggregateId(aggregateId)
.eventType(eventType)
.payload(payload)
.status("PENDING")
.createdAt(Instant.now())
.build();
outboxRepository.save(event);
}
}
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final OutboxService outboxService;
@Transactional
public Order createOrder(OrderRequest request) {
// Tạo đơn hàng
Order order = new Order(request);
orderRepository.save(order);
// Sự kiện outbox trong CÙNG giao dịch (MANDATORY)
// Nếu commit thành công → cả hai được lưu
// Nếu rollback → không cái nào được lưu
outboxService.saveEvent(
"ORDER",
order.getId(),
"ORDER_CREATED",
toJson(new OrderCreatedEvent(order))
);
return order;
}
}
// OutboxPublisher.java
// Tiến trình riêng biệt xuất bản sự kiện
@Service
public class OutboxPublisher {
private final OutboxRepository outboxRepository;
private final MessageBroker messageBroker;
// Giao dịch độc lập cho mỗi lần xuất bản
@Scheduled(fixedDelay = 1000)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void publishPendingEvents() {
List<OutboxEvent> events = outboxRepository
.findByStatusOrderByCreatedAt("PENDING");
for (OutboxEvent event : events) {
try {
messageBroker.publish(event.getEventType(), event.getPayload());
event.setStatus("PUBLISHED");
event.setPublishedAt(Instant.now());
} catch (Exception e) {
event.setStatus("FAILED");
event.setError(e.getMessage());
}
outboxRepository.save(event);
}
}
}Câu hỏi 11: Cách kiểm thử các hành vi lan truyền khác nhau?
Kiểm thử lan truyền yêu cầu sự chú ý đặc biệt để xác minh hành vi giao dịch mong đợi.
// Kiểm thử hành vi lan truyền
@SpringBootTest
@Transactional
class TransactionPropagationTest {
@Autowired
private OrderService orderService;
@Autowired
private PaymentService paymentService;
@Autowired
private TestEntityManager entityManager;
@Test
void required_shouldShareTransaction() {
// Given
Order order = orderService.createOrder(new OrderRequest());
// When - thanh toán trong cùng giao dịch (REQUIRED)
paymentService.processPayment(order.getId(), BigDecimal.TEN);
// Then - cả hai hiển thị trước commit
entityManager.flush();
assertThat(entityManager.find(Order.class, order.getId())).isNotNull();
assertThat(entityManager.find(Payment.class, order.getId())).isNotNull();
}
@Test
void requiresNew_shouldCommitIndependently() {
// Given
Long orderId = null;
try {
// When - audit trong REQUIRES_NEW
orderId = orderService.createOrderWithAudit(new OrderRequest());
throw new RuntimeException("Simulated failure after audit");
} catch (RuntimeException e) {
// Giao dịch chính rollback
}
// Then - audit (REQUIRES_NEW) vẫn được commit
assertThat(findAuditLog(orderId)).isNotNull();
// Nhưng đơn hàng được rollback
assertThat(findOrder(orderId)).isNull();
}
@Test
void mandatory_shouldThrowWithoutTransaction() {
// Given - không có giao dịch hoạt động
// When/Then - phải ném exception
assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L))
.isInstanceOf(IllegalTransactionStateException.class)
.hasMessageContaining("No existing transaction");
}
}// Kiểm thử tích hợp với rollback và commit thực
@SpringBootTest
class PropagationIntegrationTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Autowired
private AuditLogRepository auditLogRepository;
@Test
void nested_shouldRollbackOnlyNestedOnFailure() {
// Given
int initialCount = orderRepository.findAll().size();
// When - xử lý batch với NESTED
BatchResult result = orderService.processBatchWithNested(
List.of(validItem(), invalidItem(), validItem())
);
// Then - chỉ item hợp lệ được lưu
assertThat(result.getSuccessCount()).isEqualTo(2);
assertThat(result.getFailureCount()).isEqualTo(1);
assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2);
}
}Câu hỏi 12: Các thực hành tốt nhất cho cấu hình giao dịch là gì?
Một cấu hình giao dịch nhất quán ở cấp độ dự án tránh bất ngờ và dễ bảo trì.
// Cấu hình giao dịch tập trung
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
// Timeout mặc định cho tất cả giao dịch
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource);
tm.setDefaultTimeout(30); // 30 giây tối đa mỗi giao dịch
return tm;
}
}
// BaseService.java
// Chú thích mặc định cho service
@Service
@Transactional(
readOnly = true, // Chỉ đọc mặc định
rollbackFor = Exception.class // Rollback trên bất kỳ exception nào
)
public abstract class BaseService {
// Phương thức đọc kế thừa readOnly = true
}
// OrderService.java
// Service với cấu hình nhất quán
@Service
public class OrderService extends BaseService {
private final OrderRepository orderRepository;
// Kế thừa readOnly = true
public Order findById(Long id) {
return orderRepository.findById(id).orElseThrow();
}
// Override cho ghi
@Transactional(readOnly = false)
public Order createOrder(OrderRequest request) {
return orderRepository.save(new Order(request));
}
// Cấu hình rõ ràng cho trường hợp quan trọng
@Transactional(
readOnly = false,
propagation = Propagation.REQUIRES_NEW,
isolation = Isolation.SERIALIZABLE,
timeout = 10
)
public void criticalOperation(Long orderId) {
// Thao tác với cách ly tối đa và timeout ngắn
}
}Danh sách kiểm tra thực hành tốt nhất:
Cấu hình Giao dịch - Danh sách kiểm tra
✅ readOnly = true mặc định, override rõ ràng cho ghi
✅ rollbackFor = Exception.class để bao gồm checked exception
✅ Timeout phù hợp theo loại thao tác
✅ Tránh cuộc gọi nội bộ (@Transactional bị bỏ qua)
✅ REQUIRES_NEW chỉ khi cần commit độc lập
✅ MANDATORY để đảm bảo ngữ cảnh giao dịch
✅ Kiểm thử rõ ràng hành vi rollback
✅ Giám sát giao dịch dài hạn
✅ Tài liệu hóa lựa chọn lan truyền không chuẩnSẵn sàng chinh phục phỏng vấn Spring Boot?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Kết luận
Lan truyền giao dịch là khái niệm nền tảng được đánh giá trong phỏng vấn Spring Boot. Các điểm chính cần nhớ:
Loại lan truyền phổ biến:
- ✅ REQUIRED (mặc định): tham gia hoặc tạo giao dịch
- ✅ REQUIRES_NEW: giao dịch độc lập, commit riêng
- ✅ NESTED: savepoint cho rollback một phần
- ✅ MANDATORY: yêu cầu giao dịch hiện có
Bẫy cần tránh:
- ✅ Self-invocation: bỏ qua proxy, @Transactional bị bỏ qua
- ✅ Checked exception: không rollback mặc định
- ✅ REQUIRES_NEW trên cùng dữ liệu: nguy cơ deadlock
- ✅ Thiếu timeout: giao dịch bị chặn vô thời hạn
Thực hành tốt nhất:
- ✅ readOnly = true mặc định
- ✅ rollbackFor = Exception.class một cách hệ thống
- ✅ Tách service để tránh self-invocation
- ✅ Kiểm thử rõ ràng hành vi rollback
Thành thạo lan truyền giao dịch thể hiện sự hiểu biết sâu sắc về Spring và quản lý dữ liệu. Những khái niệm này thiết yếu để thiết kế ứng dụng bền vững và vượt qua phỏng vấn kỹ thuật thành công.
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Thẻ
Chia sẻ
Bài viết liên quan

30 Câu Hỏi Phỏng Vấn Spring Boot: Hướng Dẫn Đầy Đủ cho Lập Trình Viên Java
Chuẩn bị các buổi phỏng vấn Spring Boot với 30 câu hỏi cốt lõi về auto-configuration, starter, Spring Data JPA, bảo mật và kiểm thử.

Spring Modulith: Kiến trúc Monolith Mô-đun Giải thích
Học Spring Modulith để xây dựng monolith mô-đun trong Java. Kiến trúc, mô-đun, sự kiện bất đồng bộ và testing với ví dụ Spring Boot 3.

Phỏng vấn Spring Batch 5: Phân vùng, Chunk và Khả năng chịu lỗi
Chinh phục các buổi phỏng vấn Spring Batch 5: 15 câu hỏi cốt lõi về phân vùng, xử lý chunk và khả năng chịu lỗi với ví dụ Java 21.