Spring事务嵌套问题详解
当一个事务中又嵌套了其他事务时,就形成了事务嵌套的情况。Spring框架支持事务嵌套,通过在外层事务内部启动一个新的事务,使内层事务成为外层事务的一部分,一起提交或回滚。
嵌套事务与普通事务的区别在于,嵌套事务是一个子事务,必须在父事务中提交或回滚。如果父事务回滚,则所有嵌套的子事务也必须回滚;如果父事务提交,则所有嵌套的子事务也必须提交。
下面我们通过代码来演示Spring的事务嵌套。
首先,我们创建一个AccountService接口,里面定义了两个方法:transfer方法和nestedTransfer方法。
javaCopy code
public interface AccountService {
void transfer(String fromAccount, String toAccount, double amount);
void nestedTransfer(String fromAccount, String toAccount, double amount);
}
然后,我们创建一个AccountServiceImpl实现AccountService接口,并且添加@Transactional注解表示启用事务。
javaCopy code
@Service
@Transactional
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void transfer(String fromAccount, String toAccount, double amount) {
accountDao.withdraw(fromAccount, amount);
accountDao.deposit(toAccount, amount);
}
@Override
@Transactional(propagation = Propagation.NESTED)
public void nestedTransfer(String fromAccount, String toAccount, double amount) {
accountDao.withdraw(fromAccount, amount);
accountDao.deposit(toAccount, amount);
}
}
在AccountServiceImpl中,我们使用了注解@Transactional来启用事务,同时在nestedTransfer方法上使用了注解@Transactional(propagation = Propagation.NESTED)来开启嵌套事务。
为了方便演示,我们创建一个AccountDao接口,里面定义了两个方法:withdraw方法和deposit方法,用于转账。
javaCopy code
public interface AccountDao {
void withdraw(String fromAccount, double amount);
void deposit(String toAccount, double amount);
}
然后,我们创建一个AccountDaoImpl实现AccountDao接口,并且添加@Transactional注解表示启用事务。
javaCopy code
@Repository
@Transactional
public class AccountDaoImpl implements AccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public void withdraw(String fromAccount, double amount) {
String sql = "UPDATE account SET balance = balance - ? WHERE account_number = ?";
jdbcTemplate.update(sql, amount, fromAccount);
}
@Override
public void deposit(String toAccount, double amount) {
String sql = "UPDATE account SET balance = balance + ? WHERE account_number = ?";
jdbcTemplate.update(sql, amount, toAccount);
}
}
在AccountDaoImpl中,我们使用了注解@Transactional来启用事务。
现在我们来模拟一个转账操作,我们先将小明的余额从1000元转到小红的账户中。
javaCopy code
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional
@Override
public void transferMoney() {
try {
transferMoneyFromMingToHong(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void transferMoneyFromMingToHong(int amount) {
jdbcTemplate.update("UPDATE account SET balance = balance - ? WHERE name = ?", amount, "ming");
transferMoneyToHong(amount);
}
@Transactional(propagation = Propagation.NESTED)
public void transferMoneyToHong(int amount) {
jdbcTemplate.update("UPDATE account SET balance = balance + ? WHERE name = ?", amount, "hong");
if (amount == 1000) {
throw new RuntimeException("模拟转账过程中出现异常");
}
}
}
这段代码中,我们将转账的整个过程分成了两个子事务,分别是 transferMoneyFromMingToHong
和 transferMoneyToHong
,并且设置了不同的事务传播级别。
首先,我们在 transferMoney
方法上添加了 @Transactional
注解,这样整个转账过程就是一个完整的事务。然后在 transferMoneyFromMingToHong
方法上设置了 Propagation.REQUIRES_NEW
事务传播级别,这样子事务会挂起父事务并且开启一个新的事务执行。在 transferMoneyToHong
方法上设置了 Propagation.NESTED
事务传播级别,这样子事务会嵌套在父事务中执行。
接下来,我们来测试一下这个转账操作。我们先在数据库中创建一个名为 account
的表,表结构如下:
sqlCopy code
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
balance INT
);
然后向表中插入两条记录,分别是小明和小红的账户信息:
sqlCopy code
INSERT INTO account (name, balance) VALUES ('ming', 1000);
INSERT INTO account (name, balance) VALUES ('hong', 0);
最后,我们在测试代码中调用 transferMoney
方法:
javaCopy code
@RunWith(SpringRunner.class)
@SpringBootTest
public class TransactionTest {
@Autowired
private AccountService accountService;
@Test
public void testTransferMoney() {
accountService.transferMoney();
}
}
我们来分析一下代码执行过程。首先,transferMoney
方法启动一个完整的事务,并且调用 transferMoneyFromMingToHong
方法,这个方法启动了一个新的事务,并且执行了扣除小明账户余额的操作。然后,这个方法调用 transferMoneyToHong
方法
javaCopy code
@Service
public class UserService {
@Autowired
private UserDAO userDAO;
@Transactional(rollbackFor = Exception.class)
public void transferMoney(String fromUser, String toUser, int amount) throws Exception {
User from = userDAO.getUserByName(fromUser);
User to = userDAO.getUserByName(toUser);
transferMoneyToHong(from, to, amount);
}
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void transferMoneyToHong(User from, User to, int amount) throws Exception {
try {
from.setBalance(from.getBalance() - amount);
userDAO.updateUserBalance(from);
throw new RuntimeException("故意抛出异常");
} catch (Exception e) {
e.printStackTrace();
}
to.setBalance(to.getBalance() + amount);
userDAO.updateUserBalance(to);
}
}
在这个例子中,我们在 transferMoneyToHong
方法中故意抛出了一个异常,模拟转账过程中可能会遇到的异常情况。同时,我们将 transferMoneyToHong
方法的事务传播属性设置为 Propagation.REQUIRES_NEW
,这意味着这个方法的事务是一个新事务,不受外部方法的事务影响。
这样,在 transferMoney
方法中的事务如果回滚,仅仅是 transferMoney
方法自己的事务回滚,而 transferMoneyToHong
方法中的事务不受影响,因为它是一个新事务。
转载自:https://juejin.cn/post/7202226704223830071