【SpringBoot/MyBatis】单机数据库事务处理(不合理转账主动抛出运行期异常使数据库回滚)
本文涉及Springboot版本:2.5.4
例程:https://files.cnblogs.com/files/heyang78/myBank_transactional_210909_0526.rar
前言:使用JDBC操作单机数据库时,利用Connection对事务处理保证多个操作的不可分割性是比较简单方便的,在SpringBoot起,Spring开始建议在方法上加@Transactional来完成事务,本文就是用来演示下具体做法。
准备工作:
新建一个Account表,
create table account( id int, customer_id nvarchar2(20), balance int, primary key(id));
插入五条记录:
insert into account(id,customer_id,balance) values(1,'001',1000); insert into account(id,customer_id,balance) values(2,'002',1000); insert into account(id,customer_id,balance) values(3,'003',1000); insert into account(id,customer_id,balance) values(4,'004',1000); insert into account(id,customer_id,balance) values(5,'005',1000);
下面将模拟在两个账户之间转账,比如从002账户转出100元给003账户,程序的目的是确保转出和转入要么同时成功,要么同时失败。
第二步:书写对账户操作的SQL
转入和转出对Accout表的记录来说只是一个Update操作,因此我们可以快速在Mapper里快速写出SQL:
@Mapper public interface AccountMapper { @Update("Update account set balance=balance+#{count} where customer_id=#{customer_id}") int add(int count,String customer_id); }
有了这个函数,我们就能执行转账业务,还是002账户转出100元给003账户,如下调用两次就能完成。
add(-100,"002");
add(100,"003");
下面的任务就是确保上面两步是原子操作(德谟克利特:原子是不可分割的.)
第三步:书写Service代码
@Component public class AccountService { @Resource private AccountMapper amapper=null; @Transactional public void transfer(int amount,String fromAccount,String toAcccount){ int count=amapper.add(-amount, fromAccount); if(count==0) { throw new IllegalStateException("对转出账户:"+fromAccount+"操作,更新记录数为0.只有可能是该账户不存在。"); } count=amapper.add(amount, toAcccount); if(count==0) { throw new IllegalStateException("对转入账户:"+toAcccount+"操作,更新记录数为0.只有可能是该账户不存在。"); } } }
现在要着重说明一下了,transfer方法外加了Transactional注解,说明这个函数已经原子化了,只要有运行期异常(RuntimeException)抛出,Spring就会让数据库回滚。
也就是说,把上面的两句:
amapper.add(-amount, fromAccount);
amapper.add(amount, toAcccount);
直接放到方法里,Spring就能保证两个操作要么同时成功,要么同时失败,如果它做不到,那就等于自砸招牌。
但是,转账不是这么容易的,add方法里面的SQL运行起来,除非数据库突然修改了字段导致SQL运行出错,add方法是不会抛出RutimeException的。
有些同学可能还没有意识到问题的严重性,比如有一个不存在的账户009,无论对其转出还是转入,update account set balance=balance+money where customer_id='009'这一句都会运行成功的,只是账户不存在时更新的记录数为零,账户存在时更新的记录数为一。
所以我们必须要取得add的返回值,如果返回值为零即更新的记录数为零,立即抛出运行期异常让Spring告诉数据库回滚。
于是便有了上面的代码。这段代码说明程序员要根据业务写代码,数据库和Spring毕竟还是机器,它们不可能知道什么样的业务是非法的。
第四步:测试
首先测试正常情况,从001转出100元到002账户,它们的余额在转之前都是1000,转之后001是900,002是1100,两个账户的总额2000不变。
@SpringBootTest class MyBankApplicationTests { @Autowired private AccountService aService; @Test void test() { aService.transfer(100, "001", "002"); } }
然后我们执行JUnitTest,再看看数据库前后发生了什么。
如所预料,001变成900,002变成1100.
让我们恢复数据库原状,即每个账户都是1000的状态。
下面再进行一次非法测试,即从002账户转出100元到不存在的009账户,预期情况是,第二次执行add函数发现返回值为0,立即抛出异常,让数据库回滚,之前对002的转出自然会回滚,002账户里还应该是原来的余额1000.
开始执行JUnit测试:
异常果然抛出,测试如期失败,再看看数据库情况:
如期没有改变,这说明根据业务抛出的IllegalStateException异常确实让数据库回滚了。
但这个程序是存在缺陷的,因为IllegalStateException与实际业务无关,如果是抛出自定义的业务性异常使得数据库回滚就更好了,如果向知道怎么做,请看续篇 https://www.cnblogs.com/heyang78/p/15245295.html。
--END--