Spring 事务管理(13)

  • 2020 年 3 月 18 日
  • 筆記

事务管理用来确保数据的完整性和一致性。事务就是一系列的工作,它们被当做一个单独的工作单元,这些动作要么全部完成,要么全部不起作用。

事务的四个关键属性(ACID)

  • 原子性:事务是一个原子操作,由一系列动作组成,事务的原子性确保动作要么全部完成要么完全不起作用
  • 一致性:事务的执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统运行时发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态
  • 隔离性:一个事务的执行不能有其他事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发之执行的各个事务之间不能互相干扰。事务的隔离界别有4级。
  • 持续性:也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久的,不能回滚。接下来的其它操作或故障不应该对其执行结果有任何影响

Spring中的事务管理

Spring在不同的事务管理API之上定义了一个抽象层,Spring既支持编程式事务管理,也支持声明式的事务管理。

  • 编程式事务管理:将事务管理代码潜入到业务方法中来控制事务的提交和回滚
  • 声明式事务管理:将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理,Spring通过Spring AOP框架支持声明式事务管理

Spring的核心事务管理抽象是org.springframework.transaction.PlatformTransactionManager ,这是一个接口,封装了一组独立于技术的方法,无论使用Spring的哪种事务管理策略,事务管理器都是必须的。

事务管理器的不同实现:

  • org.springframework.jdbc.datasource.DataSourceTransactionManager : 在应用程序中只需要处理一个数据源,而且通过JDBC存取
  • org.springframework.transaction.jta.JtaTransactionManager 在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理

事务管理器最终以普通的Bean形式声明在Spring IOC容器中

事务的传播行为

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行

Spring支持的事务传播行为

传播属性

描述

REQURED

如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行

REQUIRED_NEW

当前的定义方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起

并发事务所导致的问题

并发事务(当同一个应用程序或不同应用程序中的多个事务在同一个数据集上并行执行时)可能导致的问题:

  • 脏读:一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据
  • 不可重复读:一个事务内,多次读同一个数据。在这个事务还没有结束时,另外一个事务也访问该统一数据,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样。
  • 幻读:第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行,同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。第一个事务同样的操作读取两次,得到的记录数并不相同

隔离级别

描述

READ_UNCOMMITED

允许事务读取未其他事务提交的变更,脏读,不可重复读和幻读的问题都会出现

READ_COMMITED

一个事务只能看见已经提交事务所做的改变,可以避免脏读,但不可重复读和幻读的问题仍旧会出现,这是默认的隔离级别

REPEATABLE——READ

确保事务可以多次从一个字段读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新,可以避免脏读和不可重复读,但幻读的问题仍然存在

SERIALZABLE

确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务执行插入,更新和删除操作,所有并发都可以避免,但性能十分低下

注意:事务的隔离级别受到数据库的限制,不同的数据库支持的的隔离级别不一定相同

代码

