• 中文
    • English
  • 注册
  • 查看作者
  • Spring:事务控制

    一.  数据准备

    事务控制是数据库区别与文件系统的重要特征之一,作用在于保证数据库的完整性,我们来举一个例子

    假设张甲给张一转账50元,这个操作主要分为两部分:

    1.  从张甲账户减去50元,并且张甲的账户余额大于等于50

    2.  从张一的账户上增加50元

    两个部分应该作为一个整体看待,如果整个过程有任何一个部分出错,都应该将张甲和张一的数据恢复到原始的状态,我们先来准备相关数据,来看一下没有事务控制下的转账操作:

    1.  Wallet

    CREATE TABLE wallet (
        id int(6) primary key AUTO_INCREMENT,
        name VARCHAR(10),
        balance DOUBLE(6,2)
    )
    
    INSERT INTO wallet (name, balance) VALUE ('张甲',100.0);
    INSERT INTO wallet (name, balance) VALUE ('张一',0.0);

    2.  WalletDao

    package io.zhangjia.spring.dao;
    
    public interface WalletDao {
        int add(Integer id,Double money);
        int sub(Integer id,Double money);
        Double queryBalanceById(Integer id);
    }

    3.  WalletDaoImpl

    在使用@Repository注解的时候,我们一般会使用其接口的首字母小写作为bean的ID,而不采用默认的walletDaoImpl

    package io.zhangjia.spring.dao.impl;
    
    import io.zhangjia.spring.dao.WalletDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    @Repository("walletDao") //一般使用其接口的首字母小写作为bean的ID
    public class WalletDaoImpl implements WalletDao {
        @Autowired
        private JdbcTemplate jdbcTemplate;
        @Override
        public int add(Integer id, Double money) {
            String sql = "UPDATE wallet SET balance = balance + ? WHERE id = ?";
            return jdbcTemplate.update(sql,money,id);
        }
    
        @Override
        public int sub(Integer id, Double money) {
            String sql = "UPDATE wallet SET balance = balance - ? WHERE id = ?";
            return jdbcTemplate.update(sql,money,id);
        }
    
        @Override
        public Double queryBalanceById(Integer id) {
            String sql = "SELECT balance FROM wallet WHERE id = ?";
            return jdbcTemplate.queryForObject(sql,Double.class,id);
        }
    }

    4.  WalletService

    package io.zhangjia.spring.service;
    
    public interface WalletService {
        int transfer(Integer fromId,Integer toId,Double money);
    
        double getUserBalance(Integer id);
    }

    5.  WalletServiceImpl

    package io.zhangjia.spring.dao.impl;
    
    import io.zhangjia.spring.dao.WalletDao;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Repository;
    
    @Repository("walletDao") //一般使用其接口的首字母小写作为bean的ID
    public class WalletDaoImpl implements WalletDao {
        @Autowired
        private JdbcTemplate jdbcTemplate;
        @Override
        public int add(Integer id, Double money) {
            String sql = "UPDATE wallet SET balance = balance + ? WHERE id = ?";
            return jdbcTemplate.update(sql,money,id);
        }
    
        @Override
        public int sub(Integer id, Double money) {
            String sql = "UPDATE wallet SET balance = balance - ? WHERE id = ?";
            return jdbcTemplate.update(sql,money,id);
        }
    
        @Override
        public Double queryBalanceById(Integer id) {
            String sql = "SELECT balance FROM wallet WHERE id = ?";
            return jdbcTemplate.queryForObject(sql,Double.class,id);
        }
    }

    6.  applicationContext.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    
        <context:component-scan base-package="io.zhangjia.spring"/>
    
    
        <context:property-placeholder location="jdbc.properties"/>
    
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="driverClassName" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </bean>
    
    
        <bean class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
    </beans>

    二.  转账测试

     测试类

    public class Test {
        public static void main(String[] args) {
    //        初始化Spring的IOC容器
            ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
            WalletService walletService = (WalletService)context.getBean("walletService");
            walletService.transfer(1,2,50d);
            context.close();
    
        }
    }

    当我们执行两次测试类后,张甲的余额已经变成了0,张一的余额变成了100,此时如果再执行第三次测试类,则张甲的余额会变成-50,张一的余额会变成150,这在实际的项目中,是不允许发生的事情,这里我们先不采用事务控制,而是通过在Service层添加相关判断来解决这问题:

    将WalletDaoImol的sub方法修改为:

    @Override
    public int sub(Integer id, Double money) {
        //获取当前账户的余额
        Double balance = queryBalanceById(id);
        if(balance < money){
            //余额不足
            return 0;
        }
    
        String sql = "UPDATE wallet SET balance = balance - ? WHERE id = ?";
        return jdbcTemplate.update(sql,money,id);
    }

    将WalletServiceImpl的transfer修改为:

    @Override
    public int transfer(Integer fromId, Integer toId, Double money) {
       int sub = walletDao.sub(fromId,money);
        if(sub == 0) {
    //            如果余额为0,则不继续执行
            return 0;
        }
       int add = walletDao.add(toId,money);
       return  sub * add;
    }

    此时无论再执行测试类,当张甲的余额为0的时候,便不会再发生转账行为。这种方式虽然能解决问题,但是在Dao和Service中都加了大量的if判断,并不是一种良好的解决方法。我们可以通过下面的方式,省去Service中的判断语句

    在WalletDaoImol的sub方法中,我们除了通过return 0 的方式来组织sql语句的执行外,还可以使用异常的方式:

    @Override
    public int sub(Integer id, Double money) {
        //获取当前账户的余额
        Double balance = queryBalanceById(id);
        if(balance < money){
            //余额不足
    //            return 0;
            throw new RuntimeException("余额不足");
        }
    
        String sql = "UPDATE wallet SET balance = balance - ? WHERE id = ?";
        return jdbcTemplate.update(sql,money,id);
    }

    并将WalletServiceImpl的transfer修改为:

    @Override
    public int transfer(Integer fromId, Integer toId, Double money) {
       int sub = walletDao.sub(fromId,money);
       int add = walletDao.add(toId,money);
       return  sub * add;
    }

    此时执行测试类,便会输出:

    Exception in thread "main" java.lang.RuntimeException: 余额不足

    上述方法虽然看似减少了判断语句,但也存在其他问题,在上述的transfer中,先给张甲减钱,再给张一加钱,这样操作是不会出现问题的,但是假如我们在transfer中,先给张一账户加钱,再给张甲账户减钱,那么当张甲的账户没钱的时候,张一的账户依旧会加钱

    @Override
    public int transfer(Integer fromId, Integer toId, Double money) {
       int add = walletDao.add(toId,money); //无论张甲的账户有没有钱,张一的账户都会加钱
       int sub = walletDao.sub(fromId,money);
       return  sub * add;
    }

    所以上述两种方法都存在着各种问题,真正解决此类问题的正确方法便是事务控制。

    三.  Spring的事务控制

    通过Spring进行事物控制是非常简单的,只需要两个操作即可

    1.  配置事物管理器并开启使用注解的方式配置事务

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
    
        <context:component-scan base-package="io.zhangjia.spring"/>
    
    
        <context:property-placeholder location="jdbc.properties"/>
    
        <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
            <property name="driverClassName" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        </bean>
    
    
        <bean class="org.springframework.jdbc.core.JdbcTemplate">
            <property name="dataSource" ref="dataSource"/>
        </bean>
    
        <!--    配置事务管理器-->
        <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
        <!--    开启使用注解的方式配置事务-->
        <tx:annotation-driven/>
        <!--  注意有多个annotation-driven,我们使用的是http://www.springframework.org/schema/tx -->
    </beans>

    2.  在需要控制的地方抛出异常,这一步我们刚才已经做过,即添加 throw new RuntimeException(“余额不足”);

     @Override
        public int sub(Integer id, Double money) {
            //获取当前账户的余额
            Double balance = queryBalanceById(id);
            if(balance < money){
                //余额不足
                throw new RuntimeException("余额不足");
            }
    
            String sql = "UPDATE wallet SET balance = balance - ? WHERE id = ?";
            return jdbcTemplate.update(sql,money,id);
        }

    3.  使用Transactional注解开启事务控制

    使用Transactional注解开启事务控制的方式也非常简单,只需要在需要开启事物控制的方法上添加@Transactional注解即可

    @Transactional
    @Override
    public int transfer(Integer fromId, Integer toId, Double money) {
       int add = walletDao.add(toId,money);
       int sub = walletDao.sub(fromId,money);
       return  sub * add;
    }

    此时,无论我们在transfer中是先执行add还是先执行sub,都不会影响数据的完整性,事务控制完成。

    需要使用事务控制的场景:一个业务如果包含多个增删改的数据操作时,便需要加入事务控制。

  • 1
  • 0
  • 0
  • 1.2k
  • success。

    请登录之后再进行评论

    登录
    单栏布局 侧栏位置: