สัมภาษณ์ Spring Boot: การกระจายธุรกรรม
เชี่ยวชาญการกระจายธุรกรรมใน Spring Boot: REQUIRED, REQUIRES_NEW, NESTED และอื่น ๆ 12 คำถามสัมภาษณ์พร้อมโค้ดและกับดักทั่วไป

การกระจายธุรกรรมเป็นแนวคิดพื้นฐานใน Spring Boot ที่ได้รับการประเมินอย่างสม่ำเสมอในการสัมภาษณ์ทางเทคนิค การเข้าใจว่าธุรกรรมโต้ตอบกันอย่างไรระหว่างเมธอดที่มีคำอธิบาย @Transactional ช่วยหลีกเลี่ยงข้อบกพร่องเล็กน้อยในระบบโปรดักชันและช่วยออกแบบสถาปัตยกรรมที่แข็งแกร่ง
ผู้สัมภาษณ์จะทดสอบความสามารถในการเลือกระดับการกระจายที่เหมาะสมตามบริบททางธุรกิจ การสามารถอธิบายว่าทำไมจึงใช้ REQUIRES_NEW แทน REQUIRED ในกรณีที่เฉพาะเจาะจงจะสร้างความแตกต่าง
พื้นฐานของการกระจายธุรกรรม
คำถามที่ 1: การกระจายธุรกรรมใน Spring คืออะไร?
การกระจายกำหนดพฤติกรรมของเมธอดที่เป็นธุรกรรมเมื่อถูกเรียกในบริบทของธุรกรรมที่มีอยู่ มันตอบคำถาม: "จะเกิดอะไรขึ้นเมื่อเมธอด @Transactional เรียกเมธอดอื่นที่มีคำอธิบายเช่นเดียวกัน?"
// แสดงแนวคิดของการกระจาย
@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 คือการกระจายเริ่มต้น หากมีธุรกรรม เมธอดจะเข้าร่วมกับมัน มิฉะนั้นจะมีการสร้างธุรกรรมใหม่ นี่เป็นพฤติกรรมที่พบบ่อยที่สุดและเป็นธรรมชาติที่สุด
// 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()));
}
}ไดอะแกรมด้านล่างแสดงการไหลของธุรกรรม:
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 โดยไม่คำนึงถึงผลของธุรกรรมหลัก
// 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:
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 ถูกเก็บรักษาREQUIRES_NEW อาจทำให้เกิด deadlock หากธุรกรรมใหม่เข้าถึงทรัพยากรเดียวกันที่ถูกล็อกโดยธุรกรรมที่ถูกหยุดชั่วคราว หลีกเลี่ยงการใช้ REQUIRES_NEW เพื่อแก้ไขตารางเดียวกับธุรกรรมหลัก
คำถามที่ 4: อธิบายการกระจาย NESTED และความแตกต่างจาก REQUIRES_NEW
NESTED สร้าง savepoint ภายในธุรกรรมปัจจุบัน หากเมธอดล้มเหลว เฉพาะการเปลี่ยนแปลงตั้งแต่ savepoint จะถูก rollback ไม่ใช่ธุรกรรมหลักทั้งหมด
// NESTED: savepoint ในธุรกรรมหลัก
@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 {
// แต่ละรายการประมวลผลด้วย 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:
NESTED:
├── ใช้ savepoint ใน TX หลัก
├── เมื่อล้มเหลว → rollback ไปที่ savepoint
├── หาก TX หลัก rollback → NESTED ก็ rollback ด้วย
└── ประสิทธิภาพดีกว่า (ไม่มีการเชื่อมต่อใหม่)
REQUIRES_NEW:
├── สร้างธุรกรรมที่อิสระอย่างสมบูรณ์
├── เมื่อล้มเหลว → rollback เฉพาะ TX ลูก
├── หาก TX หลัก rollback → TX ลูก commit ไปแล้ว
└── ต้องการการเชื่อมต่อใหม่การกระจาย NESTED ต้องการการรองรับ savepoint ของ JDBC ฐานข้อมูลสมัยใหม่ส่วนใหญ่ (PostgreSQL, MySQL, Oracle) รองรับสิ่งนี้ ตรวจสอบความเข้ากันได้ก่อนใช้
ประเภทการกระจายขั้นสูง
คำถามที่ 5: เมื่อใดควรใช้ SUPPORTS และ NOT_SUPPORTED?
SUPPORTS ทำงานในธุรกรรมที่มีอยู่หากมี มิฉะนั้นไม่มีธุรกรรม NOT_SUPPORTED หยุดธุรกรรมที่มีอยู่ใด ๆ และทำงานโดยไม่มีธุรกรรม
// 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 หลักดำเนินต่อหลังจากนั้น
}
}// ตัวอย่างการใช้งานร่วมกัน
@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 หากมี
// 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);
}
}// การใช้งาน 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);
}
}พร้อมที่จะพิชิตการสัมภาษณ์ Spring Boot แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
กับดักทั่วไปในการสัมภาษณ์
คำถามที่ 7: ทำไม @Transactional ไม่ทำงานในการเรียกภายใน?
หนึ่งในกับดักที่พบบ่อยที่สุด การเรียกเมธอดภายใน (self-invocation) จะข้าม proxy ของ Spring ทำให้ปิดการจัดการธุรกรรม
// ข้อผิดพลาดคลาสสิก: self-invocation
@Service
public class BrokenService {
private final ItemRepository itemRepository;
public void processItems(List<Long> 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);
}
}วิธีแก้ปัญหาเพื่อหลีกเลี่ยงกับดักนี้:
// วิธีที่ 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) {
// เรียกผ่าน 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<Long> 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 อัตโนมัติ
// พฤติกรรม 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
}
}การกำหนดค่าที่แนะนำสำหรับกรณีทางธุรกิจ:
// การกำหนดค่าธุรกรรมที่สอดคล้อง
@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: การแยกธุรกรรมทำงานกับการกระจายอย่างไร?
การแยกและการกระจายเสริมซึ่งกันและกัน การแยกกำหนดการมองเห็นข้อมูลระหว่างธุรกรรมที่เกิดขึ้นพร้อมกัน
// การรวมการแยก + การกระจาย
@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);
}
}ตารางสรุประดับการแยก:
| การแยก | Dirty Read | Non-Repeatable | Phantom |
|------------------|------------|----------------|---------|
| READ_UNCOMMITTED | เป็นไปได้ | เป็นไปได้ | เป็นไปได้|
| READ_COMMITTED | ไม่ | เป็นไปได้ | เป็นไปได้|
| REPEATABLE_READ | ไม่ | ไม่ | เป็นไปได้|
| SERIALIZABLE | ไม่ | ไม่ | ไม่ |ยิ่งการแยกเข้มงวดมากเท่าใด ประสิทธิภาพยิ่งได้รับผลกระทบจากล็อกมากขึ้น SERIALIZABLE อาจทำให้เกิดการแย่งชิงอย่างมากในโปรดักชันที่มีการเข้าใช้สูง
รูปแบบขั้นสูง
คำถามที่ 10: ใช้รูปแบบ "transactional outbox" อย่างไร?
รูปแบบ outbox รับประกันความสอดคล้องระหว่างการแก้ไขฐานข้อมูลและการส่งข้อความ/เหตุการณ์ แม้ในกรณีที่ล้มเหลว
// รูปแบบ 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<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);
}
}
}คำถามที่ 11: ทดสอบพฤติกรรมการกระจายต่าง ๆ อย่างไร?
การทดสอบการกระจายต้องการความสนใจเป็นพิเศษเพื่อตรวจสอบพฤติกรรมธุรกรรมที่คาดหวัง
// การทดสอบพฤติกรรมการกระจาย
@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");
}
}// การทดสอบการรวมระบบกับ 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: แนวปฏิบัติที่ดีที่สุดสำหรับการกำหนดค่าธุรกรรมคืออะไร?
การกำหนดค่าธุรกรรมที่สอดคล้องในระดับโครงการช่วยหลีกเลี่ยงความประหลาดใจและทำให้บำรุงรักษาง่ายขึ้น
// การกำหนดค่าธุรกรรมแบบรวมศูนย์
@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 สั้น
}
}รายการตรวจสอบแนวปฏิบัติที่ดีที่สุด:
การกำหนดค่าธุรกรรม - รายการตรวจสอบ
✅ readOnly = true โดยเริ่มต้น override อย่างชัดเจนสำหรับการเขียน
✅ rollbackFor = Exception.class เพื่อรวม checked exceptions
✅ Timeout ที่เหมาะสมตามประเภทการดำเนินการ
✅ หลีกเลี่ยงการเรียกภายใน (@Transactional ถูกละเลย)
✅ REQUIRES_NEW เฉพาะเมื่อต้องการ commit อิสระ
✅ MANDATORY เพื่อรับประกันบริบทธุรกรรม
✅ ทดสอบพฤติกรรม rollback อย่างชัดเจน
✅ ตรวจสอบธุรกรรมที่ทำงานนาน
✅ บันทึกการเลือกการกระจายที่ไม่เป็นมาตรฐานพร้อมที่จะพิชิตการสัมภาษณ์ Spring Boot แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
สรุป
การกระจายธุรกรรมเป็นแนวคิดพื้นฐานที่ได้รับการประเมินในการสัมภาษณ์ 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 และการจัดการข้อมูล แนวคิดเหล่านี้จำเป็นในการออกแบบแอปพลิเคชันที่แข็งแกร่งและผ่านการสัมภาษณ์ทางเทคนิคได้อย่างประสบความสำเร็จ
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

30 คำถามสัมภาษณ์ Spring Boot: คู่มือฉบับเต็มสำหรับนักพัฒนา Java
เตรียมพร้อมสัมภาษณ์ Spring Boot ด้วย 30 คำถามหลักครอบคลุม auto-configuration, starter, Spring Data JPA, ความปลอดภัย และการทดสอบ

Spring Modulith: สถาปัตยกรรม Monolith แบบโมดูลาร์
เรียนรู้ Spring Modulith เพื่อสร้าง monolith แบบโมดูลาร์ใน Java สถาปัตยกรรม โมดูล อีเวนต์อะซิงโครนัส และการทดสอบด้วย Spring Boot 3

สัมภาษณ์ Spring Batch 5: Partitioning, Chunk และ Fault Tolerance
เชี่ยวชาญการสัมภาษณ์ Spring Batch 5: 15 คำถามสำคัญเกี่ยวกับ partitioning การประมวลผลแบบ chunk และความทนทานต่อข้อผิดพลาด พร้อมตัวอย่างโค้ด Java 21