// BookShopDao.java  public interface BookShopDao {        // 根据书号获取书的单价      public  int findBookPriceByIsbn(String isbn);        // 更新书的库存,使书号对应的库存 -1      public void updateBookStock(String isbn);        // 更新用户的账户余额:使username的balacne - price      public void updateUserAccount(String username,int price);  }
// BookShopDaoImpl.java  @Repository("bookShopDao") // 可以不命名,默认使用类名首字母小写  public class BookShopDaoImpl implements BookShopDao {        @Autowired      private JdbcTemplate jdbcTemplate;        @Override      public int findBookPriceByIsbn(String isbn) {          String sql = "SELECT price FROM book WHERE id = ?";          return jdbcTemplate.queryForObject(sql,Integer.class,isbn);      }        @Override      public void updateBookStock(String isbn) {          // 检查书的库存是否足够,若不够,则抛出异常          String sql2 = "SELECT stock FROM book_stock WHERE book_id = ?";          int stock = jdbcTemplate.queryForObject(sql2,Integer.class,isbn);          if(stock == 0){              throw new BookStockException("库存不足!");          }          String sql = "UPDATE book_stock SET stock = stock - 1 WHERE book_id = ?";          jdbcTemplate.update(sql,isbn);      }        @Override      public void updateUserAccount(String username, int price) {          String sql2 = "SELECT balance FROM accout WHERE user = ?";          int balance = jdbcTemplate.queryForObject(sql2,Integer.class,username);          if(balance < price){              throw new UserAccountException("余额不足!");          }          String sql = "UPDATE accout SET balance = balance - ? WHERE user = ?";          jdbcTemplate.update(sql,price,username);      }  }
// BookShopService.java  public interface BookShopService {      public void purchase(String username,String isbn);  }
// BookShopServiceImpl.java  @Service("bookShopService")  public class BookShopServiceImpl implements BookShopService {        @Autowired      private BookShopDao bookShopDao;        // -->添加事务注解      // -->使用propagation 指定事务的传播行为,即当前的事务方法被另一个事务方法调用时      // 如何使用事务,默认取值为 REQUIRED,即使用调用方法的事务      // REQUIRED_NEW:事务自己的事务,调用的事务方法的事务被挂起      // -->使用isolation 指定事务的隔离级别,最常用的取值为READ_COMMITTED      // 默认情况下Spring的声明式事务对所有的运行时异常进行回滚,也可以通过对应的属性进行设置,通常情况下默认值即可      // -->使用timeout指定强制回滚之前事务可以占用的时间      // -->使用readOnly指定事务是否为只读。表示这个事务只读取事务但不更新数据      @Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.READ_COMMITTED,                      noRollbackFor = {UserAccountException.class},                      timeout = 3,readOnly = false)      @Override      public void purchase(String username, String isbn) {          // 1. 获取书的单价          int price = bookShopDao.findBookPriceByIsbn(isbn);            // 2. 更新书的库存          bookShopDao.updateBookStock(isbn);            // 3. 更新用户余额          bookShopDao.updateUserAccount(username,price);      }  }
// BookStockException.java  public class BookStockException extends RuntimeException {        private static final long sericlVersionUID = 1L;        public BookStockException(){          super();      }        public BookStockException(String message,Throwable cause,                                     boolean enableSuppression,boolean writeableStackTrace){          super(message,cause,enableSuppression,writeableStackTrace);      }        public BookStockException(String message,Throwable cause){          super(message,cause);      }        public BookStockException(String message){          super(message);      }        public BookStockException(Throwable cause){          super(cause);      }    }
// SpringTransactionTest.java  public class SpringTransactionTest {      private ApplicationContext ctx = null;      private BookShopDao bookShopDao = null;      private BookShopService bookShopService = null;      {          ctx = new ClassPathXmlApplicationContext("applicationContext.xml");          bookShopDao = ctx.getBean(BookShopDao.class);          bookShopService = ctx.getBean(BookShopService.class);      }        @Test      public void testBookShopDaoFindPriceByIsbn(){          System.out.println(bookShopDao.findBookPriceByIsbn("2"));      }        @Test      public void testBookShopDaoUpdateBookStock(){          bookShopDao.updateBookStock("1");      }        @Test      public void testUpdateUserAccount(){          bookShopDao.updateUserAccount("ada",1);      }        @Test      public void testBookShopService(){          bookShopService.purchase("ada","1");      }  }
// UserAccountException.java  public class UserAccountException extends RuntimeException {        private static final long serialVersionUID = 1L;        public UserAccountException(){          super();      }        public UserAccountException(String message,Throwable cause,                                boolean enableSuppression,boolean writeableStackTrace){          super(message,cause,enableSuppression,writeableStackTrace);      }        public UserAccountException(String message,Throwable cause){          super(message,cause);      }        public UserAccountException(String message){          super(message);      }        public UserAccountException(Throwable cause){          super(cause);      }  }
// 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"         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/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd          http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">        <context:component-scan base-package="com.sangyu.test12"/>      <!--导入资源文件-->      <context:property-placeholder location="classpath:db.properties"/>        <!--配置c3p0数据源-->      <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">          <property name="User" value="${jdbc.user}"/>          <property name="Password" value="${jdbc.password}"/>          <property name="DriverClass" value="${jdbc.driverClass}"/>          <property name="jdbcUrl" value="${jdbc.jdbcUrl}"/>            <property name="InitialPoolSize" value="${jdbc.initPoolSize}"/>          <property name="MaxPoolSize" value="${jdbc.maxPoolSize}"/>      </bean>        <!-- 配置 Spirng 的 JdbcTemplate -->      <bean id="jdbcTemplate"            class="org.springframework.jdbc.core.JdbcTemplate">          <property name="dataSource" ref="dataSource"></property>      </bean>        <!--配置NamedParameterJdbcTemplate,该对象可以使用具名参数,其没有无参数的构造器,所以必须为其构造器指定参数-->      <bean id="namedParameterJdbcTemplate" class="org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate">          <constructor-arg ref="dataSource"></constructor-arg>      </bean>        <!--配置bean-->      <bean class="com.sangyu.test12.BookShopDao" abstract="true"></bean>      <bean class="com.sangyu.test12.BookShopService" abstract="true"></bean>          <!-- 配置事务管理器 -->      <bean id="transactionManager"            class="org.springframework.jdbc.datasource.DataSourceTransactionManager">          <property name="dataSource" ref="dataSource"></property>      </bean>        <!-- 启用事务注解 -->      <tx:annotation-driven transaction-manager="transactionManager"/>  </beans>