Spring框架——事务处理(编程式和声明式)
一、 事务概述
●在JavaEE企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。
●事务就是一组由于逻辑上紧密关联而合并成一个整体(工作单元)的多个数据库操作,这些操作要么都执行,要么都不执行。
●事务的四个关键属性(ACID)
○原子性(atomicity):“原子”的本意是“不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。
○一致性(consistency):“一致”指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。
○隔离性(isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。
○持久性(durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。
二、 Spring事务管理
2.1 编程式事务管理
2.1.1使用原生的JDBC API进行事务管理
[1]获取数据库连接Connection对象
[2]取消事务的自动提交
[3]执行操作
[4]正常完成操作时手动提交事务
[5]执行失败时回滚事务
[6]关闭相关资源
例:
//1.获取Connection对象 Connection conn = JDBCUtils.getConnection(); try { //2.开启事务:取消自动提交 conn.setAutoCommit(false); //3.执行操作 //4.正常完成操作,手动提交事务 conn.commit(); }catch(Exception e) { //5.回滚事务 conn.rollBack(); }finally{ //6.释放资源 }
2.1.2评价
使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型的编程式事务管理。编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
2.2 声明式事务管理
大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。
事务管理代码的固定模式作为一种横切关注点,可以通过AOP方法模块化,进而借助Spring AOP框架实现声明式事务管理。
Spring在不同的事务管理API之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理API的底层实现细节,就可以使用Spring的事务管理机制。
Spring既支持编程式事务管理,也支持声明式的事务管理。
2.3 Spring提供的事务管理器
Spring从不同的事务管理API中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。
Spring的核心事务管理抽象是PlatformTransactionManager。它为事务管理封装了一组独立于技术的方法。无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
事务管理器可以以普通的bean的形式声明在Spring IOC容器中。
2.4 事务管理器的主要实现
①DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过JDBC存取。
②JtaTransactionManager:在JavaEE应用服务器上用JTA(Java Transaction API)进行事务管理
③HibernateTransactionManager:用Hibernate框架存取数据库
3 测试数据准备
3.1 需求
3.2 数据库表
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) ) ;
INSERT INTO account (`username`,`balance`) VALUES ('Tom',100000); INSERT INTO account (`username`,`balance`) VALUES ('Jerry',150000);
INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-001','book01',100); INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-002','book02',200); INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-003','book03',300); INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-004','book04',400); INSERT INTO book (`isbn`,`book_name`,`price`) VALUES ('ISBN-005','book05',500);
INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-001',1000); INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-002',2000); INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-003',3000); INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-004',4000); INSERT INTO book_stock (`isbn`,`stock`) VALUES ('ISBN-005',5000); |
4 初步实现
4.1 配置文件
<!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
<!-- 启用事务注解 --> <tx:annotation-driven transaction-manager="transactionManager"/> |
4.2 在需要进行事务控制的方法上加注解
@Transactional
5 事务的传播行为
5.1 简介
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
事务的传播行为可以由传播属性指定。Spring定义了7种类传播行为。系统默认属性为:REQUIRED。
事务传播属性可以在@Transactional注解的propagation属性中定义。
5.2 测试
5.3 说明
①REQUIRED传播行为
当bookService的purchase()方法被另一个事务方法checkout()调用时,它默认会在现有的事务内运行。这个默认的传播行为就是REQUIRED。因此在checkout()方法的开始和终止边界内只有一个事务。这个事务只在checkout()方法结束的时候被提交,结果用户一本书都买不了。
②REQUIRES_NEW传播行为
表示该方法必须启动一个新事务,并在自己的事务内运行。如果有事务在运行,就应该先挂起它。
5.4 补充
在Spring 2.x事务通知中,可以像下面这样在<tx:method>元素中设定传播事务属性。
6 事务的隔离级别
6.1 数据库事务并发问题
假设现在有两个事务:Transaction01和Transaction02并发执行。
①脏读
[1]Transaction01将某条记录的AGE值从20修改为30。
[2]Transaction02读取了Transaction01更新后的值:30。
[3]Transaction01回滚,AGE值恢复到了20。
[4]Transaction02读取到的30就是一个无效的值。
②不可重复读
[1]Transaction01读取了AGE值为20。
[2]Transaction02将AGE值修改为30。
[3]Transaction01再次读取AGE值为30,和第一次读取不一致。
③幻读
[1]Transaction01读取了STUDENT表中的一部分数据。
[2]Transaction02向STUDENT表中插入了新的行。
[3]Transaction01读取了STUDENT表时,多出了一些行。
6.2 隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
①读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
②读已提交:READ COMMITTED
要求Transaction01只能读取Transaction02已提交的修改。
③可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
④串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
⑤各个隔离级别解决并发问题的能力见下表
|
脏读 |
不可重复读 |
幻读 |
READ UNCOMMITTED |
有 |
有 |
有 |
READ COMMITTED |
无 |
有 |
有 |
REPEATABLE READ |
无 |
无 |
有 |
SERIALIZABLE |
无 |
无 |
无 |
⑥各种数据库产品对事务隔离级别的支持程度
|
Oracle |
MySQL |
READ UNCOMMITTED |
× |
√ |
READ COMMITTED |
√ |
√ |
REPEATABLE READ |
× |
√(默认) |
SERIALIZABLE |
√ |
√ |
6.3 在Spring中指定事务隔离级别
①注解
用@Transactional注解声明式地管理事务时可以在@Transactional的isolation属性中设置隔离级别
②XML
在Spring 2.x事务通知中,可以在<tx:method>元素中指定隔离级别
7 触发事务回滚的异常
7.1 默认情况
捕获到RuntimeException或Error时回滚,而捕获到编译时异常不回滚。
7.2 设置途经
①注解
@Transactional 注解
[1]rollbackFor属性:指定遇到时必须进行回滚的异常类型,可以为多个
[2]noRollbackFor属性:指定遇到时不回滚的异常类型,可以为多个
②XML
在Spring 2.x事务通知中,可以在<tx:method>元素中指定回滚规则。如果有不止一种异常则用逗号分隔。
8 事务的超时和只读属性
8.1 简介
由于事务可以在行和表上获得锁,因此长事务会占用资源,并对整体性能产生影响。
如果一个事物只读取数据但不做修改,数据库引擎可以对这个事务进行优化。
超时事务属性:事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。
只读事务属性: 表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。
8.2 设置
①注解
@Transaction注解
②XML
在Spring 2.x事务通知中,超时和只读属性可以在<tx:method>元素中进行指定
9 基于XML文档的声明式事务配置
<!-- 配置事务切面 --> <aop:config> <aop:pointcut expression="execution(* com.atguigu.tx.component.service.BookShopServiceImpl.purchase(..))" id="txPointCut"/> <!-- 将切入点表达式和事务属性配置关联到一起 --> <aop:advisor advice-ref="myTx" pointcut-ref="txPointCut"/> </aop:config>
<!-- 配置基于XML的声明式事务 --> <tx:advice id="myTx" transaction-manager="transactionManager"> <tx:attributes> <!-- 设置具体方法的事务属性 --> <tx:method name="find*" read-only="true"/> <tx:method name="get*" read-only="true"/> <tx:method name="purchase" isolation="READ_COMMITTED" No-rollback-for="java.lang.ArithmeticException,java.lang.NullPointerException" propagation="REQUIRES_NEW" read-only="false" timeout="10"/> </tx:attributes> </tx:advice> |
|
10.声明式事务例:
①导入SQL文件:declaration_transaction.sql
该文件内容如下,可直接复制到记事本,文件名后缀为.sql,通过数据库(如:Navicat Premium等)运行该sql文件就好。
/* SQLyog Ultimate v9.20 MySQL - 5.1.37-community : Database - tx ********************************************************************* */ /*!40101 SET NAMES utf8 */; /*!40101 SET SQL_MODE=''*/; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; CREATE DATABASE /*!32312 IF NOT EXISTS*/`tx` /*!40100 DEFAULT CHARACTER SET gb2312 */; USE `tx`; /*Table structure for table `account` */ DROP TABLE IF EXISTS `account`; CREATE TABLE `account` ( `username` varchar(50) NOT NULL, `balance` int(11) DEFAULT NULL, PRIMARY KEY (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=gb2312; /*Data for the table `account` */ insert into `account`(`username`,`balance`) values ('Jerry',800),('Tom',100000); /*Table structure for table `book` */ DROP TABLE IF EXISTS `book`; CREATE TABLE `book` ( `isbn` varchar(50) NOT NULL, `book_name` varchar(100) DEFAULT NULL, `price` int(11) DEFAULT NULL, PRIMARY KEY (`isbn`) ) ENGINE=InnoDB DEFAULT CHARSET=gb2312; /*Data for the table `book` */ insert into `book`(`isbn`,`book_name`,`price`) values ('ISBN-001','book01',100),('ISBN-002','book02',200),('ISBN-003','book03',300),('ISBN-004','book04',400),('ISBN-005','book05',500); /*Table structure for table `book_stock` */ DROP TABLE IF EXISTS `book_stock`; CREATE TABLE `book_stock` ( `isbn` varchar(50) NOT NULL, `stock` int(11) DEFAULT NULL, PRIMARY KEY (`isbn`) ) ENGINE=InnoDB DEFAULT CHARSET=gb2312; /*Data for the table `book_stock` */ insert into `book_stock`(`isbn`,`stock`) values ('ISBN-001',1000),('ISBN-002',2000),('ISBN-003',3000),('ISBN-004',4000),('ISBN-005',5000); /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
②创建一个动态web工程
1.加入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
commons-logging-1.1.3.jar
spring-aop-4.0.0.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar
spring-beans-4.0.0.RELEASE.jar
spring-context-4.0.0.RELEASE.jar
spring-core-4.0.0.RELEASE.jar
spring-expression-4.0.0.RELEASE.jar
spring-jdbc-4.0.0.RELEASE.jar
spring-orm-4.0.0.RELEASE.jar
spring-tx-4.0.0.RELEASE.jar
然后是mysql驱动包即C3P0的jar包:
c3p0-0.9.1.2.jar
mysql-connector-java-5.1.37-bin.jar
2.创建一份jdbc.properties文件
jdbc.user=root
jdbc.passowrd=123456
jdbc.url=jdbc:mysql://localhost:3306/tx
jdbc.driver=com.mysql.jdbc.Driver
3.在spring配置文件中配置数据源
<!-- 引入外部属性文件 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 配置数据源 --> <bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="user" value="${jdbc.user}"></property> <property name="password" value="${jdbc.passowrd}"></property> <property name="jdbcUrl" value="${jdbc.url}"></property> <property name="driverClass" value="${jdbc.driver}"></property> </bean>
4.测试数据源:
public class TestDataSource { private ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml"); @Test public void test() throws SQLException { //获取数据源类 DataSource bean = ioc.getBean(DataSource.class); //检验是否能够连接
System.out.println(bean.getConnection()); } }
5.配置jdbcTemplate:
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="comboPooledDataSource"></property> </bean>
6.创建Dao类
<!-- 在xml文件中设置扫描的包 --> <context:component-scan base-package="com.neuedu"></context:component-scan>
@Repository public class BookDao { @Autowired private JdbcTemplate jdbcTemplate; //[1]根据isbn的值查询书的价格 public Integer findPriceByIsbn(String isbn){ String sql = "SELECT price FROM book WHERE isbn = ?"; Integer price = jdbcTemplate.queryForObject(sql, Integer.class, isbn); return price; } //[2]根据isbn的值减少书的库存,假设每次都只买1本书 public void updateStockByIsbn(String isbn){ String sql = "UPDATE book_stock SET stock=stock-1 WHERE isbn=? "; jdbcTemplate.update(sql, isbn); } //[3]根据用户名减少用户账户中的余额,减少的额度就是书的价格 public void updateBalance(String username,int price){ String sql = "UPDATE account SET balance=balance-? WHERE username=? "; jdbcTemplate.update(sql,price, username); } public void updatePriceByIsbn(String isbn,int price){ String sql="UPDATE book SET price=? WHERE isbn=? "; jdbcTemplate.update(sql, price,isbn); } public int findPriceByIsbn(String isbn){ String sql = "SELECT price FROM book WHERE isbn = ?"; Integer price = jdbcTemplate.queryForObject(sql, Integer.class, isbn); return price; } }
测试上面的该方法:
public class TestDataSource { private ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml"); private BookDao bean = ioc.getBean(BookDao.class); @Test public void test01() throws SQLException { int findPriceByIsbn = bean.findPriceByIsbn("ISBN-005"); System.out.println(findPriceByIsbn); } }
然后在dao类中继续编写如下方法:
//[2]根据isbn的值减少书的库存,假设每次都只买1本书 public void updateStockByIsbn(String isbn){ String sql = "UPDATE book_stock SET stock = stock -1 WHERE isbn = ?"; jdbcTemplate.update(sql, isbn); }
继续测试:
public class TestDataSource { private ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml"); private BookDao bean = ioc.getBean(BookDao.class); @Test public void test02() throws SQLException { bean.updateStockByIsbn("ISBN-004"); } }
继续编写Dao类中的方法:
//[3]根据用户名减少用户账户中的余额,减少的额度就是书的价格 public void updateBalance(String userName,int price){ String sql = "UPDATE account SET balance = balance - ? WHERE username = ?"; jdbcTemplate.update(sql, price,userName); }
继续测试:
@Test public void test03() throws SQLException { bean.updateBalance("Tom", 1000); }
7.创建Service层:
@Service public class BookService { @Autowired private BookDao bookDao; public void doCash(String isbn,String username){ int price = bookDao.findPriceByIsbn(isbn); bookDao.updateStockByIsbn(isbn); bookDao.updateBalance(username, price); } }
8.注意哦:上面doCash方法中调用的三个方法应该在同一个事务中,要么同时成功,要么同时失败!
先统一设置一下:
UPDATE account SET balance = 10000; UPDATE book_stock SET stock = 1000;
然后先正常测试一下service中的doCash方法:
public class TestDataSource { private ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml"); private BookDao bean = ioc.getBean(BookDao.class); private BookService bookService = ioc.getBean(BookService.class); @Test public void test04() throws SQLException { bookService.doCash("ISBN-001","Tom"); } }
由于此时该方法没在事务中,如果doCash方法调用dao层的方法的时候,在中间位置出现了错误,此时就会造成一部分数据改掉了,而
另一部分数据没有改掉,这就麻烦了,所以此时应该加入事务机制!
9.如果想开启事务,就需要在spring的配置文件中配置事务管理器
<!-- 配置事务管理器,并为事务管理器配置数据源!--> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="comboPooledDataSource"></property> </bean> <!-- 开启基于注解的声明式事务功能,需要设置transaction-manager属性--> <!-- 如果 事务管理器的id正好是transaction-manager的默认值transactionManager,则可以省略--> <tx:annotation-driven transaction-manager="dataSourceTransactionManager"/>
然后在service层的doCash方法上加上:@Transactional注解这样就开启了事务!需要注意的是事务一般是加在service层的!
③数据库操作
[1]根据isbn的值查询书的价格
[2]根据isbn的值减少书的库存,假设每次都只买1本书
[3]根据用户名减少用户账户中的余额,减少的额度就是书的价格