Khái niệm về giao dịch (Transaction) trong phát triển ứng dụng
Trong các hệ thống phần mềm, đặc biệt là các ứng dụng liên quan đến tài chính, quản lý giao dịch đóng vai trò sống còn để đảm bảo tính toàn vẹn của dữ liệu. Một ví dụ điển hình là quy trình chuyển tiền giữa hai tài khoản: hệ thống phải thực hiện trừ tiền ở tài khoản gửi và cộng tiền vào tài khoản nhận. Nếu một trong hai bước thất bại, toàn bộ quá trình phải được hủy bỏ (rollback) để tránh tình trạng tiền bị mất hoặc tự nhiên sinh ra.
Một giao dịch tiêu chuẩn cần tuân thủ 4 thuộc tính ACID:
- Atomicity (Tính nguyên tử): Một giao dịch là một đơn vị công việc không thể chia cắt. Tất cả các thao tác bên trong giao dịch phải thành công hoàn toàn hoặc thất bại hoàn toàn.
- Consistency (Tính nhất quán): Sau khi giao dịch kết thúc, dữ liệu phải ở trạng thái hợp lệ theo đúng các quy tắc của hệ thống.
- Isolation (Tính cô lập): Các giao dịch thực thi đồng thời không được can thiệp lẫn nhau. Kết quả của một giao dịch chưa hoàn tất không được hiển thị cho các giao dịch khác.
- Durability (Tính bền vững): Một khi giao dịch đã được xác nhận (commit), kết quả đó phải được lưu trữ vĩnh viễn vào hệ thống, kể cả khi xảy ra sự cố phần cứng.
Các vấn đề phát sinh khi thiếu tính cô lập
Khi nhiều luồng truy cập dữ liệu cùng lúc mà không có cơ chế cô lập tốt, hệ thống có thể gặp phải các lỗi:
- Dirty Read (Đọc rác): Đọc dữ liệu từ một giao dịch khác chưa được commit.
- Non-repeatable Read (Đọc không lặp lại): Trong cùng một giao dịch, hai lần đọc cùng một dòng dữ liệu cho ra kết quả khác nhau do một giao dịch khác vừa cập nhật và commit.
- Phantom Read (Đọc ảo): Một truy vấn lấy ra một tập hợp dữ liệu, nhưng lần sau truy vấn lại thấy số lượng bản ghi thay đổi do một giao dịch khác vừa thêm hoặc xóa dòng.
Thiết lập môi trường mô phỏng
Để minh họa các cách quản lý giao dịch trong Spring, chúng ta sử dụng một dự án Maven với các phụ thuộc cơ bản sau:
<dependencies>
<!-- Spring Core & JDBC -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.10</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.26</version>
</dependency>
</dependencies>
Cấu trúc tầng DAO và Service để xử lý nghiệp vụ chuyển tiền:
// Interface cho Repository
public interface AccountRepository {
void deductBalance(String username, double amount);
void addBalance(String username, double amount);
}
// Triển khai Repository bằng JdbcTemplate
public class AccountRepositoryImpl implements AccountRepository {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public void deductBalance(String username, double amount) {
String sql = "UPDATE accounts SET balance = balance - ? WHERE username = ?";
jdbcTemplate.update(sql, amount, username);
}
@Override
public void addBalance(String username, double amount) {
String sql = "UPDATE accounts SET balance = balance + ? WHERE username = ?";
jdbcTemplate.update(sql, amount, username);
}
}
Cách 1: Quản lý giao dịch bằng lập trình (Programmatic Transaction)
Phương pháp này sử dụng TransactionTemplate để kiểm soát giao dịch trực tiếp trong mã nguồn Java. Cách này linh hoạt nhưng khiến mã nghiệp vụ bị trộn lẫn với mã quản lý hạ tầng.
public class BankService {
private AccountRepository accountRepo;
private TransactionTemplate transactionTemplate;
public void setAccountRepo(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
public void performTransfer(final String from, final String to, final double amount) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
accountRepo.deductBalance(from, amount);
// Giả lập lỗi phát sinh
int error = 10 / 0;
accountRepo.addBalance(to, amount);
}
});
}
}
Cấu hình Bean trong XML:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager" ref="transactionManager" />
</bean>
Cách 2: Quản lý giao dịch bằng cấu hình XML (Declarative Transaction)
Phương pháp này sử dụng AOP để tách rời logic giao dịch ra khỏi mã nguồn. Bạn chỉ cần cấu hình các quy tắc trong file XML.
<!-- Định nghĩa Transaction Advice -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="perform*" propagation="REQUIRED" />
<tx:method name="get*" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- Cấu hình AOP Pointcut -->
<aop:config>
<aop:pointcut id="serviceOps" expression="execution(* com.example.service.BankService.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceOps" />
</aop:config>
Cách 3: Sử dụng Annotation @Transactional
Đây là cách phổ biến nhất hiện nay nhờ sự gọn nhẹ và dễ hiểu. Bạn chỉ cần kích hoạt tính năng này trong cấu hình và thêm chú thích vào các phương thức cần quản lý.
Kích hoạt trong file XML:
<tx:annotation-driven transaction-manager="transactionManager" />
Sử dụng trong Service class:
@Service
public class BankServiceImpl implements BankService {
@Autowired
private AccountRepository accountRepo;
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT)
public void performTransfer(String from, String to, double amount) {
accountRepo.deductBalance(from, amount);
accountRepo.addBalance(to, amount);
}
}
Các cấp độ lan truyền (Propagation) và cô lập (Isolation)
Khi sử dụng @Transactional, bạn cần lưu ý các thuộc tính quan trọng:
- Propagation (Lan truyền): Định nghĩa cách một phương thức tham gia vào một giao dịch hiện có (ví dụ:
REQUIRED- dùng giao dịch hiện tại hoặc tạo mới;REQUIRES_NEW- luôn tạo giao dịch mới). - Isolation (Cô lập): Xác định mức độ nhìn thấy dữ liệu giữa các giao dịch (ví dụ:
READ_COMMITTED- chỉ đọc dữ liệu đã được xác nhận). - Rollback Rules: Spring mặc định rollback khi có
RuntimeException, nhưng bạn có thể tùy chỉnh thông quarollbackFor.