数据库事务
一:事务的特性
事务的特性:ACID
事务提供了一种机制,可用来将一系列数据库更改归入一个逻辑操作。更改数据库后,所做的更改可以作为一个单元进行提交或取消。事务可确保遵循原子性、一致性、隔离性和持续性(ACID)这几种属性,以使数据能够正确地提交到数据库中。
- 原子性(Atomicity)原子性是指事务是一个不可分割的工作单位,事务中的操作 要么都发生,要么都不发生。
- 一致性(Consistency)一个事务中,事务前后数据的完整性必须保持一致。
- 隔离性(Isolation)多个事务,事务的隔离性是指多个用户并发访问数据库时, 一个用户的 事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
- 持久性(Durability)持久性是指一个事务一旦被提交,它对数据库中数据的改变 就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
二:事务的并发会产生的问题有哪些
-
脏读:
一个事务正在对数据进行更新操作,但是更新还未提交,另一个事务这时也来操作这组数据,并且读取了前一个事务还未提交的数据,而前一个事务如果操作失败进行了回滚,后一个事务读取的就是错误的数据,这样就造成了脏读 -
不可重复读:
一个事务多次读取同一个数据,在该事务还未结束时,另一个事务也对该数据进行了操作,而且在第一个事务两次读取之间,第二个事务对数据进行了更新/删除,那么第一个事务前后两个读取到的数据是不同的,这样就造成了不可重复读 -
幻读:
第一个数据正在查询某一条数据,这时,另一个事务又插入了一条符合条件的数据,第一个事务在第二次查询符合同一条件的数据时,发现多了一条前一次查询时没有的数据,仿佛幻觉一样,这就是幻读
不可重复读和幻读的区别:
-
不可重复读:重点在于update和delete
-
幻读:重点在于insert。
如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。
但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。
避免不可重复读需要锁行就行
避免幻读则需要锁表
三:事务隔离级别(MYSQL)
首先需要了解共享锁和排他锁,他们都属于悲观锁
- 共享锁(Shared Lock,也叫S锁)
用于只读操作,如 SELECT 语句;
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
产生共享锁行锁的sql:select * from ad_plan where id=1 lock in share mode; - 排他锁(Exclusive Lock,也叫X锁)
用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。
如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
产生排他锁行锁的sql: select * from ad_plan where id=1 for update;
-
第一种隔离级别:Read uncommitted(读未提交)(级别最低)
如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据,该隔离级别可以通过“排他写锁”,但是不排斥读线程实现。这样就避免了更新丢失,却可能出现脏读,也就是说事务B读取到了事务A未提交的数据
解决了更新丢失,但还是可能会出现脏读。 -
第二种隔离级别:Read committed(读提交)(Oracle默认的隔离级别)
如果是一个读事务(线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据,该隔离级别避免了脏读,但是可能出现不可重复读。事务A事先读取了数据,事务B紧接着更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
解决了更新丢失和脏读问题 -
第三种隔离级别:Repeatable read(可重复读取)(MYSQL默认的隔离级别)
可重复读取是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写),这样就可以在同一个事务内两次读到的数据是一样的,因此称为是可重复读隔离级别,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务(包括了读写),这样避免了不可重复读和脏读,但是有时可能会出现幻读。(读取数据的事务)可以通过“共享读镜”和“排他写锁”实现。
解决了更新丢失、脏读、不可重复读、但是还会出现幻读
可重复读实现原理:https://zhuanlan.zhihu.com/p/166152616 -
第四种隔离级别:Serializable(可序化)(级别最高)
提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行,如果仅仅通过“行级锁”是无法实现序列化的,必须通过其他机制保证新插入的数据不会被执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也是最高的,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读
解决了更新丢失、脏读、不可重复读、幻读(虚读)
以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低,像Serializeble这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况来,在MYSQL数据库中默认的隔离级别是Repeatable read(可重复读)。
- MYSQL:支持上面四种隔离级别,默认的为Repeatable read(可重复读);
- Oracle:只支持Serializeble(串行化),Read committed(读已提交),默认的为Read committed级别
在MYSQL数据库中查看当前事务的隔离级别
SELECT @@tx_isolation;
在MYSQL数据库中设置事务的隔离级别:
//session:当前会话;global:全局
//设置read uncommitted级别:
set session transaction isolation level read uncommitted;//当前会话
set global transaction isolation level read uncommitted;//全局,下次打开还是
//设置read committed级别:
set session transaction isolation level read committed;
//设置repeatable read级别:
set session transaction isolation level repeatable read;
//设置serializable级别:
set session transaction isolation level serializable;
//或者
set tx_isolation='read-uncommitted';
...
记住:设置数据库的隔离级别一定要是在开启事务之前:
如果是使用JDBC对数据库的事务设置隔离级别的话,也应该是在调用Connecton对象的setAutoCommit(false)方法之前,调用Connection对象的setTransactionIsolation(level)即可设置当前连接的隔离级别,至于参数level,可以使用Connection对象的字段。
在JDBC中设置隔离级别的部分代码:
public class TestQuarantine {
public void testQuarantine(){
String sql = "UPDATE student SET student_number = ? WHERE student_name = ?";
Connection con = null;
PreparedStatement ps = null;
try {
// 1. 获取数据的Connection连接
con = JDBCUtils.getConnection("database.properties");
ps = con.prepareStatement(sql);
// 2. 事务采用读未提交的级别,即最低级别
con.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
// 3. 设置Connection的提交方式为不自动提交
con.setAutoCommit(false);
selectList(con);//查询
...
} catch (Exception e) {
try {
con.rollback();//执行了回滚操作
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
} finally {
JDBCUtils.close(con, null, ps);
}
}
}
隔离级别的设置只对当前连接有效,对于使用MYSQL命令窗口而言,一个窗口就相当于一个连接,当前窗口设置的隔离级别只对当前窗口中的事务有效,对于JDBC操作数据库来说,一个Connection对象相当与一个连接,而对于Connection对象设置的隔离级别只对该Connection对象有效,与其他连接Connection对象无关
四:Spring中事务的传播行为
事务行为 | 说明 |
---|---|
PROPAGATION_REQUIRED | 支持当前事务,假设当前没有事务。就新建一个事务 |
PROPAGATION_SUPPORTS | 支持当前事务,假设当前没有事务,就以非事务方式运行 |
PROPAGATION_MANDATORY | 支持当前事务,假设当前没有事务,就抛出异常 |
PROPAGATION_REQUIRES_NEW | 新建事务,假设当前存在事务。把当前事务挂起 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式运行操作。假设当前存在事务,就把当前事务挂起 |
PROPAGATION_NEVER | 以非事务方式运行,假设当前存在事务,则抛出异常 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
举例说明:
ServiceA {
void methodA() {
ServiceB.methodB();
}
}
ServiceB {
void methodB() {
}
}
-
PROPAGATION_REQUIRED
假如当前正要运行的事务不在另外一个事务里,那么就起一个新的事务 比方说,ServiceB.methodB的事务级别定义PROPAGATION_REQUIRED, 那么因为执行ServiceA.methodA的时候,ServiceA.methodA已经起了事务。这时调用ServiceB.methodB,ServiceB.methodB看到自己已经执行在ServiceA.methodA的事务内部。就不再起新的事务。而假如ServiceA.methodA执行的时候发现自己没有在事务中,他就会为自己分配一个事务。这样,在ServiceA.methodA或者在ServiceB.methodB内的不论什么地方出现异常。事务都会被回滚。即使ServiceB.methodB的事务已经被提交,可是ServiceA.methodA在接下来fail要回滚,ServiceB.methodB也要回滚
-
PROPAGATION_SUPPORTS
假设当前在事务中。即以事务的形式执行。假设当前不在一个事务中,那么就以非事务的形式执行 -
PROPAGATION_MANDATORY
必须在一个事务中执行。也就是说,他仅仅能被一个父事务调用。否则,他就要抛出异常 -
PROPAGATION_REQUIRES_NEW
这个就比较绕口了。 比方我们设计ServiceA.methodA的事务级别为PROPAGATION_REQUIRED,ServiceB.methodB的事务级别为PROPAGATION_REQUIRES_NEW。那么当运行到ServiceB.methodB的时候,ServiceA.methodA所在的事务就会挂起。ServiceB.methodB会起一个新的事务。等待ServiceB.methodB的事务完毕以后,他才继续运行。
他与PROPAGATION_REQUIRED 的事务差别在于事务的回滚程度了。由于ServiceB.methodB是新起一个事务,那么就是存在两个不同的事务。假设ServiceB.methodB已经提交,那么ServiceA.methodA失败回滚。ServiceB.methodB是不会回滚的。假设ServiceB.methodB失败回滚,假设他抛出的异常被ServiceA.methodA捕获,ServiceA.methodA事务仍然可能提交。
-
PROPAGATION_NOT_SUPPORTED
当前不支持事务。比方ServiceA.methodA的事务级别是PROPAGATION_REQUIRED 。而ServiceB.methodB的事务级别是PROPAGATION_NOT_SUPPORTED ,那么当执行到ServiceB.methodB时。ServiceA.methodA的事务挂起。而他以非事务的状态执行完,再继续ServiceA.methodA的事务。 -
PROPAGATION_NEVER
不能在事务中执行。
如果ServiceA.methodA的事务级别是PROPAGATION_REQUIRED。 而ServiceB.methodB的事务级别是PROPAGATION_NEVER ,那么ServiceB.methodB就要抛出异常了。 -
PROPAGATION_NESTED
如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
Spring中事务的配置
Spring自带事物管理器,只需要配置一下,方法进行规范命名即可。有配置文件的方法,注解的方法。
方法一: 配置文件+aop:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
<!-- 事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 传播行为 -->
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="insert*" propagation="REQUIRED"/>
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="create*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="select*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
<!-- AOP切面 -->
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.teacher.service.*.*(..))"/>
</aop:config>
</beans>
如果是springboot项目
- 在resources文件夹下创建xml文件。例如:transaction.xml,内容同上。
注意:dataSource是直接拿来用的,所以你在加载DataSource对象时候必须命名为dataSource。 - 在启动类上添加@ImportResource(“classpath:transaction.xml”)
或者配置类的方法:
@Configuration
public class TxAnoConfig {
@Autowired
private DataSource dataSource;
@Bean("txManager")
public DataSourceTransactionManager txManager() {
return new DataSourceTransactionManager(dataSource);
}
/*事务拦截器*/
@Bean("txAdvice")
public TransactionInterceptor txAdvice(DataSourceTransactionManager txManager) {
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
/*只读事务,不做更新操作*/
RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
readOnlyTx.setReadOnly(true);
readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
/*当前存在事务就使用当前事务,当前不存在事务就创建一个新的事务*/
//RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
//requiredTx.setRollbackRules(
// Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
//requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute(TransactionDefinition.PROPAGATION_REQUIRED,
Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
requiredTx.setTimeout(5);
Map<String, TransactionAttribute> txMap = new HashMap<>();
txMap.put("add*", requiredTx);
txMap.put("save*", requiredTx);
txMap.put("insert*", requiredTx);
txMap.put("update*", requiredTx);
txMap.put("delete*", requiredTx);
txMap.put("get*", readOnlyTx);
txMap.put("query*", readOnlyTx);
source.setNameMap(txMap);
return new TransactionInterceptor(txManager, source);
}
/**
* 切面拦截规则 参数会自动从容器中注入
*/
@Bean
public DefaultPointcutAdvisor defaultPointcutAdvisor(TransactionInterceptor txAdvice) {
DefaultPointcutAdvisor pointcutAdvisor = new DefaultPointcutAdvisor();
pointcutAdvisor.setAdvice(txAdvice);
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution (* com.test.service.*.*(..))");
pointcutAdvisor.setPointcut(pointcut);
return pointcutAdvisor;
}
}
方法二:注解的方式
<?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: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-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!--开启注解的方式-->
<tx:annotation-driven transaction-manager="txManager"/>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
//如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
@Transactional(propagation=Propagation.REQUIRED)
//容器不为这个方法开启事务
@Transactional(propagation=Propagation.NOT_SUPPORTED)
//不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
@Transactional(propagation=Propagation.REQUIRES_NEW)
//必须在一个已有的事务中执行,否则抛出异常
@Transactional(propagation=Propagation.MANDATORY)
//必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.NEVER)
//如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
@Transactional(propagation=Propagation.SUPPORTS)
在类或者方法上加上@Transactional,即可加入spring的事务管理,两者可以同时加,但方法上的事务优于类的事务。
- @Transactional的属性
@Transactional的属性与基于XML配置的<tx:method>包含的属性差不多,如下两个属性新增:
- rollbackForClassName:需要回滚的类名
- noRollbackForClassName:不需要回滚的类名集
默认属性: - Propagation setting is PROPAGATION_REQUIRED.
- Isolation level is ISOLATION_DEFAULT.
- Transaction is read/write.
- Transaction timeout defaults to the default timeout of the underlying transaction system, or to none if timeouts are not supported.
- Any RuntimeException triggers rollback, and any checked Exception does not.
- <tx:annotation-driven/>属性
- transaction-manager:事务管理器名字;如果事务管理器bean的
名字默认为“transactionManager”,则此属性默认就会关联;
如果名字不是“transactionManager”,则需要指定。 - mode:代理模式,默认“proxy”,直接使用spring aop方式处理;
也可以选择“aspectj”,则用aspectj方式处理,具体参考文档。
(基于XML的配置不可以选择。) - proxy-target-class:默认false,基于JDK动态代理,只适用于面向接口编程;当有些特殊情况不是面向接口编程时,可以使用CGLIB等基于继承的代理方式值为true。基于XML方式的配置也可以指定此值,在<aop:config>有此属性。
- order:当有多个advise存在时,可以通过此属性指定执行顺序。
基于XML方式的配置可以参考文档。
五:spring声明式事务回滚机制
在Spring 的事务框架中推荐的事务回滚方法是,在当前执行的事务上下文中抛出一个异常。如果异常未被处理,当抛出异常调用堆栈的时候,Spring 的事务框架代码将捕获任何未处理的异常,然后并决定是否将此事务标记为回滚。
- 在默认配置中,Spring 的事务框架代码只会将出现unchecked异常的事务标记为回滚;
也就是说事务中抛出的异常是RuntimeException或者是其子类,这样事务才会回滚(默认情况下Error也会导致事务回滚)。 - 在默认配置的情况下,所有的 checked 异常都不会引起事务回滚。如果有需要,可以通过rollback-for 和no-rollback-for进行指定。
Java的异常层次结构图:
将派生于Error或者RuntimeException的异常称为unchecked异常,所有其他的异常成为checked异常。
六:分布式事务处理
-
两阶段提交方案/XA方案:
事务管理器(负责协调多个数据库的事务)先问各个数据库是否都准备好了,如果“是”就正式提交事务;如果任何其中一个数据库“否”,那么就回滚事务。
比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。技术:Spring + JTA。
三阶段是前面加个验证处理,验证能够处理再进行后续操作。 -
TCC 方案(Try、Confirm、Cancel):
Try 阶段:对各个服务的资源做检测以及对资源进行锁定或者预留。
Confirm 阶段:在各个服务中执行实际的操作。
Cancel 阶段:如果任何一个服务的业务方法执行出错,把那些执行成功的服务回滚。
事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大。
很少用,一般来说跟钱相关的,跟钱打交道的,支付、交易相关的场景,会用 TCC。 -
本地消息表:
严重依赖于数据库的消息表来管理事务 -
可靠消息最终一致性方案:
就是干脆不要用本地的消息表了,直接基于 MQ 来实现事务。 -
最大努力通知方案