zoukankan      html  css  js  c++  java
  • 【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--

  • 相关阅读:
    语音合成
    JAVA的18条BASE
    Java关键字final、static使用总结
    JAVA学习之路:不走弯路,就是捷径
    每个java初学者都应该搞懂的问题
    Tomcat5.5.9+JSP经典配置实例
    FineUI控件集合
    AngularJS基础
    数据库优化方案之SQL脚本优化
    数据库分库分表策略之MS-SQL读写分离方案
  • 原文地址:https://www.cnblogs.com/heyang78/p/15245291.html
Copyright © 2011-2022 走看看