一、事务概述
事务的作用:
将一个事务中所有的sql语句当作一个整体,要么全都成功,要么全都失败,通过这样的方式来保证数据的正确性;
事务的四大特性:
- 原子性:atomic,即事务中的多条语句是一个整体且不可分割,要么都成功要么都失败
- 隔离性:isolation,多个事务之间的执行不能相互影响
- 一致性:consistence,事务开始前和事务结束后,数据保持一致,就比如转账前后的总金额要保证一致
- 持久性:durability,即事务一旦提交,那么对数据造成的修改是永久的,不可逆的
事务的几个隔离级别:
类型 | 说明 |
---|---|
脏读 | 即一个事务读取到另一个事务未提交的数据 |
不可重复读 | 即一个事务读取到另一个事务已经提交的数据 |
幻读 | 即一个事务多次读取到的数据量不同 |
二、声明式事务概述
什么是声明式事务?
即通过Spring的面向切面编程思想来实现事务的管理方式,即使用面向切面编程思想来管理事务就叫做声明式事务,Spring声明式事务简写为TX;
实现原理:
利用面向切面编程,在方法执行前开启事务,执行完方法之后提交或回滚事务从而实现单个方法中的所有操作都在同一个事务中;
Spring中声明式事务的隔离级别:
事务级别 | 含义 | 说明 |
---|---|---|
ISOLATION_DEFAULT | 使用数据库的默认隔离级别 | mysql是可重复读,而Oracle为不可重复度 |
ISOLATION_READ_UNCOMMITTED | 读未提交 | 即存在脏读问题 |
ISOLATION_READ_COMMITTED | 读已提交 | 解决了脏读问题,但是存在不可重复读的问题 |
ISOLATION_REPEATABLE_READ | 可重复读 | 解决可以重复读的问题,但是存在幻读的问题 |
ISOLATION_SERIALIZABLE | 串行化 | 解决了幻读的问题,事务需要排队进行,但性能差 |
Spring中声明式事务的事务传播行为:
行为 | 含义 | 说明 |
---|---|---|
REQUIRED | 需要 | 如果当前存在事务则加入,如果没有就创建一个新事务 |
SUPPORTS | 支持 | 如果存在事务就加入事务,如果没有就以非事务方式执行 |
MANDATORY | 强制 | 如果当前存在事务,则加入该事务,如果没有则抛出异常 |
REQUIRES_NEW | 新 | 创建一个新事务,如果当前存在事务就把当前事务挂起,使用新事务 |
NOT_SUPPORTED | 不支持 | 以非事务的方式执行,如果当前存在事务就把当前事务挂起 |
NEVER | 绝不 | 以非事务的方式运行,如果当前存在事务,抛出异常 |
NESTED | 嵌套 | 如果当前存在事务就创建一个新事务当作当前事务的子事务嵌套运行,如果当前不存在事务依然会创建一个事务 |
如何理解事务的传播行为:
即被调用方法对事务的要求,看起来比较抽象,但其实就是设置这个方法对于事务的要求,而方法不能一开始就自己调用自己,所以是被调用方法对事务的要求,而调用这个方法可能是有事务的也有可能是没有事务的,所以才存在这么多内容;
Spring事务相关API:
相关接口或类 | 名称 | 说明 |
---|---|---|
PlatformTransactionManager | 事务管理接口 | 定义了提交事务、获取事务状态、回滚事务等方法的接口 |
TransactionStatus | 事务状态接口 | 定义了Spring内部事务状态的规范,即它描述某一个时间点上某个事务状态的信息 |
TransactionDefinition | 事务信息定义接口 | 定义了事务的隔离级别、传播行为、超时时间、是否只读等 |
三、声明式事务的使用
1.环境准备
maven依赖:
<!--Spring-context 即IOC容器-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<!--Spring jdbc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<!--spring test-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<!--阿里巴巴连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.18</version>
</dependency>
<!--日志文件系统-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!--第三方实现AOP类-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
实体类:
package com.ps.pojo;
public class Account {
private int id;
private int uid;
private double money;
@Override
public String toString() {
return "Account{" +
"id=" + id +
", uid=" + uid +
", money=" + money +
'}';
}
public Account() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getUid() {
return uid;
}
public void setUid(int uid) {
this.uid = uid;
}
public double getMoney() {
return money;
}
public void setMoney(double money) {
this.money = money;
}
public Account(int id, int uid, double money) {
this.id = id;
this.uid = uid;
this.money = money;
}
}
数据库配置内容:
mysql.driver:com.mysql.jdbc.Driver
mysql.url:jdbc:mysql://localhost:3306/javaclass
mysql.username:root
mysql.password:123456
数据访问层:
package com.ps.dao.Impl;
import org.springframework.jdbc.core.JdbcTemplate;
public class AccountDaoImpl {
private JdbcTemplate jdbcTemplate;
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public void updateMoney(int id, double money) {
String sql = "update account set money = money +? where id=?";
jdbcTemplate.update(sql, money, id);
}
}
业务层:
package com.ps.service;
import com.ps.dao.Impl.AccountDaoImpl;
public class AccountServiceImpl {
private AccountDaoImpl dao;
public void setDao(AccountDaoImpl dao) {
this.dao = dao;
}
public void transfer(int username, int inUser, double money) {
dao.updateMoney(username, -money);
dao.updateMoney(inUser, money);
}
}
测试代码:
可能会好奇为什么放到这里,其实是因为对于这两配置方式,测试代码需要修改的内容太少了,所以就放到前面了
package com.ps.dap;
import com.ps.service.AccountServiceImpl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
//xml配置使用这个
@ContextConfiguration("classpath:translation.xml")
/*注解配置使用下面这个
classes后面的内容填你主要配置类的类名就行了,剩下的配置类就在主配置类上加注解就好了
如果你喜欢在后面疯狂加的话,那就随意了
@ContextConfiguration(classes = SpringConfig.class)
*/
public class TestUserDao {
@Autowired
private AccountServiceImpl accountService;
@Test
public void test01() {
accountService.transfer(6, 7, 200);
}
}
2.XML配置事务管理
Spring配置文件:
<?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:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!--创建数据源,我是用的是阿里巴巴的Druid-->
<bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
<property name="driverClassName" value="${mysql.driver}"></property>
<property name="url" value="${mysql.url}"></property>
<property name="username" value="${mysql.username}"></property>
<property name="password" value="${mysql.password}"></property>
</bean>
<!--创建spring jdbc模板类-->
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="template">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--创建account dao实例对象-->
<bean class="com.ps.dao.Impl.AccountDaoImpl" id="accountDao">
<property name="jdbcTemplate" ref="template"></property>
</bean>
<!--配置业务层对象-->
<bean class="com.ps.service.AccountServiceImpl" id="accountService">
<property name="dao" ref="accountDao"></property>
</bean>
<!--下面就是配置事务相关的内容了-->
<!--配置事务管理器,建议使用transactionManager-->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager">
<!--这里一定要配置数据源,否则无法管理-->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--配置AOP-->
<aop:config>
<aop:pointcut id="pt" expression="execution(public void com.ps.service.*.*(..))"/>
<!--定义事务规则和切入点的关系
pointcut-ref:引入已经存在的切入点
advice-ref:事务的管理规则的id
-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt"></aop:advisor>
</aop:config>
<tx:advice id="txAdvice">
<tx:attributes>
<!--配置方法使用的事务规则
name:方法名
isolation:事务隔离级别,默认隔离级别是可重复读
propagation:传播行为,默认值是:REQUIRED
timeout:超时时间,-1即永不超时知道执行结束;如果是正数,单位是毫秒
read-only:是否只读,只读的话不可以进行修改
注意这里也可以使用之前的符号来使得规则通用,比如:find*即所有以find开头的查询类都应该是这个事务管理规则
-->
<tx:method name="transfer" isolation="DEFAULT" read-only="false"
propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
</beans>
3.注解配置事务管理
如果想要使用注解配置事务管理器,那么就需要在配置文件中添加几个配置,其实就是开启注解事务支持;
配置文件内容配置:
即把上面配置
<!-- 添加事务注解支持 -->
<tx:annotation-driven />
<!-- 创建事务管理器 -->
<!-- 建议id不要改: 固定使用transactionManager -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="ds"/>
</bean>
修改后的service层内容:
package com.ps.service;
import com.ps.dao.Impl.AccountDaoImpl;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public class AccountServiceImpl {
private AccountDaoImpl dao;
public void setDao(AccountDaoImpl dao) {
this.dao = dao;
}
/**
* Transactional:修饰类,方法
* 作用:给方法添加事务的管理功能
* propagation:配置传播行为,也就是配置这个方法的事务,默认就是Propagation.REQUIRED
* isolation:配置事务的隔离级别
* value: 指定事务管理器名称,一般只有一个事务管理器,所以一般不指定
* rollbackFor:指定回滚的异常类型,换句话说就是发生什么情况采取回滚,因为有写异常不影响你的事务
* 就比如你添加是日志打印的功能,结果日志打印失败了,由于拿不到系统IO,这个时候就不需要回滚事务
* noRollbackFor:设置不回滚的异常类型,如果方法内出现了某个异常将不会回滚
* readOnly:是否只读,默认为false,表示只能获得数据而不能修改,一般用于查询语句
* timeout: 是否超时, 默认-1表示不超时, 单位毫秒,
*/
//如果你不知道怎么加事务,可以直接默认,如果不满足在更换
@Transactional()
public void transfer(int username, int inUser, double money) {
dao.updateMoney(username, -money);
// System.out.println(100/0);
dao.updateMoney(inUser, money);
}
}
4.纯注解配置
纯注解配置就比较简单了因为上面我们已经配置好了事务,接下来就是配置好原配置文件中的其他内容即可
数据访问层:
package com.ps.dao.Impl;
import com.ps.config.JdbcConfig;
import jdk.nashorn.internal.scripts.JD;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class AccountDaoImpl {
@Autowired
//由于注解配置不需要setter方法也可以注入所以代码可以更简单
private JdbcTemplate jdbcTemplate;
public void updateMoney(int id, double money) {
String sql = "update account set money = money +? where id=?";
jdbcTemplate.update(sql, money, id);
}
}
业务层
package com.ps.service;
import com.ps.dao.Impl.AccountDaoImpl;
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;
@Service
public class AccountServiceImpl {
@Autowired
private AccountDaoImpl dao;
/**
* Transactional:修饰类,方法
* 作用:给方法添加事务的管理功能
* propagation:配置传播行为,也就是配置这个方法的事务,默认就是Propagation.REQUIRED
* isolation:配置事务的隔离级别
* value: 指定事务管理器名称,一般只有一个事务管理器,所以一般不指定
* rollbackFor:指定回滚的异常类型,换句话说就是发生什么情况采取回滚,因为有写异常不影响你的事务
* 就比如你添加是日志打印的功能,结果日志打印失败了,由于拿不到系统IO,这个时候就不需要回滚事务
* noRollbackFor:设置不回滚的异常类型,如果方法内出现了某个异常将不会回滚
* readOnly:是否只读,默认为false,表示只能获得数据而不能修改,一般用于查询语句
* timeout: 是否超时, 默认-1表示不超时, 单位毫秒,
*/
@Transactional()
public void transfer(int username, int inUser, double money) {
dao.updateMoney(username, -money);
// System.out.println(100/0);
dao.updateMoney(inUser, money);
}
}
JDBC配置类
package com.ps.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
//表示这是一个配置类
@Configuration
//配置导入外部的配置文件
@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {
@Value("${mysql.url}")
private String url;
@Value("${mysql.username}")
private String username;
@Value("${mysql.driver}")
private String driver;
@Value("${mysql.password}")
private String password;
@Bean//修饰方法,方法的返回值会自动添加到IOC容器中,带有这个注解的放,会被放到IOC容器中获取
public DataSource getDataSource() {
//创建第三方连接池
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setDriverClassName(driver);
dataSource.setPassword(password);
dataSource.setUsername(username);
//返回数据源
return dataSource;
}
@Bean
public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
//直接调用获得数据源,并返回JDBC模板对象
return new JdbcTemplate(dataSource);
}
}
Spring主配置类
package com.ps.config;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
//标识这是一个配置文件
@Configuration
//开启注解支持,即该包下的所有通过注解的都可以生效,如果不在该包下,则无法生效
@ComponentScan("com.ps")
//开启事务注解支持
@EnableTransactionManagement()
//导入外部配置类
@Import(JdbcConfig.class)
//开启AOP注解支持,可以不写,因为再@EnableTransactionManagement中已经有相关属性了
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class SpringConfig {
@Bean
public DataSourceTransactionManager getTransactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
5.Spring编程式事务
概述:
即通过代码来管理事务
编程式事务和声明式事务的区别:
编程式事务通过代码来实现,也就是把管理事务的代码写到了业务层的代码中,而这种方式被称为入侵了业务层的代码,而声明式编程是通过代理来实现的,业务层不会出现其他层的代码,保持了业务层的一致性;
层污染:即一层中出现了另一层的代码(就比如上面的业务层出现了持久层的代码);
业务层代码:
package com.ps.service;
import com.ps.dao.Impl.AccountDaoImpl;
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 org.springframework.transaction.support.TransactionTemplate;
@Service
public class AccountServiceImpl {
@Autowired
private AccountDaoImpl dao;
private TransactionTemplate transactionTemplate;
public void setDao(AccountDaoImpl dao) {
this.dao = dao;
}
public void setTransactionTemplate(TransactionTemplate transactionTemplate) {
this.transactionTemplate = transactionTemplate;
}
/**
* Transactional:修饰类,方法
* 作用:给方法添加事务的管理功能
* propagation:配置传播行为,也就是配置这个方法的事务,默认就是Propagation.REQUIRED
* isolation:配置事务的隔离级别
* value: 指定事务管理器名称,一般只有一个事务管理器,所以一般不指定
* rollbackFor:指定回滚的异常类型,换句话说就是发生什么情况采取回滚,因为有写异常不影响你的事务
* 就比如你添加是日志打印的功能,结果日志打印失败了,由于拿不到系统IO,这个时候就不需要回滚事务
* noRollbackFor:设置不回滚的异常类型,如果方法内出现了某个异常将不会回滚
* readOnly:是否只读,默认为false,表示只能获得数据而不能修改,一般用于查询语句
* timeout: 是否超时, 默认-1表示不超时, 单位毫秒,
*/
@Transactional()
public void transfer(int username, int inUser, double money) {
System.out.println(dao.getClass());
dao.updateMoney(username, -money);
// System.out.println(100/0);
dao.updateMoney(inUser, money);
}
}
主配置类中的代码
@Bean
public TransactionTemplate transactionTemplate(DataSourceTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}
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:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!--创建数据源-->
<bean class="com.alibaba.druid.pool.DruidDataSource" id="dataSource">
<property name="driverClassName" value="${mysql.driver}"></property>
<property name="url" value="${mysql.url}"></property>
<property name="username" value="${mysql.username}"></property>
<property name="password" value="${mysql.password}"></property>
</bean>
<!--创建spring jdbc模板类-->
<bean class="org.springframework.jdbc.core.JdbcTemplate" id="template">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!--创建account dao实例对象-->
<bean class="com.ps.dao.Impl.AccountDaoImpl" id="accountDao">
<property name="jdbcTemplate" ref="template"></property>
</bean>
<!--配置业务层对象-->
<bean class="com.ps.service.AccountServiceImpl" id="accountService">
<property name="dao" ref="accountDao"></property>
</bean>
<!-- 创建事务管理器 -->
<!-- 建议id不要改: 固定使用transactionManager -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean class="org.springframework.transaction.support.TransactionTemplate" id="transactionTemplate">
<property name="transactionManager" ref="transactionManager"></property>
</bean>
</beans>
运行结果: