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.

Spring Boot Lan truyền Giao dịch: câu hỏi phỏng vấn và ví dụ thực tế

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.

Lời khuyên chuẩn bị

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?"

OrderService.javajava
// 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.

UserService.javajava
// 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:

text
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ủy

Câ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.

PaymentService.javajava
// 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:

text
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ữ
Cẩn thận deadlock

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.

BatchProcessingService.javajava
// 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:

text
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ới
Hỗ trợ NESTED

Lan 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.

ReportingService.javajava
// 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
    }
}
OrderService.javajava
// 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ó.

AuditService.javajava
// 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);
    }
}
SecurityService.javajava
// 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.

BrokenService.javajava
// 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:

java
// 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.

TransactionRollbackDemo.javajava
// 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ụ:

BaseTransactionalService.javajava
// 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.

IsolationDemo.javajava
// 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:

text
| 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   |
Tác động hiệu suất

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.

OutboxService.javajava
// 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.

TransactionPropagationTest.javajava
// 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");
    }
}
PropagationIntegrationTest.javajava
// 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ì.

TransactionConfig.javajava
// 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:

text
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ẩn

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.

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ẻ

#spring boot
#transactions
#propagation
#java
#interview

Chia sẻ

Bài viết liên quan