1、Spring事务控制概述
Spring支持编程式事务管理和声明式事务管理两种数据库事务管理方式。
- 编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager,通过硬编码的方式来管理数据库的事物。对于编程式事务管理,Spring推荐使用TransactionTemplate。
- 声明式事务管理是建立在AOP(面向切面编程)之上的。其本质是对将要执行的SQL与语句进行拦截,然后在目标SQL开始之前创建或者加入一个事务,在执行完目标SQL之后根据执行情况提交或者回滚事务。声明式事务管理有两种常用的使用方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于
@Transactional
注解。显然基于注解的方式更简单易用。
显然声明式事务管理要优于编程式事务管理,这也正是Spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。
说了这么多,不如来点实际的,下面我们就来看看Spring推荐的声明式事物如何在我们的项目中使用。我将以注解和XML两种配置方式来一一为大家介绍。
2、搭建实验环境
首先新建一个Maven项目,导入需要的依赖:
<dependency>
<!--单元测试-->
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--数据库驱动的依赖包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!--Spring核心-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--Spring IOC容器-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring Bean工厂-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<!--SpEL(Spring表达式)-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring面向切面编程-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<!--提供对AspectJ的支持,以便可以方便的将面向切面的功能集成进IDE中-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!--aspectj的runtime包(必须)-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.1</version>
</dependency>
<!--aspectjweaver是aspectj的织入包(必须)-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.1</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.1</version>
</dependency>
<!--Spring jdbc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring 事物-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring 映射-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
创建数据库和表:新建tx数据库,并在tx数据库中新建三张表account、book、book_stock。具体的数据库脚本如下:
/*!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 */;
导入后应该是这样的三张表:

