• 中文
    • 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,都不会影响数据的完整性,事务控制完成。

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

    • 0
    • 0
    • 0
    • 940
    • 请登录之后再进行评论

      登录

      赞助本站

      • 支付宝
      • 微信
      • QQ

      感谢一直支持本站的所有人!

      单栏布局 侧栏位置: