概要
过度
我们前面介绍了Spring的基础知识后,介绍了Spring对一些常用框架api的封装:
- jdbc
- mybatis
这里主要介绍了对持久化层中的sql语句的封装,使我们避免直接手写sql,接下来介绍一下Spring对sql的事务的封装,这能让我们避免使用事务时的复杂设置操作。
内容简介
本文分两部分。
第一部分主要介绍事务的基础知识,包括:
- 是什么(事务的定义、特点)
- 为什么(现实问题)
- 怎么做(api调用)
emmmmm,是不是有点粗糙,有点像是初中政治凑字数。但是我觉得把这三点大概熟悉之后,我们在工作中日常使用事务应该问题不大了。至于深入,等后面有时间再说吧。
第二部分主要介绍事务api直接使用时的一些不方便的地方,并引出Spring对事务api的封装。
后面会在本文中提出的Spring对事务api封装的基础上进行spring-tx组件的介绍。
所属环节
Spring-tx 组件引入
上下环节
上文: Spring 框架基本实现介绍、Spring基本的持久化框架介绍
下文: Spring-tx框架详细介绍
事务基础知识
事务介绍及基本特点
上面是拷贝自百度知道的一句话,我觉得其中 执行单元 这个用词比较准确。执行单元有以下特点:要么成功要么失败,不存在执行到一半的情况。
事务有以下特点:
- 原子性(Atomic)
- 一致性(Consistency)
- 隔离性(Isolation)
- 持久性(Durability)
对各个性质简要记录如下:
原子性
和最上面拷贝的一句话差不多,事务是一个执行单元,要么成功要么失败,不存在成功一半的可能
一致性
事务中的修改状态的操作对外部的可见是语义上有意义的状态,符合数据库和业务的相关约束
一致性和原子性的区别在于:原子性强调的是事务执行的不可分割性,要么成功要不失败,不存在执行一半的情况;一致性强调的是状态的对外暴露,及数据的可见性,只有最初和最终的状态是可见的
隔离性
并发执行的事务会排除彼此的影响。
对于隔离性,数据库提供了多种隔离级别:
Read Uncommitted (读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
Read Committed (读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
Repeatable Read (可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
Serializable (可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。
例如:
脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据【一般是修改】。
幻读(Phantom Read):在一个事务的两次查询中数据不一致,幻读是重点在插入和删除,不可重复读重点在修改
隔离级别 脏读 不可重复读 幻读 读未提交 是 是 是 读提交 否 是 是 可重复读 否 否 是 可串行化 否 否 否
一般情况下,MySQL的事务隔离级别是可重复读,也就是说会锁行,一个事务中两次读到的同一行数据是不会变化【被修改的】,但是两次读取可能会多出来或者少新的数据行。
持久性
事务在执行完成提交后,造成的改动会被持久化保存。
使用事务的原因
默认情况下,数据库执行一条SQL语句就是一个原子的操作,但是很多时候业务逻辑要求我们将几个操作绑定在一起,比如最经典的“转账案例”:A账户扣减100元和B账户打入100元的操作是不可分割的。对于这种单个流程操作的整合,我们需要使用事务。
当然,数据库事务和分布式事务的使用场景还是不太一样的:
数据库事务是用来锁一台机器的,也就是说事务是保证这一台机器的操作具有不可分割、读取的每一个状态都是一致的这种效果。【使用默认状态】【具体是锁的机器还是和线程对应的
connector
,这个没弄特别透彻】在多个机器并发的情况下很容易出问题,所以我们在工作中一般使用的是分布式锁,保证关键事务能锁住整个应用,同时其他的事务能正常执行。
事务API
我们先看一个普通的jdbc调用:
public static void main(String args[]) throws ClassNotFoundException, SQLException {
// 在 com.mysql.cj.jdbc.Driver 类的静态代码块中进行了DriverManager的注册。
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:", "", "");
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery("select * from form");
while (resultSet.next()) {
String x = resultSet.getString(1);
System.out.println("x=" + x);
}
}
基本就是获得connection
,得到statement
,执行,关闭几个步骤。
MySQL默认是将执行的sql丢进去,他会自行执行并提交,一个SQL就是一个事务,所以如果我们想自己控制事务提交就要做如下改动:
- 禁止
Connection
自动提交事务 - 如果有可回滚的小步骤,自行设置保存点,并在执行后根据情况决定是否回滚至保存点
- 在执行完整体的事务之后,看情况回滚还是提交
- 执行结束后关闭或者释放
Connection
所以使用事务API的demo如下:
public static void main(String args[]) throws ClassNotFoundException, SQLException {
// 在 com.mysql.cj.jdbc.Driver 类的静态代码块中进行了DriverManager的注册。
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://", "", "");
// 设置事务
connection.setAutoCommit(false);
PreparedStatement statement = connection.prepareStatement(sql_a);
statement.setString(1,"lpc create 2");
statement.setInt(2,1);
statement.setString(3,"lpc modify2");
statement.setLong(4,123L);
statement.setString(5,"form name2");
statement.setLong(6,123L);
statement.setString(7,"aaaaa2");
statement.execute();
Savepoint savepoint = connection.setSavepoint();
PreparedStatement statement1 = connection.prepareStatement(sql_a);
statement1.setString(1,"lpc create 1");
statement1.setInt(2,1);
statement1.setString(3,"lpc modify 1");
statement1.setLong(4,123L);
statement1.setString(5,"form name 1");
statement1.setLong(6,123L);
statement1.setString(7,"aaaaa 1");
statement1.execute();
// 回滚至保存点【保存点之后的修改作废】
connection.rollback(savepoint);
// 整体事务执行完成,提交【当然这里也可以回滚】
connection.commit();
// 关闭
statement.close();
connection.close();
}
事务API的使用规律及不足
不足
这个和前面的问题相似,就是存在大量的废代码,比如:
- 加载jdbc着一系列模版代码,还有
Connection
和Statement
的创建关闭啥的一大堆东西 - 事务的管理和
Connection
相关,如果有多层子函数调用,可能要到处传Connection
- 安全点的相关创建管理也特变容易乱
规律
首先:
- jdbc的相关东西我们在前面都实现了相关的操作
所以我们专门关注事务的就行了,我们很容易发现
- 安全点的创建和回滚是对应的,从回滚往上找到最近的一个安全点即可
- 事务的提交/回滚和事务创建也是对应的,从提交/回滚往上找最近的也就可以了
这个操作过程是不是特别像栈。我们后面是否可以考虑用AOP对原有业务做一个较小侵入式的事务功能支持!
Spring 对事务的封装
Spring 实现的demo
Spring 配置的xml:
<!--增加对事务的声明式支持-->
<tx:annotation-driven/>
<!--注册事务-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
代码:
在需要使用事务的地方【接口、方法、具体类】打上注解@Transactional(propagation = Propagation.XXX)
其中:
Propagation.REQUIRED
:如果有事务就复用,没有就新建
Propagation.SUPPORTS
:如果有事务就复用,没有就不在事务中跑
Propagation.MANDATORY
:必须在事务中跑,没有就抛出异常
Propagation.REQUIRES_NEW
:必须在新事务中跑,如果当前有事务就先阻塞掉当前事务
Propagation.NOT_SUPPORTED
:必须不在事务中跑,如果当前有事务就先阻塞掉当前事务
Propagation.NEVER
:必须不在事务中跑,如果当前有事务就抛出异常
Propagation.NESTED
:如果当前有事务,就复用,如果没有就抛出异常
这些感觉不用记,有需要看api的注释就行,主要是后面过代码时用。
猜测实现思路
我们前面已经了解了Spring框架的基本工作原理,大概熟悉了他的主要功能,根据使用方法我们可以大概反推一下Spring-tx的实现过程:
- 使用了
<tx:annotation-driven/>
标签,和前面的MyBatis思路相似,应该是注册一个BeanFactory
的后处理器,它在完成注册会被ApplicationContext
调用,新创建一个实现了Advisor
接口的含有事务逻辑的增强器。 - 在新创建的含有事务逻辑的增强器中会依赖我们注册的
transactionManager
,并在执行到增强器时进行注解内容的读取,并根据注解配置执行指定的事务操作。
因为我们的AOP切面恰好和方法调用堆栈一样,正好契合了事务的安全点、阻塞+恢复、回滚、提交,所以上面这个思路是可行的。
扩展
问题遗留
参考文献
事务特性:https://www.cnblogs.com/dooor/p/5303904.html
幻想读和不可重读读读区别:https://www.cnblogs.com/xiaohanlin/p/8644749.html