# สัมภาษณ์ Spring Boot: การกระจายธุรกรรม > เชี่ยวชาญการกระจายธุรกรรมใน Spring Boot: REQUIRED, REQUIRES_NEW, NESTED และอื่น ๆ 12 คำถามสัมภาษณ์พร้อมโค้ดและกับดักทั่วไป - Published: 2026-03-18 - Updated: 2026-05-01 - Author: SharpSkill - Tags: spring boot, transactions, propagation, java, interview - Reading time: 14 min --- การกระจายธุรกรรมเป็นแนวคิดพื้นฐานใน Spring Boot ที่ได้รับการประเมินอย่างสม่ำเสมอในการสัมภาษณ์ทางเทคนิค การเข้าใจว่าธุรกรรมโต้ตอบกันอย่างไรระหว่างเมธอดที่มีคำอธิบาย `@Transactional` ช่วยหลีกเลี่ยงข้อบกพร่องเล็กน้อยในระบบโปรดักชันและช่วยออกแบบสถาปัตยกรรมที่แข็งแกร่ง > **คำแนะนำในการเตรียมตัว** > > ผู้สัมภาษณ์จะทดสอบความสามารถในการเลือกระดับการกระจายที่เหมาะสมตามบริบททางธุรกิจ การสามารถอธิบายว่าทำไมจึงใช้ REQUIRES_NEW แทน REQUIRED ในกรณีที่เฉพาะเจาะจงจะสร้างความแตกต่าง ## พื้นฐานของการกระจายธุรกรรม ### คำถามที่ 1: การกระจายธุรกรรมใน Spring คืออะไร? การกระจายกำหนดพฤติกรรมของเมธอดที่เป็นธุรกรรมเมื่อถูกเรียกในบริบทของธุรกรรมที่มีอยู่ มันตอบคำถาม: "จะเกิดอะไรขึ้นเมื่อเมธอด `@Transactional` เรียกเมธอดอื่นที่มีคำอธิบายเช่นเดียวกัน?" ```java // OrderService.java // แสดงแนวคิดของการกระจาย @Service public class OrderService { private final PaymentService paymentService; private final OrderRepository orderRepository; // ธุรกรรมหลัก - เริ่มต้นธุรกรรมใหม่ @Transactional public void createOrder(OrderRequest request) { // บันทึกคำสั่งซื้อในธุรกรรมปัจจุบัน Order order = orderRepository.save(new Order(request)); // เรียกเมธอด @Transactional อื่น // การกระจายตัดสิน: ธุรกรรมเดียวกันหรือใหม่? paymentService.processPayment(order.getId(), request.getAmount()); } } @Service public class PaymentService { // การกระจายเริ่มต้น: REQUIRED // เข้าร่วมธุรกรรมที่มีอยู่จาก createOrder() @Transactional public void processPayment(Long orderId, BigDecimal amount) { // ดำเนินการในธุรกรรมเดียวกันกับ createOrder() // หากเมธอดนี้ล้มเหลว คำสั่งซื้อจะถูก rollback ด้วย } } ``` Spring มีระดับการกระจายเจ็ดระดับ ซึ่งแต่ละระดับเหมาะกับความต้องการทางธุรกิจที่เฉพาะเจาะจง การเลือกส่งผลโดยตรงต่อความสอดคล้องของข้อมูลและประสิทธิภาพ ### คำถามที่ 2: อธิบายพฤติกรรมของ REQUIRED (การกระจายเริ่มต้น) REQUIRED คือการกระจายเริ่มต้น หากมีธุรกรรม เมธอดจะเข้าร่วมกับมัน มิฉะนั้นจะมีการสร้างธุรกรรมใหม่ นี่เป็นพฤติกรรมที่พบบ่อยที่สุดและเป็นธรรมชาติที่สุด ```java // UserService.java // REQUIRED: พฤติกรรมเริ่มต้น @Service public class UserService { private final UserRepository userRepository; private final AuditService auditService; @Transactional(propagation = Propagation.REQUIRED) public void updateUser(Long userId, UserUpdateRequest request) { // เริ่มธุรกรรมหากไม่มี User user = userRepository.findById(userId).orElseThrow(); user.setEmail(request.getEmail()); userRepository.save(user); // Audit เข้าร่วมธุรกรรมเดียวกัน auditService.logUpdate(userId, "EMAIL_CHANGED"); } } @Service public class AuditService { private final AuditLogRepository auditLogRepository; @Transactional(propagation = Propagation.REQUIRED) public void logUpdate(Long userId, String action) { // เข้าร่วมธุรกรรมของ updateUser() // Commit หรือ rollback ร่วมกัน auditLogRepository.save(new AuditLog(userId, action, Instant.now())); } } ``` ไดอะแกรมด้านล่างแสดงการไหลของธุรกรรม: ```text updateUser() เริ่ม TX-1 ├── save(user) → TX-1 └── logUpdate() → เข้าร่วม TX-1 (REQUIRED) └── save(audit) → TX-1 หาก logUpdate() ล้มเหลว → rollback TX-1 → user และ audit ถูกยกเลิก ``` ### คำถามที่ 3: เมื่อใดควรใช้ REQUIRES_NEW แทน REQUIRED? REQUIRES_NEW จะหยุดธุรกรรมที่มีอยู่และสร้างธุรกรรมอิสระใหม่ มีประโยชน์เมื่อการดำเนินการต้องได้รับการ commit โดยไม่คำนึงถึงผลของธุรกรรมหลัก ```java // PaymentService.java // REQUIRES_NEW: ธุรกรรมอิสระ @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 ต้องถูกบันทึกแม้การชำระเงินจะล้มเหลวภายหลัง auditService.logPaymentAttempt(orderId, amount); // จำลองข้อผิดพลาดหลัง audit if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException("Negative amount"); } } } @Service public class PaymentAuditService { private final PaymentAuditRepository auditRepository; // REQUIRES_NEW: commit แยกจากธุรกรรมหลัก @Transactional(propagation = Propagation.REQUIRES_NEW) public void logPaymentAttempt(Long orderId, BigDecimal amount) { // ธุรกรรมใหม่ TX-2 ถูกสร้างขึ้น // TX-1 (processPayment) ถูกหยุดชั่วคราว auditRepository.save(new PaymentAudit(orderId, amount, "ATTEMPTED")); // TX-2 commit ที่นี่ แยกจาก TX-1 } } ``` การไหลของธุรกรรมด้วย REQUIRES_NEW: ```text processPayment() เริ่ม TX-1 ├── save(payment) → TX-1 ├── logPaymentAttempt() → TX-1 ถูกหยุดชั่วคราว │ └── เริ่ม TX-2 → ธุรกรรมใหม่ │ └── save(audit) → TX-2 │ └── COMMIT TX-2 → audit ถูกบันทึก │ └── TX-1 ดำเนินต่อ └── throw InvalidAmountException └── ROLLBACK TX-1 → การชำระเงินถูกยกเลิก แต่ audit ถูกเก็บรักษา ``` > **ระวัง deadlock** > > REQUIRES_NEW อาจทำให้เกิด deadlock หากธุรกรรมใหม่เข้าถึงทรัพยากรเดียวกันที่ถูกล็อกโดยธุรกรรมที่ถูกหยุดชั่วคราว หลีกเลี่ยงการใช้ REQUIRES_NEW เพื่อแก้ไขตารางเดียวกับธุรกรรมหลัก ### คำถามที่ 4: อธิบายการกระจาย NESTED และความแตกต่างจาก REQUIRES_NEW NESTED สร้าง savepoint ภายในธุรกรรมปัจจุบัน หากเมธอดล้มเหลว เฉพาะการเปลี่ยนแปลงตั้งแต่ savepoint จะถูก rollback ไม่ใช่ธุรกรรมหลักทั้งหมด ```java // BatchProcessingService.java // NESTED: savepoint ในธุรกรรมหลัก @Service public class BatchProcessingService { private final ItemRepository itemRepository; private final ItemProcessor itemProcessor; @Transactional public BatchResult processBatch(List items) { BatchResult result = new BatchResult(); for (Item item : items) { try { // แต่ละรายการประมวลผลด้วย savepoint itemProcessor.processItem(item); result.addSuccess(item.getId()); } catch (ProcessingException e) { // Rollback เฉพาะรายการนี้ ไม่ใช่ batch ทั้งหมด result.addFailure(item.getId(), e.getMessage()); } } return result; // Commit รายการที่สำเร็จ } } @Service public class ItemProcessor { private final ItemRepository itemRepository; // NESTED: สร้าง savepoint สามารถ rollback บางส่วนได้ @Transactional(propagation = Propagation.NESTED) public void processItem(Item item) { item.setStatus("PROCESSING"); itemRepository.save(item); // การตรวจสอบทางธุรกิจ if (!isValid(item)) { throw new ProcessingException("Invalid item"); // Rollback ไปที่ savepoint → เฉพาะรายการนี้ } item.setStatus("COMPLETED"); itemRepository.save(item); } } ``` การเปรียบเทียบ NESTED กับ REQUIRES_NEW: ```text NESTED: ├── ใช้ savepoint ใน TX หลัก ├── เมื่อล้มเหลว → rollback ไปที่ savepoint ├── หาก TX หลัก rollback → NESTED ก็ rollback ด้วย └── ประสิทธิภาพดีกว่า (ไม่มีการเชื่อมต่อใหม่) REQUIRES_NEW: ├── สร้างธุรกรรมที่อิสระอย่างสมบูรณ์ ├── เมื่อล้มเหลว → rollback เฉพาะ TX ลูก ├── หาก TX หลัก rollback → TX ลูก commit ไปแล้ว └── ต้องการการเชื่อมต่อใหม่ ``` > **การรองรับ NESTED** > > การกระจาย NESTED ต้องการการรองรับ savepoint ของ JDBC ฐานข้อมูลสมัยใหม่ส่วนใหญ่ (PostgreSQL, MySQL, Oracle) รองรับสิ่งนี้ ตรวจสอบความเข้ากันได้ก่อนใช้ ## ประเภทการกระจายขั้นสูง ### คำถามที่ 5: เมื่อใดควรใช้ SUPPORTS และ NOT_SUPPORTED? SUPPORTS ทำงานในธุรกรรมที่มีอยู่หากมี มิฉะนั้นไม่มีธุรกรรม NOT_SUPPORTED หยุดธุรกรรมที่มีอยู่ใด ๆ และทำงานโดยไม่มีธุรกรรม ```java // ReportingService.java // SUPPORTS: ธุรกรรมเสริม @Service public class ReportingService { private final ReportRepository reportRepository; // SUPPORTS: ทำงานได้ทั้งมีและไม่มีธุรกรรม // มีประโยชน์สำหรับการอ่านที่ไม่ต้องการการรับประกันธุรกรรม @Transactional(propagation = Propagation.SUPPORTS) public Report generateReport(Long reportId) { // หากเรียกจากเมธอด @Transactional → ใช้ TX ของมัน // หากเรียกโดยตรง → ไม่มีธุรกรรม (อ่าน OK) return reportRepository.generateComplexReport(reportId); } } @Service public class ExternalNotificationService { private final ExternalApiClient apiClient; // NOT_SUPPORTED: ไม่อยู่ในธุรกรรม // หลีกเลี่ยงการบล็อก TX ระหว่างการเรียกภายนอกที่ช้า @Transactional(propagation = Propagation.NOT_SUPPORTED) public void sendExternalNotification(String message) { // TX หลักถูกหยุดชั่วคราวระหว่างการเรียก apiClient.send(message); // การเรียก HTTP ที่อาจช้า // TX หลักดำเนินต่อหลังจากนั้น } } ``` ```java // OrderService.java // ตัวอย่างการใช้งานร่วมกัน @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); // สร้างรายงานใน TX เดียวกัน (SUPPORTS) Report report = reportingService.generateReport(orderId); // การแจ้งเตือนภายนอก นอกธุรกรรม (NOT_SUPPORTED) // หลีกเลี่ยง timeout ของ TX หาก API ภายนอกช้า notificationService.sendExternalNotification( "Order " + orderId + " completed" ); } } ``` ### คำถามที่ 6: อธิบาย MANDATORY และ NEVER MANDATORY ต้องการธุรกรรมที่มีอยู่ และจะโยน exception หากไม่มี NEVER ต้องการไม่มีธุรกรรม และจะโยน exception หากมี ```java // AuditService.java // MANDATORY: ต้องเรียกจากภายในธุรกรรม @Service public class AuditService { private final AuditRepository auditRepository; // MANDATORY: ปฏิเสธการทำงานโดยไม่มีธุรกรรม // รับประกันว่า audit เป็น atomic กับการดำเนินการที่ตรวจสอบเสมอ @Transactional(propagation = Propagation.MANDATORY) public void logCriticalAction(String action, Long entityId) { // หากเรียกโดยไม่มีธุรกรรม → IllegalTransactionStateException auditRepository.save(new AuditLog(action, entityId, Instant.now())); } } // CacheService.java // NEVER: ห้ามอยู่ในธุรกรรม @Service public class CacheService { private final CacheManager cacheManager; // NEVER: cache ไม่ควรเข้าร่วมธุรกรรม // หลีกเลี่ยงความไม่สอดคล้องระหว่าง cache และ DB หลัง rollback @Transactional(propagation = Propagation.NEVER) public void invalidateCache(String cacheKey) { // หากเรียกจาก @Transactional → IllegalTransactionStateException cacheManager.getCache("entities").evict(cacheKey); } } ``` ```java // SecurityService.java // การใช้งาน MANDATORY ที่ถูกต้อง @Service public class SecurityService { private final AuditService auditService; @Transactional public void changeUserPassword(Long userId, String newPassword) { // การดำเนินการที่ละเอียดอ่อน... updatePassword(userId, newPassword); // Audit ต้องอยู่ในธุรกรรมเดียวกัน // MANDATORY บังคับใช้ข้อจำกัดทางสถาปัตยกรรมนี้ auditService.logCriticalAction("PASSWORD_CHANGE", userId); } // ข้อผิดพลาด: เรียกโดยตรงโดยไม่มีธุรกรรม public void badUsage() { // โยน IllegalTransactionStateException เพราะไม่มี TX auditService.logCriticalAction("TEST", 1L); } } ``` ## กับดักทั่วไปในการสัมภาษณ์ ### คำถามที่ 7: ทำไม @Transactional ไม่ทำงานในการเรียกภายใน? หนึ่งในกับดักที่พบบ่อยที่สุด การเรียกเมธอดภายใน (self-invocation) จะข้าม proxy ของ Spring ทำให้ปิดการจัดการธุรกรรม ```java // BrokenService.java // ข้อผิดพลาดคลาสสิก: self-invocation @Service public class BrokenService { private final ItemRepository itemRepository; public void processItems(List itemIds) { for (Long id : itemIds) { // กับดัก: การเรียกภายใน → ข้าม proxy // @Transactional บน processItem() ถูกละเลย processItem(id); } } @Transactional public void processItem(Long itemId) { // ธุรกรรมนี้ไม่เคยถูกสร้างในการเรียกภายใน Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` วิธีแก้ปัญหาเพื่อหลีกเลี่ยงกับดักนี้: ```java // วิธีที่ 1: Self-injection @Service public class FixedServiceWithSelfInjection { private final ItemRepository itemRepository; @Lazy @Autowired private FixedServiceWithSelfInjection self; // Self-injection public void processItems(List itemIds) { for (Long id : itemIds) { // เรียกผ่าน proxy → @Transactional ทำงาน self.processItem(id); } } @Transactional public void processItem(Long itemId) { // ธุรกรรมถูกสร้างอย่างถูกต้อง Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } // วิธีที่ 2: แยกเป็นสอง service (แนะนำ) @Service public class ItemOrchestrator { private final ItemProcessor processor; public void processItems(List itemIds) { for (Long id : itemIds) { // เรียกไปยัง bean อื่น → proxy ทำงาน processor.processItem(id); } } } @Service public class ItemProcessor { private final ItemRepository itemRepository; @Transactional public void processItem(Long itemId) { // ธุรกรรมจัดการอย่างถูกต้อง Item item = itemRepository.findById(itemId).orElseThrow(); item.setStatus("PROCESSED"); itemRepository.save(item); } } ``` ### คำถามที่ 8: จัดการ exception และ rollback อย่างไร? โดยเริ่มต้น Spring จะ rollback เฉพาะ RuntimeException และ Error Checked exception จะไม่กระตุ้น rollback อัตโนมัติ ```java // TransactionRollbackDemo.java // พฤติกรรม rollback ตามประเภท exception @Service public class TransactionRollbackDemo { private final OrderRepository orderRepository; // RuntimeException → ROLLBACK อัตโนมัติ @Transactional public void methodWithRuntimeException() { orderRepository.save(new Order()); throw new RuntimeException("Error"); // ROLLBACK } // Checked Exception → ไม่ rollback โดยเริ่มต้น @Transactional public void methodWithCheckedException() throws IOException { orderRepository.save(new Order()); throw new IOException("File error"); // COMMIT แม้กระนั้น! } // บังคับ rollback บน checked exception @Transactional(rollbackFor = IOException.class) public void methodWithRollbackFor() throws IOException { orderRepository.save(new Order()); throw new IOException("Error"); // ROLLBACK เพราะ rollbackFor } // ยกเว้น RuntimeException จาก rollback @Transactional(noRollbackFor = BusinessException.class) public void methodWithNoRollbackFor() { orderRepository.save(new Order()); throw new BusinessException("Warning"); // COMMIT แม้มี exception } } ``` การกำหนดค่าที่แนะนำสำหรับกรณีทางธุรกิจ: ```java // BaseTransactionalService.java // การกำหนดค่าธุรกรรมที่สอดคล้อง @Service public abstract class BaseTransactionalService { // Rollback บน exception ทั้งหมด (checked และ 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) { // ไม่ rollback - ต้องการเก็บบันทึกความพยายาม throw new InsufficientFundsException("Insufficient balance"); } return new PaymentResult(payment.getId(), "SUCCESS"); } } ``` ### คำถามที่ 9: การแยกธุรกรรมทำงานกับการกระจายอย่างไร? การแยกและการกระจายเสริมซึ่งกันและกัน การแยกกำหนดการมองเห็นข้อมูลระหว่างธุรกรรมที่เกิดขึ้นพร้อมกัน ```java // IsolationDemo.java // การรวมการแยก + การกระจาย @Service public class IsolationDemo { private final AccountRepository accountRepository; // READ_COMMITTED: เห็นข้อมูลที่ commit โดยธุรกรรมอื่น @Transactional( isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED ) public BigDecimal getAccountBalance(Long accountId) { // อาจเห็นค่าต่างกันหากอ่านซ้ำระหว่างธุรกรรม return accountRepository.findById(accountId) .map(Account::getBalance) .orElse(BigDecimal.ZERO); } // REPEATABLE_READ: รับประกันการอ่านเดียวกันระหว่างธุรกรรม @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(); // แม้ธุรกรรมอื่นจะแก้ไข 'from' ในระหว่างนี้ // เราเห็นค่าเริ่มต้นเสมอ (snapshot) from.debit(amount); to.credit(amount); accountRepository.save(from); accountRepository.save(to); } // SERIALIZABLE: การแยกสูงสุด ไม่มีการทำงานพร้อมกัน @Transactional( isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW ) public void criticalOperation(Long accountId) { // บล็อกธุรกรรมอื่นใด ๆ บนข้อมูลนี้ // ใช้อย่างประหยัด - มีผลกระทบต่อประสิทธิภาพ Account account = accountRepository.findById(accountId).orElseThrow(); account.performCriticalUpdate(); accountRepository.save(account); } } ``` ตารางสรุประดับการแยก: ```text | การแยก | Dirty Read | Non-Repeatable | Phantom | |------------------|------------|----------------|---------| | READ_UNCOMMITTED | เป็นไปได้ | เป็นไปได้ | เป็นไปได้| | READ_COMMITTED | ไม่ | เป็นไปได้ | เป็นไปได้| | REPEATABLE_READ | ไม่ | ไม่ | เป็นไปได้| | SERIALIZABLE | ไม่ | ไม่ | ไม่ | ``` > **ผลกระทบต่อประสิทธิภาพ** > > ยิ่งการแยกเข้มงวดมากเท่าใด ประสิทธิภาพยิ่งได้รับผลกระทบจากล็อกมากขึ้น SERIALIZABLE อาจทำให้เกิดการแย่งชิงอย่างมากในโปรดักชันที่มีการเข้าใช้สูง ## รูปแบบขั้นสูง ### คำถามที่ 10: ใช้รูปแบบ "transactional outbox" อย่างไร? รูปแบบ outbox รับประกันความสอดคล้องระหว่างการแก้ไขฐานข้อมูลและการส่งข้อความ/เหตุการณ์ แม้ในกรณีที่ล้มเหลว ```java // OutboxService.java // รูปแบบ Transactional Outbox @Service public class OutboxService { private final OutboxRepository outboxRepository; // บันทึกเหตุการณ์ในธุรกรรมเดียวกันกับเอนทิตีทางธุรกิจ @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) { // สร้างคำสั่งซื้อ Order order = new Order(request); orderRepository.save(order); // เหตุการณ์ outbox ในธุรกรรมเดียวกัน (MANDATORY) // หาก commit สำเร็จ → ทั้งสองถูกบันทึก // หาก rollback → ไม่มีอะไรถูกบันทึก outboxService.saveEvent( "ORDER", order.getId(), "ORDER_CREATED", toJson(new OrderCreatedEvent(order)) ); return order; } } // OutboxPublisher.java // กระบวนการแยกที่เผยแพร่เหตุการณ์ @Service public class OutboxPublisher { private final OutboxRepository outboxRepository; private final MessageBroker messageBroker; // ธุรกรรมอิสระสำหรับการเผยแพร่แต่ละครั้ง @Scheduled(fixedDelay = 1000) @Transactional(propagation = Propagation.REQUIRES_NEW) public void publishPendingEvents() { List 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); } } } ``` ### คำถามที่ 11: ทดสอบพฤติกรรมการกระจายต่าง ๆ อย่างไร? การทดสอบการกระจายต้องการความสนใจเป็นพิเศษเพื่อตรวจสอบพฤติกรรมธุรกรรมที่คาดหวัง ```java // TransactionPropagationTest.java // การทดสอบพฤติกรรมการกระจาย @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 - ชำระเงินในธุรกรรมเดียวกัน (REQUIRED) paymentService.processPayment(order.getId(), BigDecimal.TEN); // Then - ทั้งสองมองเห็นได้ก่อน 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 ใน REQUIRES_NEW orderId = orderService.createOrderWithAudit(new OrderRequest()); throw new RuntimeException("Simulated failure after audit"); } catch (RuntimeException e) { // ธุรกรรมหลัก rollback } // Then - audit (REQUIRES_NEW) ยังคง commit assertThat(findAuditLog(orderId)).isNotNull(); // แต่คำสั่งซื้อ rollback assertThat(findOrder(orderId)).isNull(); } @Test void mandatory_shouldThrowWithoutTransaction() { // Given - ไม่มีธุรกรรมที่ใช้งานอยู่ // When/Then - ต้องโยน exception assertThatThrownBy(() -> auditService.logCriticalAction("TEST", 1L)) .isInstanceOf(IllegalTransactionStateException.class) .hasMessageContaining("No existing transaction"); } } ``` ```java // PropagationIntegrationTest.java // การทดสอบการรวมระบบกับ rollback และ commit จริง @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 - การประมวลผล batch ด้วย NESTED BatchResult result = orderService.processBatchWithNested( List.of(validItem(), invalidItem(), validItem()) ); // Then - เฉพาะรายการที่ถูกต้องถูกบันทึก assertThat(result.getSuccessCount()).isEqualTo(2); assertThat(result.getFailureCount()).isEqualTo(1); assertThat(orderRepository.findAll().size()).isEqualTo(initialCount + 2); } } ``` ### คำถามที่ 12: แนวปฏิบัติที่ดีที่สุดสำหรับการกำหนดค่าธุรกรรมคืออะไร? การกำหนดค่าธุรกรรมที่สอดคล้องในระดับโครงการช่วยหลีกเลี่ยงความประหลาดใจและทำให้บำรุงรักษาง่ายขึ้น ```java // TransactionConfig.java // การกำหนดค่าธุรกรรมแบบรวมศูนย์ @Configuration @EnableTransactionManagement public class TransactionConfig { // Timeout เริ่มต้นสำหรับธุรกรรมทั้งหมด @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { DataSourceTransactionManager tm = new DataSourceTransactionManager(dataSource); tm.setDefaultTimeout(30); // 30 วินาทีสูงสุดต่อธุรกรรม return tm; } } // BaseService.java // คำอธิบายเริ่มต้นสำหรับ service @Service @Transactional( readOnly = true, // อ่านอย่างเดียวโดยเริ่มต้น rollbackFor = Exception.class // Rollback บน exception ใด ๆ ) public abstract class BaseService { // เมธอดอ่านสืบทอด readOnly = true } // OrderService.java // Service พร้อมการกำหนดค่าที่สอดคล้อง @Service public class OrderService extends BaseService { private final OrderRepository orderRepository; // สืบทอด readOnly = true public Order findById(Long id) { return orderRepository.findById(id).orElseThrow(); } // Override สำหรับการเขียน @Transactional(readOnly = false) public Order createOrder(OrderRequest request) { return orderRepository.save(new Order(request)); } // การกำหนดค่าอย่างชัดเจนสำหรับกรณีสำคัญ @Transactional( readOnly = false, propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE, timeout = 10 ) public void criticalOperation(Long orderId) { // การดำเนินการด้วยการแยกสูงสุดและ timeout สั้น } } ``` รายการตรวจสอบแนวปฏิบัติที่ดีที่สุด: ```text การกำหนดค่าธุรกรรม - รายการตรวจสอบ ✅ readOnly = true โดยเริ่มต้น override อย่างชัดเจนสำหรับการเขียน ✅ rollbackFor = Exception.class เพื่อรวม checked exceptions ✅ Timeout ที่เหมาะสมตามประเภทการดำเนินการ ✅ หลีกเลี่ยงการเรียกภายใน (@Transactional ถูกละเลย) ✅ REQUIRES_NEW เฉพาะเมื่อต้องการ commit อิสระ ✅ MANDATORY เพื่อรับประกันบริบทธุรกรรม ✅ ทดสอบพฤติกรรม rollback อย่างชัดเจน ✅ ตรวจสอบธุรกรรมที่ทำงานนาน ✅ บันทึกการเลือกการกระจายที่ไม่เป็นมาตรฐาน ``` ## สรุป การกระจายธุรกรรมเป็นแนวคิดพื้นฐานที่ได้รับการประเมินในการสัมภาษณ์ Spring Boot ประเด็นสำคัญที่ต้องจดจำ: **ประเภทการกระจายทั่วไป:** - ✅ REQUIRED (เริ่มต้น): เข้าร่วมหรือสร้างธุรกรรม - ✅ REQUIRES_NEW: ธุรกรรมอิสระ commit แยก - ✅ NESTED: savepoint สำหรับ rollback บางส่วน - ✅ MANDATORY: ต้องการธุรกรรมที่มีอยู่ **กับดักที่ต้องหลีกเลี่ยง:** - ✅ Self-invocation: ข้าม proxy, @Transactional ถูกละเลย - ✅ Checked exception: ไม่ rollback โดยเริ่มต้น - ✅ REQUIRES_NEW บนข้อมูลเดียวกัน: ความเสี่ยง deadlock - ✅ ไม่มี timeout: ธุรกรรมถูกบล็อกอย่างไม่จำกัด **แนวปฏิบัติที่ดีที่สุด:** - ✅ readOnly = true โดยเริ่มต้น - ✅ rollbackFor = Exception.class อย่างเป็นระบบ - ✅ แยก service เพื่อหลีกเลี่ยง self-invocation - ✅ ทดสอบพฤติกรรม rollback อย่างชัดเจน การเชี่ยวชาญการกระจายธุรกรรมแสดงถึงความเข้าใจอย่างลึกซึ้งใน Spring และการจัดการข้อมูล แนวคิดเหล่านี้จำเป็นในการออกแบบแอปพลิเคชันที่แข็งแกร่งและผ่านการสัมภาษณ์ทางเทคนิคได้อย่างประสบความสำเร็จ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/spring-boot/spring-boot-transaction-propagation-interview