【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--

 

posted @ 2021-09-09 05:24  逆火狂飙  阅读(272)  评论(0编辑  收藏  举报
生当作人杰 死亦为鬼雄 至今思项羽 不肯过江东