Spring事务管理(声明式事务)
一、事务概念
-
事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。
-
事务的四个关键属性(ACID)
- 原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
- 一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
- 隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
- 持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。
二、环境准备
数据库表:
CREATE TABLE book (
isbn VARCHAR (50) PRIMARY KEY,
book_name VARCHAR (100),
price INT
) ;
CREATE TABLE book_stock (
isbn VARCHAR (50) PRIMARY KEY,
stock INT,
CHECK (stock > 0)
) ;
CREATE TABLE account (
username VARCHAR (50) PRIMARY KEY,
balance INT,
CHECK (balance > 0)
) ;
事务问题分析:
对于用户买书的结账操作,需要扣除account表中用户的余额,还需要减去book_stock表中图书的库存。所以该操作必须用到事务
BookDao:
@Repository
public class BookDao {
@Autowired
JdbcTemplate jdbcTemplate;
/**
* 减余额
* @param userName
* @param price
*/
public void updateBalance(String userName,int price){
String sql = "update account set balance=balance-? where username = ?";
jdbcTemplate.update(sql,price,userName);
}
/**
* 按照图书的ISBN获取某本图书的价格
* @param isbn
* @return
*/
public int getPrice(String isbn){
String sql = "select price from book where isbn=?";
return jdbcTemplate.queryForObject(sql, Integer.class,isbn);
}
/**
* 减库存,减去某本书的库存:为了简单起见每次减一
* @param isbn
* @param price
*/
public void updateStock(String isbn){
String sql = "update# book_stock set stock=stock-1 where isbn=?";
jdbcTemplate.update(sql,isbn);
}
}
BookService:
@Service
public class BookService {
@Autowired
BookDao bookDao;
/**
* 结账,传入用户买了哪本书
*/
public void checkout(String username,String isbn){
//1、查询该书的价格
int price = bookDao.getPrice(isbn);
//2、从余额中扣除
bookDao.updateBalance(username, price);
//3、扣除书的数量
bookDao.updateStock(isbn);
}
}
Spring配置文件:
<!-- 加载配置文件 -->
<context:property-placeholder location="classpath:dbconfig.properties"/>
<!-- 配置连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
</bean>
<!-- 配置JdbcTemplate对象,注入DataSource -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg ref="dataSource"></constructor-arg>
</bean>
三、Spring事务管理(声明式事务管理)
3.1 Spring进行事务管理的常用API
- Spring 支持编程式事务和声明式事务管理两种方式,这里学习声明式事务管理。
- Spring 声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
1、Spring 事务管理主要有3个接口:
TransactionDefinition
接口:事务定义信息(隔离,传播,超时,只读)PlatformTransactionManager
接口:事务管理器(用来管理事务,包含事务的提交,回滚)Transaction Status
接口:封装了一些控制事务查询和执行的方法。
2、使用Spring管理事务时,首先需要告诉Spring使用哪一个事务管理器
Spring进行事务操作时候,主要使用PlatformTransactionManager接口,它表示事务管理器,即真正管理事务的对象。
Spring并不直接管理事务,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,也就是将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。
Spring针对不同的持久化框架,提供了不同PlatformTransactionManager接口的实现类:
- org.springframework.jdbc.datasource.DataSourceTransactionManager :使用 Spring JDBC或iBatis 进行持久化数据时使用
- org.springframework.orm.hibernate3.HibernateTransactionManager :使用 Hibernate版本进行持久化数据时使用
3、Spring 事务回滚规则
指示Spring 事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。
默认配置下,spring只有在抛出的异常为运行时异常时才回滚该事务,也就是抛出的异常为RuntimeException的子类(Errors也会导致事务回滚),而抛出checked异常则不会导致事务回滚。可以明确的配置在抛出那些异常时回滚事务,包括checked异常。也可以明确定义那些异常抛出时不回滚事务。还可以编程性的通过setRollbackOnly()方法来指示一个事务必须回滚,在调用完setRollbackOnly()后你所能执行的唯一操作就是回滚。
3.2 声明式事务初步实现(注解方式)(重点)
1.在spring配置文件配置事务管理器(切面)(需要导入面向切面编程的包)
<!-- 1、配置事务管理器(切面)让其进行事务控制:一定导入面向切面编程的包
spring-aspects-4.0.0.RELEASE.jar
com.springsource.net.sf.cglib-2.2.0.jar
com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
2.在spring配置文件开启事务注解
<!-- 2、开启基于注解的事务控制模式:依赖tx名称空间 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
3.给类或事务方法加注解 @Transactional
1.@Transactional,这个注解添加到类上面,也可以添加方法上面
2.如果把这个注解添加类上面,这个类里面所有的方法都添加事务
3.如果把这个注解添加方法上面,则是为这个方法添加事务
@Service
public class BookService {
@Autowired
BookDao bookDao;
/**
* 结账,传入用户买了哪本书
* 将checkout声明为事务方法
*/
@Transactional
public void checkout(String username,String isbn){
//1、查询该书的价格
int price = bookDao.getPrice(isbn);
//2、从余额中扣除
bookDao.updateBalance(username, price);
System.out.println(1/0);//再事务中加入异常信息
//3、扣除书的数量
bookDao.updateStock(isbn);
}
}
测试:
@Test
public void test01(){
ApplicationContext ioc = new ClassPathXmlApplicationContext("ApplicationContext.xml");
BookService bean = ioc.getBean(BookService.class);
bean.checkout("Tom", "ISBN-005");
System.out.println("结账完成");
//有事务的业务逻辑,容器中保存的是这个业务逻辑的代理对象
System.out.println(bookService.getClass());//class com.zb.service.BookService$$EnhancerByCGLIB$$f8c685bb
}
结果:
出现异常,但是数据库值没有改变,成功!
四、事务的超时和只读属性
- 由于事务可以在行和表上获得锁,因此长事务会占用资源,并对整体性能产生影响。
如果一个事物只读取数据但不做修改,数据库引擎可以对这个事务进行优化。 - 超时事务属性:事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。
- 只读事务属性: 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。
设置
-
@Transaction注解
设置超时事务属性:
@Transactional(timeout=3)//事务超出指定执行时长后自动终止并回滚 public void checkout(String username,String isbn){ //1、查询该书的价格 int price = bookDao.getPrice(isbn); try { Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } //2、从余额中扣除 bookDao.updateBalance(username, price); //3、扣除书的数量 bookDao.updateStock(isbn); }
设置只读事务属性:
//readOnly-boolean:设置事务为只读事务:。 @Transactional(readOnly=true) public void checkout(String username,String isbn){ //如果设置了readOnly=true,那么方法中只能进行查询操作,而不能进行增删改操作 //readOnly=true:加快查询速度,进行事务优化;不用管事务那一堆操作了。 }
-
XML
在Spring 2.x事务通知中,超时和只读属性可以在< tx:method >元素中进行指定
五、触发事务回滚的异常(重点)
-
默认情况
事务的回滚,默认发生运行时异常都回滚,发生编译时异常不会回滚
-
设置途经
-
@Transactional 注解
rollbackFor属性:哪些异常事务可以不回滚(可以让原来默认回滚的异常给他不回滚)
noRollbackFor属性:原本不回滚(原本编译时异常是不回滚的)的异常指定让其回滚//设置发生ArithmeticException和NullPointerException异常不回滚 //@Transactional(noRollbackForClassName={"java.lang.ArithmeticException"})//写全类名也可以 @Transactional(noRollbackFor={ ArithmeticException.class,NullPointerException.class},readOnly=false) public void checkout(String username,String isbn) throws Exception{ //1、查询该书的价格 int price = bookDao.getPrice(isbn); //2、从余额中扣除 bookDao.updateBalance(username, price); //new FileInputStream("D://a.txt"); int a = 1/0;//制造一个算数运行异常 //3、扣除书的数量 bookDao.updateStock(isbn); }
//设置发生FileNotFoundException异常时回滚 @Transactional(rollbackFor={ FileNotFoundException.class}) public void checkout(String username,String isbn) throws Exception{ //1、查询该书的价格 int price = bookDao.getPrice(isbn); //2、从余额中扣除 bookDao.updateBalance(username, price); new FileInputStream("D://a.txt");//制造一个FileNotFoundException异常 //3、扣除书的数量 bookDao.updateStock(isbn); }
-
XML
在Spring 2.x事务通知中,可以在<tx:method>元素中指定回滚规则。如果有不止一种异常则用逗号分隔。
-
六、Spring中事务的隔离级别
6.1 数据库事务并发问题
读取的三种问题:
同一个应用程序中的多个事务或不同应用程序中的多个事务在同一个数据集上并发执行时, 可能会出现许多意外的问题,这些问题可分为如下三种类型:
- 脏读:对于两个事务T1,T2。T1读取了已经被T2更新但还没有被提交的字段之后,若T2回滚,T1读取的内容就是临时且无效的。
- 不可重复读:对于两个事务T1,T2。T1读取了一个字段,然后T2更新了该字段之后,T1再次读取同一个字段,值就不同了。
- 幻读:对于两个事务T1,T2。T1从一个表中读取了一个字段,然后T2在该表中插入了一些新的行之后,如果T1再次读取同一个表,就会多出几行。
总结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
6.2 隔离级别
解决办法:
- 数据库事务的隔离性:数据库系统必须具有隔离并发运行各个事务的能力,使他们不会相互影响,避免各种并发问题。
- 一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性弱。
- 数据库提供的4种事务隔离级别:
6.3 在Spring中指定事务隔离级别(重点)
-
用 @Transactional 注解声明式地管理事务时可以在 @Transactional 的 isolation属性中设置隔离级别
@Transactional(isolation=Isolation.READ_UNCOMMITTED) public int getPrice(String isbn){ return bookDao.getPrice(isbn); }
-
XML
在 Spring 2.x 事务通知中,可以在<tx:method>元素中指定隔离级别
七、事务的传播行为(重点)
-
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
-
事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为。
REQUIRED:将之前事务用的connection传递给这个方法使用;
REQUIRES_NEW:这个方法直接使用新的connection; -
REQUIRED传播行为: 如果有事务在运行,当前的方法就在这个事务内运行,否则就开启一个新的事务,并在自己的事务内运行。
大事务类MulService :
public class MulService { @Autowired BookService bookService; @Transactional public void mulTx(){ //传播行为来设置这个事务方法是不是和之前的大事务共享一个事务(使用同一条连接) //REQUIRED,表示和大事务公用一个事务 bookService.checkout("Tom", "ISBN-002"); //REQUIRED,表示和大事务公用一个事务 bookService.updatePrice("ISBN-002", 666); System.out.println(1/0);//出现异常一起回滚,数据库没有改变 } }
小事务类BookService :
@Service public class BookService { @Autowired BookDao bookDao; @Transactional(propagation=Propagation.REQUIRED) public void checkout(String username,String isbn){ //1、查询该书的价格 int price = bookDao.getPrice(isbn); //2、从余额中扣除 bookDao.updateBalance(username, price); //3、扣除书的数量 bookDao.updateStock(isbn); } @Transactional(propagation=Propagation.REQUIRED) public void updatePrice(String isbn,int price){ bookDao.updatePrice(isbn, price); } }
-
REQUIRES_NEW传播行为: 表示该方法必须启动一个新事务,并在自己的事务内运行。如果有事务在运行,就应该先挂起它
大事务类MulService :
@Service public class MulService { @Autowired BookService bookService; @Transactional public void mulTx(){ //传播行为来设置这个事务方法是不是和之前的大事务共享一个事务(使用同一条连接) //REQUIRED,表示和大事务公用一个事务 bookService.checkout("Tom", "ISBN-002");//购买图书操作 //REQUIRES_NEW,表示新开启一个事务 bookService.updatePrice("ISBN-002", 666);//修改价格操作 System.out.println(1/0);//出现异常,购买图书操作回滚,修改价格操作不受影响 } }
小事务类BookService :
@Service public class BookService { @Autowired BookDao bookDao; @Transactional(propagation=Propagation.REQUIRED) public void checkout(String username,String isbn){ //1、查询该书的价格 int price = bookDao.getPrice(isbn); //2、从余额中扣除 bookDao.updateBalance(username, price); //3、扣除书的数量 bookDao.updateStock(isbn); } @Transactional(propagation=Propagation.REQUIRES_NEW) public void updatePrice(String isbn,int price){ bookDao.updatePrice(isbn, price); } }
注意:如果是REQUIRED,事务的属性都是继承于大事务的。
本类事务方法之间的调用就只是一个事务 -
补充:
在Spring 2.x事务通知中,可以像下面这样在< tx:method >元素中设定传播事务属性。
八、XML配置事务(重点)
xml配置所用到的标签:
- 事务配置
<tx:advice>
通知标签(增强)- 属性id:自定义唯一表示
- transaction-manager属性:事务管理类,配置事务管理类的id属性值
- 事务属性配置
<tx:attributes>
子标签
<tx:method>
事务方法标签- 属性name:方法名
- 属性read-only:是否只读事务,查询都是只读,其他是非只读
- 属性propagation:事务的传播行为,默认配置REQUIRED或者SUPPORTS
- 属性isolation:事务隔离级别,默认配置DEFAULT
- 属性timeout:事务超时时间,配置-1
- 属性no-rollback-for:遇到什么异常不回滚,配置异常类名,多个类逗号分开
- 属性rollback-for:遇到什么异常回滚
- 以上回滚属性不配置,遇到异常就回滚
- aop切面配置
<aop:config>
标签
<aop:advisor>
子标签- 属性advice-ref:引用通知,配置tx:advice标签的属性值
- 属性pointcut:切点配置
<?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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">
<!-- 自动扫描 -->
<context:component-scan base-package="com.zb"></context:component-scan>
<!-- 引入外部文件 -->
<context:property-placeholder location="classpath:dbconfig.properties"/>
<!-- 配置连接池 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${jdbc.user}"></property>
<property name="password" value="${jdbc.password}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
</bean>
<!-- 配置JdbcTemplate对象 -->
<bean class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- ================配置声明式事务==================== -->
<!-- 1.配置事务的管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 指定要对哪个数据库进行事务操作 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 2.配置事务的增强,指定对哪个事务管理器进行增强
transaction-manager="transactionManager":指定是配置哪个事务管理器
-->
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<!-- 事务属性 -->
<tx:attributes>
<!-- 指定哪些方法是事务方法,切入点表达式只是说,事务管理器要切入这些方法,哪些方法加事务使用tx:method指定 -->
<!-- 设置具体的事务方法和属性 -->
<tx:method name="*"/><!-- 给事务管理器切入的所有方法都加上指定为事务方法 -->
<tx:method name="checkout" propagation="REQUIRED" timeout="-1" />
<tx:method name="get*" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 3.配置事务切入点和切面 -->
<aop:config>
<!-- 切入点 -->
<aop:pointcut expression="execution(* com.zb.service.*.*(..))" id="txPoint"></aop:pointcut>
<!-- 切面,即表示把哪个增强用在哪个切入点上,事务建议,事务增强 advice-ref:指向事务管理器的配置 -->
<aop:advisor advice-ref="myAdvice" pointcut-ref="txPoint"></aop:advisor>
</aop:config>
</beans>