likes
comments
collection
share

Spring事务嵌套问题详解

作者站长头像
站长
· 阅读数 14

当一个事务中又嵌套了其他事务时,就形成了事务嵌套的情况。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 方法中的事务不受影响,因为它是一个新事务。