3、基于Annotation的声明式事务管理配置和使用
(1)首先配置数据源,这里使用的阿里的Druid数据连接池
<!--引入外部文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--配置数据连接池-->
<bean id="DruidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${jdbc.initialSize}"/>
<property name="minIdle" value="${jdbc.minIdle}"/>
<property name="maxActive" value="${jdbc.maxActive}"/>
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${jdbc.maxWait}"/>
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" />
<property name="validationQuery" value="${jdbc.validationQuery}" />
<property name="testWhileIdle" value="${jdbc.testWhileIdle}" />
<property name="testOnBorrow" value="${jdbc.testOnBorrow}" />
<property name="testOnReturn" value="${jdbc.testOnReturn}" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false。-->
<property name="poolPreparedStatements" value="${jdbc.poolPreparedStatements}" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="${jdbc.maxPoolPreparedStatementPerConnectionSize}" />
<!-- 配置监控统计拦截的filters -->
<property name="filters" value="${jdbc.filters}" />
</bean>
(2)数据库配置jdbc.properties如下:
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://127.0.0.1:3306/tx?useSSL=false
jdbc.username=root
jdbc.password=95162437
jdbc.initialSize=10
jdbc.minIdle=10
jdbc.maxActive=50
jdbc.maxWait=60000
jdbc.timeBetweenEvictionRunsMillis=60000
jdbc.minEvictableIdleTimeMillis=300000
jdbc.validationQuery=SELECT 'x' FROM DUAL
jdbc.testWhileIdle=true
jdbc.testOnBorrow=false
jdbc.testOnReturn=false
jdbc.poolPreparedStatements=true
jdbc.maxPoolPreparedStatementPerConnectionSize=20
jdbc.filters=wall,stat
(3)配置JdbcTemplate,这里出于学习的目的先使用JdbcTemplate。
<!--配置JdbcTemplate-->
<bean id="JdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg name="dataSource" value="#{DruidDataSource}"></constructor-arg>
</bean>
(4)配置事物管理器
<!--DataSourceTransactionManager适用于使用JDBC来操作数据库的场景-->
<bean id="TransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" value="#{DruidDataSource}"></property>
</bean>
(5)配置开启注解事物管理
<!--开启注解事物管理-->
<tx:annotation-driven transaction-manager="TransactionManager"/>
完整的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:aop="http://www.springframework.org/schema/aop" 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
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<context:component-scan base-package="com.xust.iot"/>
<!--引入外部文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--配置数据连接池-->
<bean id="DruidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="url" value="${jdbc.url}"/>
<property name="driverClassName" value="${jdbc.driver}"/>
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${jdbc.initialSize}"/>
<property name="minIdle" value="${jdbc.minIdle}"/>
<property name="maxActive" value="${jdbc.maxActive}"/>
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${jdbc.maxWait}"/>
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" />
<property name="validationQuery" value="${jdbc.validationQuery}" />
<property name="testWhileIdle" value="${jdbc.testWhileIdle}" />
<property name="testOnBorrow" value="${jdbc.testOnBorrow}" />
<property name="testOnReturn" value="${jdbc.testOnReturn}" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小 如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false。-->
<property name="poolPreparedStatements" value="${jdbc.poolPreparedStatements}" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="${jdbc.maxPoolPreparedStatementPerConnectionSize}" />
<!-- 配置监控统计拦截的filters -->
<property name="filters" value="${jdbc.filters}" />
</bean>
<!--配置JdbcTemplate-->
<bean id="JdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg name="dataSource" value="#{DruidDataSource}"></constructor-arg>
</bean>
<!--Spring事务控制-->
<!--1.配置事务管理器,本身事物管理器是需要我们写一个切面类通过AOP编程的方式来实现,但是Spring提供了我们就直接使用Spring提供的事物管理器-->
<bean id="TransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" value="#{DruidDataSource}"/>
</bean>
<!--2.开启基于注解的事物管理-->
<!--属性transaction-manager就是事物管理器-->
<tx:annotation-driven transaction-manager="TransactionManager"></tx:annotation-driven>
<!--3.在需要事物的地方用@Transactional来告诉Spring这个地方需要进行事物管理-->
</beans>
完成上面的操作后,我们就可以在需要事物管理的地方使用@Transactional
注解标注在类或方法上,以此告诉Spring这里需要事物管理。当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性。另外, @Transactional 注解应该只被应用到 public 方法上,这是由 Spring AOP 的本质决定的。如果你在 protected、private 或者默认可见性的方法上使用 @Transactional 注解,这将被忽略,也不会抛出任何异常。
- @Transactional注解的属性总结
属性 | 需要的参数的类型 | 描述 |
---|---|---|
value | String | 指定使用的事务管理器(必须) |
propagation | enum:Propagation | 事务传播行为设置(可选) |
isolation | enum: Isolation | 事务隔离级别设置(可选) |
readOnly | boolean | 读写或只读事务,默认(false)读写 |
timeout | int | 事务超时时间设置,单位:秒 |
rollbackFor | Class[],必须继承自Throwable | 指定发生异常后哪些异常要回滚 |
rollbackForClassName | String[],必须继承自Throwable | 指定发生异常后哪些异常要回滚的异常类名字数组 |
noRollbackFor | Class[],必须继承自Throwable | 指定发生异常后哪些异常不回滚 |
noRollbackForClassName | String[],必须继承自Throwable | 指定发生异常后哪些异常不回滚的异常类名字数组 |
使用示例: DAO层,直接使用JdbcTemplate操作数据库,写3个方法分别用于:更新用户余额、更新图书库存、以及查询价格,具体如下:
package com.xust.iot.dao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BookDaoimp {
@Autowired
JdbcTemplate jdbcTemplate;
/**
* 减库存
*/
public void updateBookStock(Integer books,String isbn){
String sql="update book_stock set stock=stock-? where isbn=?";
jdbcTemplate.update(sql,books,isbn);
}
/**
* 减用户对余额
*/
public void updateUserBalance(Integer payment,String username){
String sql="updatess account set balance=balance-? where username=?";
jdbcTemplate.update(sql,payment,username);
}
/**
* 查询图书的价格
* @param isbn
* @return
*/
public Integer getBookPrice(String isbn){
String sql="select price from book where isbn=?";
return jdbcTemplate.queryForObject(sql,Integer.class,isbn);
}
}
业务层,写一个结账的方法,使用@Transactional告诉Spring要帮我们控制事务
package com.xust.iot.service;
import com.xust.iot.dao.BookDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class BookService {
@Autowired
BookDao bookDao;
/**
* 模拟结账
* @param username 用户姓名
* @param isbn 图书编号
* @param books 用户买了几本书
*/
@Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
public void cheockout(String username,String isbn,Integer books){
//减库存
bookDao.updateBookStock(books,isbn);
Integer price=bookDao.getBookPrice(isbn);
Integer payment=price*books;
//减账户余额
bookDao.updateUserBalance(payment,username);
}
}
4、基于XML的声明式事务管理配置和使用
其他的步骤和使用注解一样,不同点的核心的在下面:
<!--基于XML的事物管理-->
<!--1.配置事务管理器:这个和使用注解时一样-->
<bean id="TransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" value="#{DruidDataSource}"></property>
</bean>
<!--2.配置事物属性:使用<tx:advice>标签声明事物通知 -->
<tx:advice id="myTransaction" transaction-manager="TransactionManager">
<!--事物属性-->
<tx:attributes>
<tx:method name="get*" propagation="REQUIRED" read-only="true"/>
<tx:method name="cheockout" propagation="REQUIRED" isolation="REPEATABLE_READ"/>
</tx:attributes>
</tx:advice>
<!--3.配置事物切入点,把事物切入点和事物属性关联起来-->
<aop:config>
<aop:pointcut id="pointCut" expression="execution(* com.xust.iot.service.*.*(..))"/>
<aop:advisor advice-ref="myTransaction" pointcut-ref="pointCut"/>
</aop:config>
- 测试:在src/main/test包下新建测试类TxTest.java测试
package test;
import com.alibaba.druid.pool.DruidDataSource;
import com.xust.iot.service.BookService;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Date;
public class TxTest {
ApplicationContext ioc=new ClassPathXmlApplicationContext("ApplicationContext.xml");
private JdbcTemplate jdbcTemplate=ioc.getBean(JdbcTemplate.class);
@Test
public void txTest(){
BookService bookService=ioc.getBean(BookService.class);
bookService.cheockout("Tom","ISBN-005",3);
System.out.println("结账成功!");
}
}