文章目录
事务的特性
事务就是一组逻辑操作的组合,它们执行的结果要么全部成功,要么全部失败。
四个原则: ACID
- 原子性(Atomicity):一个事务就是一个不可再分解的单位,事务中的操作要么全部做,要么全部不做。原子性强调的是事务的整体
- 一致性(Consistency):事务执行后,所有的数据都应该保持一致状态。一致性强调的是数据的完整
- 隔离性(Isolation):多个数据库操作并发执行时,一个请求的事务操作不能被其它操作干扰,多个并发事务执行之间要相互隔离。隔离性强调的是并发的隔离
- 持久性(Durability):事务执行完成后,它对数据的影响是永久性的。持久性强调的是操作的结果
事务并发存在的问题
- 脏读: 一个事务读到了另一个事务没有提交的数据。
关键字:未提交
如果一个事务(A)读到另一个事务(B)并未提交的数据,恰好事务(B)由于某些原因导致了事务回滚,那么刚刚事务(A)就相当于读到了实际并不存在的数据。 - 不可重复读: 一个事务读到了另一个事务已提交修改的数据,对同一行数据查询两次,结果不一致。
关键字:update
比如在一个事务(A)中,查询了一次账户余额。这时另一个事务(B)在该账户中扣除一笔钱(比如自动还款)并提交了事务,这时事务(A)再次查询账户余额,发现余额变了,这就不可重复读了。很显然,这种情况同样是存在问题。 - 幻读(读取了insert、delete并已提交的数据):在一个事务中查询两次,但是第二次比第一次多查询出一些数据;两次查询中间有别的事务插入数据了并事务已提交。
关键词:insert、delete
- 丢失更新: 事务提交时,把其他事务已提交更新的数据覆盖了。
注意:不可重复读重点是修改,而幻读重点是新增或删除。
事务的隔离级别
隔离级别 | 隔离级别值 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交(Read uncommitted) | 0 | 可能 | 可能 | 可能 |
读已提交(不可重复读)(Read committed) | 1 | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 2 | 不可能 | 不可能 | 可能 |
可串行化(Serializable) | 3 | 不可能 | 不可能 | 不可能 |
Read Uncommitted(读未提交):不解决任何问题。
Read Committed(读已提交):解决脏读。
Repetable Read(可重复读):解决脏读、不可重复读。
Serilizable:解决脏读、不可重复读、幻读。
四种隔离级别,自上而下级别逐级增高,但并发性能逐级降低。
MySQL默认采用Repetable Read隔离级别;Oracle默认采用Read Committed隔离级别。
TransactionDefinition
接口中定义了五个表示隔离级别的常量:
TransactionDefinition.ISOLATION_DEFAULT
:使用底层数据库默认的隔离级别。TransactionDefinition.ISOLATION_READ_UNCOMMITTED
:允许一个事务可以读取另一个事务修改但还没有提交的数据。可能导致脏读、幻读、不可重复读,基本不会使用该隔离级别。TransactionDefinition.ISOLATION_READ_COMMITTED
:允许一个事务只能读取另一个事务已经提交的数据。可防止脏读,但幻读、不可重复读可能发生,推荐值。TransactionDefinition.ISOLATION_REPEATABLE_READ
:一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。可以防止脏读、不可重复读,但幻读任然可能发生。TransactionDefinition.ISOLATION_SERIALIZABLE
:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,防止脏读、不可重复读以及幻读。但是会严重影响程序的性能,基本不会使用该隔离级别。
幻读解决方案
对行记录加锁,在事务中使用select … for update对某记录进行预先占用,只要本事务还在,其他事务就别想占有它。
事务的基本原理
Mysql 在没有手动开启事务的情况下,默认一条执行命令就是一个事务。
对于 JDBC 操作数据库,想要用到事务,需要按照以下步骤进行:
- 1、加载数据库驱动:
Class.forName("com.mysql.jdbc.Driver");
- 2、获取连接:
Connection con = DriverManager.getConnection();
- 3、开启事务:
con.setAutoCommit(false);
- 4、创建 statement 实例,专门用来执行
sql Statement statement = con.createStatement();
- 5、执行CRUD
- 6、提交事务/回滚事务:
con.commit() / con.rollback();
- 7、关闭连接:
conn.close();
使用 Spring 的事务管理功能后,开启事务、提交事务、回滚事务
由 Spring 自动完成。
Spring 自动在 CRUD 之前和之后开启事务和关闭事务的原理如下:
- 1、配置开启事务注解驱动,在被需要事务管理的类或者方法上加上注解
@Transactional
。 - 2、Spring 启动的时候会解析生成相关的bean,为
@Transactional
类和方法生成代理类,然后在代理类中自动把相关的事务操作处理掉了。
Spring 事务管理 API 分析
三个高级抽象接口:
PlatformTransactionManager
平台相关事务管理器 (控制事务核心API )TransactionDefinition
事务定义信息 (在配置文件,配置中如何对事务进行控制 )TransactionStatus
事务状态信息 (在事务运行某个时刻,当前状态信息 )
三个接口关系:
Spring 进行事务管理对象是 PlatformTransactionManager
, 根据配置文件中定义事务管理信息 TransactionDefinition
进行事务管理, 通过TransactionStatus
获取某时事务状态信息。
PlatformTransactionManager
接口定义:
public interface PlatformTransactionManager {
/**
* 获取某个时刻事务状态
*/
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
/**
* 提交事务
*/
void commit(TransactionStatus status) throws TransactionException;
/**
* 回滚事务
*/
void rollback(TransactionStatus status) throws TransactionException;
}
Spring 为不同的持久化框架提供了不同PlatformTransactionManager
接口实现:
DataSourceTransactionManager
:适用于使用JDBC和iBatis进行数据持久化操作的情况。HibernateTransactionManager
:适用于使用Hibernate进行数据持久化操作的情况。JpaTransactionManager
:适用于使用JPA进行数据持久化操作的情况。
对 Connection 进行事务管理:DataSourceTransactionManager
对 Hibernate 的 session 进行事务管理:HibernateTransactionManager
TransactionDefinition
在配置文件中,对事务进行配置,对应管理信息 。
包含了事务的静态属性,比如:事务传播行为、事务隔离级别 、超时时间、是否只读
等等。
Spring 为我们提供了一个默认的实现类:DefaultTransactionDefinition
,该类适用于大多数情况。如果该类不能满足需求,可以通过实现 TransactionDefinition
接口来实现自己的事务定义。
TransactionStatus
某个时刻的事务状态信息。
接口定义:
public interface TransactionStatus extends SavepointManager, Flushable {
/**
* 是否为新建事务
*/
boolean isNewTransaction();
/**
* 判断是否有保存点
*/
boolean hasSavepoint();
/**
* 设置标记状态
*/
void setRollbackOnly();
/**
* 判断事务是否标记为回滚 (代码执行rollback 称为标记回滚,事务还需要提交)
*/
boolean isRollbackOnly();
/**
* 事务已经被刷出 (flush和commit 不是一个概念)
*/
@Override
void flush();
/**
* 事务是否完成
*/
boolean isCompleted();
}
事务的传播行为
事务传播行为用于解决两个被事务管理的方法互相调用问题。
七种事务传播行为如下:
TransactionDefinition.PROPAGATION_REQUIRED
:默认值,如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。TransactionDefinition.PROPAGATION_REQUIRES_NEW
:新建事务;如果当前有事务,就挂起当前事务。新建的事务和被挂起的事务没有任何关系,是两个独立的事务,外层事务失败回滚后,不能回滚内层事务执行的结果。TransactionDefinition.PROPAGATION_SUPPORTS
:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。TransactionDefinition.PROPAGATION_NOT_SUPPORTED
:以非事务方式运行,如果当前存在事务,则把当前事务挂起。TransactionDefinition.PROPAGATION_NEVER
:以非事务方式运行,如果当前存在事务,则抛出异常。TransactionDefinition.PROPAGATION_MANDATORY
:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。TransactionDefinition.PROPAGATION_NESTED
:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
事务超时
指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒。
TIME_DEFAULT
:默认超时时间 ,默认值 -1 ,使用数据库默认超时时间
事务的只读属性
在 TransactionDefinition 中以 boolean 类型来表示该事务是否只读。
事务的回滚规则
事务中抛出了未检查异常(继承自 RuntimeException 的异常),则默认将回滚事务。
如果没有抛出任何异常,或者抛出了已检查异常,则仍然提交事务。
也可以根据需要人为控制事务在抛出某些未检查异常时任然提交事务,或者在抛出某些已检查异常时回滚事务。
Spring 对事务管理的支持
Spring 提供两种事务管理方式 : 编程式事务管理 、 声明式事务管理
编程式事务管理(基本不用)
在开发代码中,侵入式事务管理 。
基于底层 API 的编程式事务管理:
public class BankServiceImpl implements BankService {
private BankDao bankDao;
private TransactionDefinition transactionDefinition;
private PlatformTransactionManager transactionManager;
public boolean transfer() {
TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
boolean result = false;
try {
result = bankDao.transfer();
transactionManager.commit(transactionStatus);
} catch (Exception e) {
result = false;
transactionManager.rollback(transactionStatus);
System.out.println("Transfer Error!");
}
}
}
配置文件:
<bean id="bankService" class="xxx.BankServiceImpl">
<property name="bankDao" ref="bankDao"/>
<property name="txManager" ref="transactionManager"/>
<property name="transactionDefinition">
<bean class="org.springframework.transaction.support.DefaultTransactionDefinition">
<property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
</bean>
</property>
</bean>
在服务类中增加了两个属性:TransactionDefinition
用于定义一个事务;PlatformTransactionManager
用于执行事务管理操作。
PlatformTransactionManager.getTransaction(…)
方法启动一个事务。
创建并启动了事务之后,便可以开始编写业务逻辑代码,然后在适当的地方执行事务的提交或者回滚。
基于 TransactionTemplate 的编程式事务管理:
基于底层API实现事务管理方式很容易理解,但是,事务管理的代码散落在业务逻辑代码中,破坏了原有代码的条理性,并且每一个业务方法都包含了类似的启动事务、提交/回滚事务的样板代码。
Spring 提供了简化的方法,这就是 Spring 在数据访问层非常常见的模板回调模式。
public class BankServiceImpl implements BankService {
private BankDao bankDao;
private TransactionTemplate transactionTemplate;
public boolean transfer() {
return (Boolean) transactionTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
// 被事务管理的代码
Object result;
try {
result = bankDao.transfer();
} catch (Exception e) {
status.setRollbackOnly();
result = false;
System.out.println("Transfer Error!");
}
return result;
}
});
}
}
配置文件:
<bean id="bankService" class="xxx.BankServiceImpl">
<property name="bankDao" ref="bankDao"/>
<property name="transactionTemplate" ref="transactionTemplate"/>
</bean>
声明式事务管理
基于AOP思想,在创建目标对象时,为目标进行代理,通过环绕通知,动态为目标添加事务管理代码。
基于 <tx>
命名空间的声明式事务管理:(最推荐)
<!-- 平台事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 将数据源注入平台事务管理器,connection.setAutoCommit(false); -->
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 事务管理Advice -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置事务属性 TransactionDefinition -->
<tx:attributes>
<!-- name是方法名 ,*匹配任意字符串 -->
<!--
isolation 配置隔离级别
propagation 配置传播行为
timeout 超时时间
read-only 是否只读
rollback-for 配置一些异常类型,发生这些异常,事务回滚
no=rollback-for 配置一些异常类型,发生这些异常,事务不会回滚
-->
<tx:method name="transfer" isolation="DEFAULT" propagation="REQUIRED" timeout="-1" read-only="false" />
<!-- 对增、删、改方法进行事务支持 -->
<tx:method name="create*" propagation="REQUIRED" rollback-for="Exception" />
<tx:method name="save*" propagation="REQUIRED" rollback-for="Exception" />
<tx:method name="update*" propagation="REQUIRED" rollback-for="Exception" />
<tx:method name="remove*" propagation="REQUIRED" rollback-for="Exception" />
<!-- 对查找方法进行只读事务 -->
<tx:method name="get*" propagation="SUPPORTS" read-only="true" />
<tx:method name="load*" propagation="SUPPORTS" read-only="true" />
<tx:method name="find*" propagation="SUPPORTS" read-only="true" />
<!-- 指定所有get开头的方法执行在只读事务上下文中 -->
<tx:method name="get*" read-only="true"/>
<!-- 其余方法执行在默认的读写上下文中 -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 切面 -->
<aop:config proxy-target-class="true">
<!-- <aop:pointcut/>元素定义AspectJ的切面表示法,这里是表示 cn.yq.service 包下的任意方法。 -->
<aop:pointcut id="pointcut" expression="execution(* cn.yq.service.*.*(..))" />
<!-- <aop:advisor/>把这个切面和tx:advice绑定在一起,表示当这个切面:pointcut 执行时tx:advice定义的通知逻辑将被执行 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
</aop:config>
基于 @Transactional 的声明式事务管理:
<!-- 平台事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 将数据源注入平台事务管理器,connection.setAutoCommit(false); -->
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 利用注解进行事务管理,开启注解驱动事务管理-->
<tx:annotation-driven transaction-manager="transactionManager"/>
在需要管理事务方法或者类上面 应用 @Transactional
注解。
@Transactional注解,支持事务属性配置 :
- isolation 隔离级别
- propagation 传播行为
- timeout 超时时间
- readOnly 是否只读
- rollbackFor发生异常回滚
- noRollbackFor 发生异常不回滚
SpringBoot 对事务管理
在SpringBoot中推荐使用@Transactional
注解来申明事务。
@EnableAspectJAutoProxy(exposeProxy = true)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
当引入依赖之后,SpringBoot会自动默认注入管理事物的相关对象,所以不需要任何额外配置就可以用@Transactional
注解进行事务的使用。
事务常见问题总结
异常并没有被 ”捕获“ 到
案例代码:
@Transactional
public void isertUser2(User user) throws Exception { // 插入用户信息
userMapper.insertUser(user); // 手动抛出异常
throw new SQLException("数据库异常");
}
看上面这个代码,其实并没有什么问题,手动抛出一个 SQLException
来模拟实际中操作数据库
发生的异常,在这个方法中,既然抛出了异常,那么事务应该回滚,实际却不如此,仍然是可以插入一条用户数据的。
那么问题出在哪呢?
因为 Spring Boot 默认的事务规则是遇到运行异常(RuntimeException)
和程序错误(Error)
才会回滚。比如抛出的 RuntimeException
就没有问题,但是抛出 SQLException
(非运行时异常)就无法回滚了。针对非运行时异常,如果要进行事务回滚的话,可以在@Transactional
注解中使用 rollbackFor
属性来指定异常,比如 @Transactional(rollbackFor = Exception.class)
,这样就没有问题了,所以在实际项目中,一定要指定异常。
异常被 ”吃“ 掉
案例代码:
@Transactional(rollbackFor = Exception.class)
public void isertUser3(User user) {
try {
// 插入用户信息
userMapper.insertUser(user);
// 手动抛出异常
throw new SQLException("数据库异常");
} catch (Exception e) {
// 异常处理逻辑
}
}
运行上面的代码,发现,仍然是可以插入一条用户数据,说明事务并没有因为抛出异常而回滚。
因为抛出的异常被自己捕获了,并没有往上抛。
那这种怎么解决呢?
直接往上抛,给上一层来处理即可,千万不要在事务中把异常自己 ”吃“ 掉。
事务的范围
案例代码:
@Override
@Transactional(rollbackFor = Exception.class)
public synchronized void isertUser4(User user) {
// 实际中的具体业务……
userMapper.insertUser(user);
}
因为要考虑并发问题,所以在业务层代码的方法上加了个 synchronized
关键字。
举个实际的场景,比如一个数据库中,针对某个用户,只有一条记录,下一个插入动作过来,会先判断该数据库中有没有相同的用户,如果有就不插入,就更新,没有才插入,所以理论上,数据库中永远就一条同一用户信息,不会出现同一数据库中插入了两条相同用户的信息。
但是在压测时,数据库中出现有两条同一用户的信息。
分析原因: 在于事务的范围和锁的范围问题。
从上面方法中可以看到,方法上是加了事务的,那么也就是说,在执行该方法开始时,事务启动,执行
完了后,事务关闭。但是 synchronized
没有起作用,其实根本原因是因为事务的范围比锁的范围大。
也就是说,在加锁的那部分代码执行完之后,锁释放掉了,但是事务还没结束,此时另一个线程进来
了,事务没结束的话,第二个线程进来时,数据库的状态和第一个线程刚进来是一样的。即由于mysql
Innodb引擎的默认隔离级别是可重复读(在同一个事务里,SELECT的结果是事务开始时时间点的状
态),线程二事务开始的时候,线程一还没提交完成,导致读取的数据还没更新。第二个线程也做了插
入动作,导致了脏数据。
怎么避免这个问题?
第一,把事务去掉即可(不推荐);
第二,在调用该 service
的地方加锁,保证锁的范围比事务的范围大即可。
事务失效的场景
1、注解@Transactional配置的方法非public权限修饰;
2、注解@Transactional所在类非Spring容器管理的bean;
3、注解@Transactional所在类中,注解修饰的方法被类内部方法调用;
4、业务代码抛出异常类型非RuntimeException,事务失效;
5、业务代码中存在异常时,使用try…catch…语句块捕获,而catch语句块没有throw new RuntimeExecption异常;
6、注解@Transactional中Propagation属性值设置错误即Propagation.NOT_SUPPORTED(一般不会设置此种传播机制)
7、mysql关系型数据库,且存储引擎是MyISAM而非InnoDB,则事务会不起作用(基本开发中不会遇到);
注解修饰的方法被类内部方法调用
在类A里面有方法a 和方法b, 方法a没有加@Transactional,然后方法b上面用 @Transactional加了方法级别的事务,在方法a里面调用了方法b, 方法b里面的事务不会生效。
原因: Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.b(),此时的b方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。
解决方案: 类内部使用其代理类调用事务方法。
AopContext.currentProxy()
可以获取当前上下文的代理类。