一.环境搭建
- 导入SQL文件创建一个数据库,数据库中有三张表,分别是客户表(客户名,余额),书籍表(编号,书籍名称,单价)和书籍库存表(编号,库存余额)。导入jar包
- 编写配置文件,在配置文件中配置JDBCTemplate。
<?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 http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <!-- 包扫描 --> <context:component-scan base-package="com.test"></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="jdbcUrl" value="${jdbc.jdbcUrl}"></property> <property name="driverClass" value="${jdbc.driverClass}"></property> </bean> <!-- 配置JdbcTemplate --> <bean class="org.springframework.jdbc.core.JdbcTemplate" > <constructor-arg name="dataSource" ref="dataSource"></constructor-arg> </bean> </beans>
- 编写类和方法模拟结账操作
package com.test.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BookDao {
@Autowired
JdbcTemplate jdbcTemplate;
/*
* 减余额
* 减去某个用户的余额
*/
public void updateBalance(String userName,int price){
String sql="UPDATE account SET balance=balance-? WHERE username=?";
jdbcTemplate.update(sql, price,userName);
}
/*
* 按照某本书的IDBN获取某本图书的价格
*/
public int getPrice(String isbn){
String sql="SELECT price FROM book WHERE isbn=?";
return jdbcTemplate.queryForObject(sql,Integer.class,isbn);
}
/*
* 减库存,减去某本书的库存,为了简单期间每次减一
*/
public void updateStock(String isbn){
String sql="UPDATE book_stock SET stock=stock-1 WHERE isbn=?";
jdbcTemplate.update(sql, isbn);
}
}
package com.test.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.test.dao.BookDao;
@Service
public class BookService {
@Autowired
BookDao bookDao;
/*
* 结账:传入哪个用户买了哪本书
*/
public void checkOut(String userName,String isbn){
//减库存
bookDao.updateStock(isbn);
//获取价格
int price=bookDao.getPrice(isbn);
//减余额
bookDao.updateBalance(userName, price);
}
}
二.概述
2.1事务
事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。事务有四个关键属性:
- 原子性,事务的原子性表现为一个事务中涉及的多个操作在逻辑上缺一不可,事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
- 一致性,“一致”指的是数据的一致,具体的是指:所有的数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及多少个操作,都必须保证事务执行之前的数据时正确的,事务执行之后的数据仍然是正确的。如果一个事务在执行的过程中,如果某一个或某几个操作失败了,则必须将其他的操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
- 隔离型,在应用程序实际运行过程中,事务往往是并发执行的,所有很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发过程中不会相互干扰。
- 持久性,持久性原则要求事务执行完成之后,对数据的修改永久的保存下来,不会因为各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写到持久化存储器中。
2.2编程式事务
使用原生的JDBC API进行事务管理,步骤如下:
1)获取数据库连接Connection对象
2)取消事务的自动提交
3)执行操作
4)正常完成操作时手动提交事务
5)执行失败时回滚事务
6)关闭相关资源
使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型的编程式事务管理。编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用相同模式的代码进行事务管理。显然会造成较大程度的代码冗余。
2.3声明式事务
以前通过复杂的编程来编写事务,替换为只需要告诉Spring哪个方法是事务方法即可,Spring自动进行事务控制。
大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
事务管理代码的固定模式作为一种横切关注点,可以通过AOP方式模块化,进而借助Spring AOP框架实现声明式事务管理。
Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。
Spring即支持编程式事务管理,也支持声明式的事务管理。
三.事务管理接口和事务管理实验
3.1 Spring事务管理接口
- PlatformTransactionManager:事务管理器
- TransactionDefinition:事务定义信息(事务隔离级别,传播行为,超时,只读,回滚规则)
- TransactionStatus:事务运行状态
3.1.1 Spring提供的事务管理器:PlatformTransactionManager
Spring从不同的事务管理API抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。
Spring的核心事务管理抽象是PlatformTransactionManager。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
事务管理器可以以普通的bean的形式声明在Spring IOC容器中。
事务管理器的主要实现:
- DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过JDBC存取。
- JtaTransactionManager:在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理。
- HibernateTransactionManager:用Hibernate框架存取数据库。
3.1.2 TransactionDefinition
事务管理器接口PlatformTransactionManager通过getTransaction(TransactionDefinition definition)方法得到一个事务,这个方法里面的参数就是TransactionDefinition类,这个类就定义了一些基本事务属性。
那什么是事务属性呢?事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上,事务属性包含了五个方面。
TransactionDefinition接口中定义了5个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等的常量,后面会提到这些常量。
TransactionDefinition接口中的方法如下:
public interface TransactionDefinition {
// 返回事务的传播行为
int getPropagationBehavior();
// 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
int getIsolationLevel();
// 返回事务必须在多少秒内完成
//返回事务的名字
String getName();
int getTimeout();
// 返回是否优化为只读事务。
boolean isReadOnly();
}
3.2 事务实验:快速的为某个方法添加事务:
基础配置:
<!-- 包扫描 -->
<context:component-scan base-package="com.test"></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="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
</bean>
<!-- 配置JdbcTemplate操作数据库 -->
<bean class="org.springframework.jdbc.core.JdbcTemplate" >
<property name="dataSource" value="#{dataSource}"></property>
</bean>
1.配置事务管理器(切面)让其进行事务控制 ,一定导入有关切面的包
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 控制数据源 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
2.开启基于注解的事务控制模式:依赖tx名称空间
<tx:annotation-driven transaction-manager="transactionManager"/>
3.给事务方法加注解@Transactional
@Transactional
public void checkOut(String userName,String isbn){
...
}
注意:
IOC容器中保存的是业务逻辑组件(事务)的代理对象。
四.事务细节(基于注解)
4.1 事务的超时和只读属性
由于事务可以在行和表上获得锁,因此上事务会占用资源,并对整体性能产生影响。如果一个事务只读取数据但不做修改,数据库引擎可以对这个事务进行优化。
- 超时事务属性timeout:就是指一个事务所允许的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在TransactionDefinition中以int的值来表示超时时间,其单位是秒。这样可以防止长期运行的事务占用资源。
- 只读事务属性readOnly:对事务性资源进行只读操作或者读写操作。所谓事务性资源就是指那些被事务管理的资源,比如数据源,JMS资源以及自定义的事务性资源。如果确定只对事务性资源进行只读操作,那么我们就可以将事务标志位只读的,这样可以帮助数据库引擎优化事务。在TransactionDefinition以boolean类型来表示事务是否只读。
/*
* readOnly-boolean:设置事务为只读事务,默认值为false
* 可以进行事务优化
* readOnly=true:加快查询速度,不用管事务哪一堆操作
*
*
* timeout-int(秒为单位):超时,事务超出指定时长后自动终止并回滚
*
*
*/
@Transactional(timeout=3,readOnly=true)
public void checkOut(String userName,String isbn){
...
}
4.2 触发事务回滚的异常(定义事务回滚规则)
这些规则定义了哪些异常会导致事务回滚而哪些不会。首先要知道异常分为两类:编程时异常和运行时异常。捕获到RuntimeException或Error时(运行时)回滚,而捕获到编译时异常不回滚。
- rollbackFor属性:指定遇到时必须进行回滚的异常类型,可以为多个。
- norollbackFor属性:指定遇到时不回滚的异常类型,可以为多个。
/*
* noRollbackFor-Class[]:哪些异常事务可以不回滚
* noRollbackForClassName-String[](String全类名):
*
*
* rollbackFor-Class[]:哪些异常事务需要回滚
* rollbackForClassName-String[](String全类名):
*
* 异常分类:
* 运行时异常(非检查异常):可以不用处理,默认都回滚
* 编译时异常(检查异常),要么try-catch,要么在方法上声明throws,默认都不回滚
* 事务的回滚,默认发生运行时异常都回滚,发生编译异常不会回滚
* noRollbackFor:哪些异常可以不回滚(可以让原来回滚的异常不回滚)
* rollbackFor:原本不回滚的异常回滚
*/
@Transactional(rollbackFor={FileNotFoundException.class},noRollbackFor={ArithmeticException.class})
public void checkOut(String userName,String isbn) throws FileNotFoundException{
//减库存
bookDao.updateStock(isbn);
//获取价格
int price=bookDao.getPrice(isbn);
//减余额
bookDao.updateBalance(userName, price);
//数学异常,属于运行时异常,回滚
int i=100/0;
//编译式异常:不回滚
new FileInputStream("D://hhhh.aa");
}
4.3 事务的隔离级别
4.3.1 数据库事务并发问题
假如现在有两个事务:Transaction01和Transaction02并发执行。
(1)脏读:当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库,这是另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据时还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
①Transaction01将某条记录的AGE值从20修改为30。
②Transaction02读取了Transaction01更新后的值:30。
③Transaction01回滚,AGE值恢复到了20。
④Transaction02读取到的30就是一个无效的值。
(2)丢失修改:指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据,那么这样第一个事务内修改的修改结果就被丢失,因此称为丢失修改。
(3)不可重复读:指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
①Transaction01读取了AGE值为20。
②Transaction02将AGE值修改为30。
③Transaction01再次读取AGE值为30,和第一次读取不一致。
(4)幻读:幻读与不可重读类似。它发生在一个事务Transaction01读取了几行数据,接着另一个并发事务Transaction02插入了一些数据时。在随后的查询中,第一个事务Transaction01就会发现多了一些原本不存在的记录,就好像发生了幻觉,所以称为幻读。
①Transaction01读取了STUDENT表中的一部分数据。
②Transaction02向STUDENT表中插入了新的行。
③Transaction01读取了STUDENT表时,多出了一些行。
4.3.2 隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务的隔离程度称为隔离级别。
TransactionDefinition接口中定义了五个表示隔离级别的常量:
- TransactionDefinition.ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql 默认采用的 REPEATABLE_READ(可重复读)隔离级别 Oracle 默认采用的 READ_COMMITTED(读已提交)隔离级别。
- TransactionDefinition.ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读,幻读或不可重复读。
- TransactionDefinition.ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍然有可能发生。
- TransactionDefinition.ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果可能是一致的,除非是数据被自身事务自己修改,可以阻止脏读和不可重复读,但幻读仍然有可能。
-
TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
用@Transactional注解声明式地管理事务时可以在@Transactional的isolation属性中设置隔离级别。
@Transactional(readOnly=true,isolation=Isolation.READ_COMMITTED)
public int getPrice(String isbn)
{
System.out.println(bookDao.getPrice(isbn));
return bookDao.getPrice(isbn);
}
4.4 事务的传播行为
4.4.1 传播行为的常量
当前事务方法被另一个事务方法调用时,必须制定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。在TransactionDefinition定义中包括如下几个表示传播行为的常量:
支持当前事务的情况:
- TransactionDefinition.Propagation.REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- TransactionDefinition.Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- TransactionDefinition.Propagation.MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
不支持当前事务的情况:
- TransactionDefinition.Propagation.REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- TransactionDefinition.Propagation.NEVER:以非事务方式运行,如果存在当前事务,则抛出异常。
其他情况:
- TransactionDefinition.Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,则该取值等价于TransactionDefinition.Propagation.REQUIRED
这里需要指出的是,前面的六种事务传播行为是Spring从EJB中引入的,它们共享相同的概念。而Propagation.NESTED是Spring持有的,以Propagation.NESTED启动的事务内嵌于外部事务中(如果存在外部事务的话),此时,内嵌事务并不是一个独立的事务,它依赖于外部事务的存在,只有用过外部的事务提交,才能引起内部事务的提交,嵌套的子事务不能单独提交。另外,外部事务的回滚也会导致嵌套子事务的回滚。
4.4.2传播行为的实验
BookService中有两个事务:
package com.test.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.test.dao.BookDao;
@Service
public class BookService {
@Autowired
BookDao bookDao;
/*
* Propagation.REQUIRED:将之前的事务用的connection传递给这个方法使用
* Propagation.REQUIRES_NEW:这个方法使用新的connection
*/
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void checkOut(String userName,String isbn) {
//减库存
bookDao.updateStock(isbn);
//获取价格
int price=bookDao.getPrice(isbn);
//减余额
bookDao.updateBalance(userName, price);
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void updatePrice(String isbn,int price){
bookDao.updatePrice(isbn, price);
}
}
编写一个MulService类:
package com.test.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MulService {
@Autowired
private BookService bookService;
@Transactional
public void mulTx(){
//是否回滚都是可以设置的
//传播行为来设置这个事务方法是不是和之前的大事务共享一个事务(使用同一条连接)
//Propagation.REQUIRED 事务的属性都是继承于大事务
//Propagation.REQUIRES_NEW
bookService.checkOut("Tom", "ISBN-001");
//Propagation.REQUIRED
bookService.updatePrice("ISBN-002", 998);
}
}
这两个属性用图示来解释:
4.4.3 本类方法之间的调用就是一个事务
在BookService中有这这样一个方法;
@Transactional
public void mulTx()
{
checkOut("Tom", "ISBN-001");
updatePrice("ISBN-001", 998);
int i=10/0;
}
测试:
package com.test.test;
import java.io.FileNotFoundException;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.test.service.BookService;
import com.test.service.MulService;
public class TxTest {
ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml");
@Test
public void test() throws FileNotFoundException {
BookService bookService=ioc.getBean(BookService.class);
bookService.mulTx();
//如果MulService-mulTx()--调用BookService两个方法
//BookService--mulTX()--直接调用两个方法
/*
* MulServiceProxy.mulTx(){
* bookServiceProxy.checkout();
* bookServiceProxy.updatePrice();
* }
*
* 本类方法的嵌套调用就是一个事务
* BookServiceProxy.mulTx(){
* checkout();
* updatePrice();
* //相当于把这两个方法中的执行语句复制过来
* // bookDao.updateStock(isbn);
* // int price=bookDao.getPrice(isbn);
* // bookDao.updateBalance(userName, price);
* // bookDao.updatePrice(isbn, price);
* }
*/
}
}
五.事务控制实验(基于xml)
首先去掉BookService上的注解,留下BookDao的自动注入注解,然后配置文件:
<?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.test"></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="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
<property name="driverClass" value="${jdbc.driverClass}"></property>
</bean>
<!-- 配置JdbcTemplate操作数据库 -->
<bean class="org.springframework.jdbc.core.JdbcTemplate" >
<property name="dataSource" value="#{dataSource}"></property>
</bean>
<!-- 基于xml配置的事务 ,依赖tx名称空间和AOP名称空间-->
<!-- 事务控制 -->
<!-- 1.配置事务管理器(切面)让其进行事务控制 ,一定导入有关切面的包-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 控制数据源 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 2.配置出事务方法-->
<!-- 3.告诉Spring哪些是事务方法 (事务按照切入点表达式切入事务方法)
-->
<bean class="com.test.service.BookService" id="bookService"> </bean>
<aop:config>
<aop:pointcut expression="execution(* com.text.ser*.*.*(..))" id="txPoint"></aop:pointcut>
<!-- 事务建议,事务增强 advice-ref:指向事务管理器的配置-->
<aop:advisor pointcut-ref="txPoint" advice-ref="myAdivce"></aop:advisor>
</aop:config>
<!-- 配置事务管理器:
transaction-manager:指定是配置哪个事务管理器 -->
<tx:advice id="myAdivce" transaction-manager="transactionManager">
<!-- 事务属性 -->
<tx:attributes>
<!-- 指明哪些方法是事务方法:切入点只是说,事务管理器要切入这些方法,哪些方法要加事务使用tx:method说明 -->
<tx:method name="*"/>
<tx:method name="checkout" propagation="REQUIRED" />
<tx:method name="get*" read-only="true"/>
</tx:attributes>
</tx:advice>
</beans>