日常开发中,经常碰到数据库事务,数据库表锁,行锁,spring事务,数据库事务隔离等级,spring事务隔离等级,spring事务传播等级,事务失效场景等很多概念。他们到底是什么意思?又有哪些联系呢?概念太多,容易混乱,今天我就用一片文章给大家从头理清。
事务基本概念及解释参考帖子:事务基础定义
事务是什么?
首先,什么是数据库事务呢?引用百度百科的解释--数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
简单总结:事务是数据库提供的一种在多线程环境下将多个动作合并成不可分割的一个操作的一种机制。
事务的特性(ACID):
原子性(Atomicity):原子性是指一个事务中的操作,要么全部成功,要么全部失败,如果失败,就回滚到事务开始前的状态。
一致性(Consistency):一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。那转账举栗子,A账户和B账户之间相互转账,无论如何操作,A、B账户的总金额都必须是不变的。
隔离性(Isolation):隔离性是当多个用户 并发的 访问数据库时,如果操作同一张表,数据库则为每一个用户都开启一个事务,且事务之间互不干扰,也就是说事务之间的并发是隔离的。再举个栗子,现有两个并发的事务T1和T2,T1要么在T2开始前执行,要么在T2结束后执行,如果T1先执行,那T2就在T1结束后在执行。关于数据的隔离性级别,将在后文讲到。
持久性(Durability):持久性就是指如果事务一旦被提交,数据库中数据的改变就是永久性的,即使断电或者宕机的情况下,也不会丢失提交的事务操作。
说明:原子性,一致性,持久性。数据库已经实现且客户端程序员不可更改。隔离性,出于性能考虑,数据库为客户端程序员提供了不同的隔离等级,供客户端程序员根据实际业务场景需求自我选择。
什么是事务的隔离性(Isolation)呢?
隔离性是指,多个用户的并发事务访问同一个数据库时,一个用户的事务不应该被其他用户的事务干扰,多个并发事务之间要相互隔离。
如果不考虑隔离性,会发生什么事呢?
1.脏读:
脏读是指一个事务在处理数据的过程中,读取到另一个未提交事务的数据。
2.不可重复读:
不可重复读是指对于数据库中的某个数据,一个事务范围内的多次查询却返回了不同的结果,这是由于在查询过程中,数据被另外一个事务修改并提交了。
不可重复读和脏读的区别是,脏读读取到的是一个未提交的数据,而不可重复读读取到的是另一个事务提交的数据。
而不可重复读在一些情况也并不影响数据的正确性,比如需要多次查询的数据也是要以最后一次查询到的数据为主。
3.幻读
幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。
幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。
总的来说,解决不可重复读的方法是 锁行,解决幻读的方式是 锁表。
数据库事务的隔离等级:
四种隔离级别解决了上述问题
1.读未提交(Read uncommitted):
这种事务隔离级别下,select语句不加锁。
此时,可能读取到不一致的数据,即“读脏 ”。这是并发最高,一致性最差的隔离级别。
2.读已提交(Read committed):
可避免 脏读 的发生。
在互联网大数据量,高并发量的场景下,几乎 不会使用 上述两种隔离级别。
3.可重复读(Repeatable read):
MySql默认隔离级别。
可避免 脏读 、不可重复读 的发生。
4.串行化(Serializable ):
可避免 脏读、不可重复读、幻读 的发生。
以上四种隔离级别最高的是 Serializable 级别,最低的是 Read uncommitted 级别,当然级别越高,执行效率就越低。像 Serializable 这样的级别,就是以 锁表 的方式,使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。SQL Server默认的隔离级别为rcommitted,MySQL默认的隔离级别为Repeatable read, Oracle默认隔离级别为Read committed。
SQL Server和MySQL数据库支持上面四种隔离级别,而 Oracle数据库只支持Serializable (串行化) 级别和 Read committed (读已提交) 这两种级别。
不同隔离等级解决的问题
so,隔离等级中保证了不可重复读的等级,会锁行,保证了幻读的等级,会锁表。
数据库事务如何使用?
1.首先我们在建表时选择该支持事务的数据库引擎,例如mySQL数据库默认使用支持事务的Innodb引擎,
2.设置事务隔离等级,若不设置则采用默认等级
3.开启事务(start transaction)
执行sql操作(普通sql操作)
提交/回滚(commit/rollback)
通过上诉步骤我们就完成了数据库层面的一次完整事务体验。
jdbc控制事务:
我们日常开发中,我们就需要在应用代码中控制数据库的事务,这时候JDBC为我们提供了标准api可控制数据库的事务,例子如下:
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try{
// 1.注册 JDBC 驱动
Class.forName("com.mysql.jdbc.Driver");
// 2.创建链接
System.out.println("连接数据库...");
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/my_db","root","root");
//设置事务隔离等级
conn.setTransactionIsolation(2)
//关闭事务自动提交(jdbc默认事务自动提交,此处仅做演示,因此仅做演示)
conn.setAutoCommit(false);
// 3.发起请求
stmt = conn.createStatement();
String sql = "SELECT id, name, url FROM websites";
rs = stmt.executeQuery(sql);
// 4.输出结果
System.out.print("查询结果:" + rs);
//手动提交事务
con.commit();
// 关闭资源(演示代码,不要纠结没有写在finally中)
rs.close();
stmt.close();
conn.close();
} catch (SQLException se)
se.printStackTrace();
}catch(Exception e){
e.printStackTrace();
}
从上面可知,我们通过jdbc提供的api可以实现在java代码中控制事务,但是不够优雅,耦合度很很高,这时候spring大好人就过来帮忙了,帮我们再次封装。
spring事务:
spring在jdbc的基础上再次封装(但是根本基础还是依赖数据库提供的事务)。
Spring事务,分为编程式事务和声明式事务两种形式(两种写法)
编程式事务:
编程式事务对代码侵入程度较高,因此不推荐使用,此处不再缀述。
声明式事务:
@Transactional(rollbackFor = Exception.class)
public Boolean approvalRecharge(ApprovalRechargeDto approvalRechargeDto, Long accountId) {
MicroClinicPayRecord microClinicPayRecord = new MicroClinicPayRecord();
BeanUtils.copyProperties(approvalRechargeDto,microClinicPayRecord);
//设置操作人
microClinicPayRecord.setUpdateBy(accountId);
// Integer num = baseMapper.approvalRecharge(approvalRechargeDto);
//更新申请状态
updateById(microClinicPayRecord);
//修改钱包
if(approvalRechargeDto.getStatus().equals(RefundExamineStatusEnum.PASS)){
//查询该单对应的诊所Id
microClinicPayRecord = getById(microClinicPayRecord.getId());
saasMicroWalletService.recharge(microClinicPayRecord.getRechargeAmountReal(),microClinicPayRecord.getTenantId());
}
return true;
}
声明式事务基于AOP面向切面的,它将具体业务与事务处理部分解耦,代码侵入性很低,所以在实际开发中声明式事务用的比较多。
刚刚我们在使用sping声明式事务时,并没有设置事务的隔离等级,那数据库怎么知道我们到底想采用什么隔离等级呢?
我们来查看一下@Transactional注解的源码可以看到spring给我们默认配置了
我们再打开Isolation枚举的源码
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionDefinition;
/**
* Enumeration that represents transaction isolation levels for use
* with the {@link Transactional} annotation, corresponding to the
* {@link TransactionDefinition} interface.
*
* @author Colin Sampaleanu
* @author Juergen Hoeller
* @since 1.2
*/
public enum Isolation {
/**
* Use the default isolation level of the underlying datastore.
* All other levels correspond to the JDBC isolation levels.
* @see java.sql.Connection
* 默认-使用底层数据存储的默认隔离级别。所有其他级别对应于 JDBC 隔离级别。
*/
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
/**
* A constant indicating that dirty reads, non-repeatable reads and phantom reads
* can occur. This level allows a row changed by one transaction to be read by
* another transaction before any changes in that row have been committed
* (a "dirty read"). If any of the changes are rolled back, the second
* transaction will have retrieved an invalid row.
* @see java.sql.Connection#TRANSACTION_READ_UNCOMMITTED
* 读未提交-指示可能发生脏读、不可重复读和幻读的常量。此级别允许在提交该行中的任何更改之前由
* 另一个事务
* 读取由一个事务更改的行(“脏读”)。如果回滚任何更改,则第二个事务将检索到无效行。
*
*/
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
/**
* A constant indicating that dirty reads are prevented; non-repeatable reads
* and phantom reads can occur. This level only prohibits a transaction
* from reading a row with uncommitted changes in it.
* @see java.sql.Connection#TRANSACTION_READ_COMMITTED
* 读已提交-指示防止脏读的常量;可能发生不可重复读取和幻读。此级别仅禁止事务读取其中包含未提
* 交更改的行
*/
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
/**
* A constant indicating that dirty reads and non-repeatable reads are
* prevented; phantom reads can occur. This level prohibits a transaction
* from reading a row with uncommitted changes in it, and it also prohibits
* the situation where one transaction reads a row, a second transaction
* alters the row, and the first transaction rereads the row, getting
* different values the second time (a "non-repeatable read").
* @see java.sql.Connection#TRANSACTION_REPEATABLE_READ
* 可重复读-指示防止脏读和不可重复读的常量;可能发生幻读。该级别禁止一个事务读取其中未提交更
*改的行,同时也禁止一个事务读取一行,第二个事务更改该行,第一个事务重新读取该行,第二次得到
*不同值的情况( “不可重复读取”)。
*/
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
/**
* A constant indicating that dirty reads, non-repeatable reads and phantom
* reads are prevented. This level includes the prohibitions in
* {@code ISOLATION_REPEATABLE_READ} and further prohibits the situation
* where one transaction reads all rows that satisfy a {@code WHERE}
* condition, a second transaction inserts a row that satisfies that
* {@code WHERE} condition, and the first transaction rereads for the
* same condition, retrieving the additional "phantom" row in the second read.
* @see java.sql.Connection#TRANSACTION_SERIALIZABLE
* 串行化-指示防止脏读、不可重复读和幻读的常量。此级别包括 ISOLATION_REPEATABLE_READ 中的
* 禁止,并
* 进一步禁止以下情况:一个事务读取所有满足 WHERE 条件的行,第二个事务插入满足 WHERE 条件的
* 行,第一个事务重新读取相同的条件,检索附加的“幻”行在第二次读取。
*/
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
private final int value;
Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
这里面就是spring定义的隔离等级啦,虽然注释已经写的很明白,我还是来给大家总结一下吧。
spirng的隔离等级对比mysql的隔离等级多了一个默认等级,当spring选择默认隔离等级时,会直接采用数据库自己设置的隔离等级,其他四个等级与数据库一一对应,那么在这里问大家一个问题。如果数据库隔离等级与spring隔离等级设置的不同,以谁为准呢?答案是以spring的为准,spring会在创建事务时去更改数据库的隔离级别。
spring事务传播机制
那么单个方法事务从数据库到spring我们就弄通了,可是现实总是复杂凌乱的,若多个方法相互调用,有的方法被@Transactional注解修饰,有的不被修饰。那么我么的spring到底该如何处理呢?
这时候spring就在@Transactional提供了另外一个参数,propagation(翻译过来是 传播)
对于这种情况,spring提供了七种策略,源码如下--注释我已翻译在源码注释后边
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.transaction.annotation;
import org.springframework.transaction.TransactionDefinition;
/**
* Enumeration that represents transaction propagation behaviors for use
* with the {@link Transactional} annotation, corresponding to the
* {@link TransactionDefinition} interface.
*
* @author Colin Sampaleanu
* @author Juergen Hoeller
* @since 1.2
*/
public enum Propagation {
/**
* Support a current transaction, create a new one if none exists.
* Analogous to EJB transaction attribute of the same name.
* <p>This is the default setting of a transaction annotation.
* 支持当前事务,如果不存在则创建一个新事务。类似于同名的 EJB 事务属性。
* 这是事务注释的默认设置。
*/
REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),
/**
* Support a current transaction, execute non-transactionally if none exists.
* Analogous to EJB transaction attribute of the same name.
* <p>Note: For transaction managers with transaction synchronization,
* {@code SUPPORTS} is slightly different from no transaction at all,
* as it defines a transaction scope that synchronization will apply for.
* As a consequence, the same resources (JDBC Connection, Hibernate Session, etc)
* will be shared for the entire specified scope. Note that this depends on
* the actual synchronization configuration of the transaction manager.
* 支持当前事务,如果不存在则以非事务方式执行。类似于同名的 EJB 事务属性。
*注意:对于具有事务同步的事务管理器,SUPPORTS 与根本没有事务略有不同,因为它定义了同步将适
*用的事务范围。因此,相同的资源(JDBC 连接、Hibernate Session 等)将在整个指定范围内共享。
*请注意,这取决于事务管理器的实际同步配置。
*
* @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setTransactionSynchronization
*/
SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),
/**
* Support a current transaction, throw an exception if none exists.
* Analogous to EJB transaction attribute of the same name.
*/支持当前事务,如果不存在则抛出异常。类似于同名的 EJB 事务属性。
MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),
/**
* Create a new transaction, and suspend the current transaction if one exists.
* Analogous to the EJB transaction attribute of the same name.
* <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
* on all transaction managers. This in particular applies to
* {@link org.springframework.transaction.jta.JtaTransactionManager},
* which requires the {@code javax.transaction.TransactionManager} to be
* made available to it (which is server-specific in standard Java EE).
* 创建一个新事务,如果存在则暂停当前事务。类似于同名的 EJB 事务属性。
* 注意:实际的事务暂停不会在所有事务管理器上开箱即用。这尤其适用于
* org.springframework.transaction.jta.JtaTransactionManager,它要求
* javax.transaction.TransactionManager 对其可用(在标准 Java EE 中是特定于服务器的)
* @see
org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
*/
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
/**
* Execute non-transactionally, suspend the current transaction if one exists.
* Analogous to EJB transaction attribute of the same name.
* <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
* on all transaction managers. This in particular applies to
* {@link org.springframework.transaction.jta.JtaTransactionManager},
* which requires the {@code javax.transaction.TransactionManager} to be
* made available to it (which is server-specific in standard Java EE).
* org.springframework.transaction.jta.JtaTransactionManager,它要求
* javax.transaction.TransactionManager 对其可用(在标准 Java EE 中是特定于服务器的)
* @see
* org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
* 以非事务方式执行,如果存在则暂停当前事务。类似于同名的 EJB 事务属性。
* 注意:实际的事务暂停不会在所有事务管理器上开箱即用。这尤其适用于
* org.springframework.transaction.jta.JtaTransactionManager,它要求
* javax.transaction.TransactionManager 对其可用(在标准 Java EE 中是特定于服务器的)。
*/
NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),
/**
* Execute non-transactionally, throw an exception if a transaction exists.
* Analogous to EJB transaction attribute of the same name.
* 以非事务方式执行,如果存在事务则抛出异常。类似于同名的 EJB 事务属性。
*/
NEVER(TransactionDefinition.PROPAGATION_NEVER),
/**
* Execute within a nested transaction if a current transaction exists,
* behave like {@code REQUIRED} otherwise. There is no analogous feature in EJB.
* <p>Note: Actual creation of a nested transaction will only work on specific
* transaction managers. Out of the box, this only applies to the JDBC
* DataSourceTransactionManager. Some JTA providers might support nested
* transactions as well.
* @see org.springframework.jdbc.datasource.DataSourceTransactionManager
* 如果当前事务存在,则在嵌套事务中执行,否则行为类似于 REQUIRED。 EJB 中没有类似的特性。
*注意:嵌套事务的实际创建仅适用于特定的事务管理器。开箱即用,这只适用于 JDBC
* DataSourceTransactionManager。一些 JTA 提供者也可能支持嵌套事务。
*/
NESTED(TransactionDefinition.PROPAGATION_NESTED);
private final int value;
Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
如果注释还没有看明白,推荐这篇博主的帖子,讲得很详细。
事务传播https://blog.csdn.net/qq_41242680/article/details/118877807
spring事务失效
使用spring事务的时候,有很多注意的点,其中事务失效是不得不面对的问题。下面列举详细的事务失效场景。
1.数据库不支持事务(根本还是利用了数据库的事务)
Spring事务生效的前提是所连接的数据库要支持事务,如果底层的数据库都不支持事务,则Spring的事务肯定会失效。例如,如果使用的数据库为MySQL,并且选用了MyISAM存储引擎,则Spring的事务就会失效。
2.方法没有被public修饰(源码就是这样设计的)
spring要求被代理方法必须是public的。想要了解代理方法必须是public的,我们需要看spring事务的源码,在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
3.方法用final修饰(方法无法被重写)
spirng 事务是利用aop动态代理实现的,因此aop不能代理的方法自然事务不能生效。
关于第二点和第三点本子还是代理机制导致的问题,参见帖子
final修饰的方法能被代理吗https://www.csdn.net/tags/MtTaMg2sNjg1Mjk2LWJsb2cO0O0O.html
4.方法内部调用(关键是target对象的this为未被代理前的对象)--此处不懂的,自己去手敲一下代理模式的demo就明白了。
如果同一个类中的两个方法分别为A和B,方法A上没有添加事务注解,方法B上添加了 @Transactional事务注解,方法A调用方法B,则方法B的事务会失效。
非要这么做,该怎么办呢?
1.新加一个Service方法 2.在该Service类中注入自己 3. 通过AopContent类(推荐做法)
5.多线程调用(线程会使用不通的连接)
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
Spring事务不回滚
1.错误的配置事务传播特性
2.异常被捕获处理后没有继续抛出
3.rollbackFor参数配置不正确
@Transactional注解中如果不配置rollbackFor属性,那么事务只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚
5.嵌套事务回滚多了
关于事务失效和事务不回滚,可以参考这篇不错的帖子
事务失效与不回滚的原因以及处理办法https://baijiahao.baidu.com/s?id=1714667126401049636&wfr=spider&for=pc
分布式事务