8-基于Spring的框架-事务——8-1 事务引入

概要

过度

我们前面介绍了Spring的基础知识后,介绍了Spring对一些常用框架api的封装:

  • jdbc
  • mybatis

这里主要介绍了对持久化层中的sql语句的封装,使我们避免直接手写sql,接下来介绍一下Spring对sql的事务的封装,这能让我们避免使用事务时的复杂设置操作。

内容简介

本文分两部分。

第一部分主要介绍事务的基础知识,包括:

  • 是什么(事务的定义、特点)
  • 为什么(现实问题)
  • 怎么做(api调用)

emmmmm,是不是有点粗糙,有点像是初中政治凑字数。但是我觉得把这三点大概熟悉之后,我们在工作中日常使用事务应该问题不大了。至于深入,等后面有时间再说吧。

第二部分主要介绍事务api直接使用时的一些不方便的地方,并引出Spring对事务api的封装。

后面会在本文中提出的Spring对事务api封装的基础上进行spring-tx组件的介绍。

所属环节

Spring-tx 组件引入

上下环节

上文: Spring 框架基本实现介绍、Spring基本的持久化框架介绍

下文: Spring-tx框架详细介绍

事务基础知识

事务介绍及基本特点

事务在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)

上面是拷贝自百度知道的一句话,我觉得其中 执行单元 这个用词比较准确。执行单元有以下特点:要么成功要么失败,不存在执行到一半的情况。

事务有以下特点:

  • 原子性(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就是一个事务,所以如果我们想自己控制事务提交就要做如下改动:

  1. 禁止Connection自动提交事务
  2. 如果有可回滚的小步骤,自行设置保存点,并在执行后根据情况决定是否回滚至保存点
  3. 在执行完整体的事务之后,看情况回滚还是提交
  4. 执行结束后关闭或者释放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的使用规律及不足

不足

这个和前面的问题相似,就是存在大量的废代码,比如:

  1. 加载jdbc着一系列模版代码,还有ConnectionStatement的创建关闭啥的一大堆东西
  2. 事务的管理和Connection相关,如果有多层子函数调用,可能要到处传Connection
  3. 安全点的相关创建管理也特变容易乱

规律

首先:

  1. jdbc的相关东西我们在前面都实现了相关的操作

所以我们专门关注事务的就行了,我们很容易发现

  1. 安全点的创建和回滚是对应的,从回滚往上找到最近的一个安全点即可
  2. 事务的提交/回滚和事务创建也是对应的,从提交/回滚往上找最近的也就可以了

这个操作过程是不是特别像栈。我们后面是否可以考虑用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的实现过程:

  1. 使用了<tx:annotation-driven/>标签,和前面的MyBatis思路相似,应该是注册一个BeanFactory的后处理器,它在完成注册会被ApplicationContext调用,新创建一个实现了Advisor接口的含有事务逻辑的增强器。
  2. 在新创建的含有事务逻辑的增强器中会依赖我们注册的transactionManager,并在执行到增强器时进行注解内容的读取,并根据注解配置执行指定的事务操作。

因为我们的AOP切面恰好和方法调用堆栈一样,正好契合了事务的安全点、阻塞+恢复、回滚、提交,所以上面这个思路是可行的。

扩展

问题遗留

参考文献

事务特性:https://www.cnblogs.com/dooor/p/5303904.html

幻想读和不可重读读读区别:https://www.cnblogs.com/xiaohanlin/p/8644749.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,417评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,921评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,850评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,945评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,069评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,188评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,239评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,994评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,409评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,735评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,898评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,578评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,205评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,916评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,156评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,722评论 2 363
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,781评论 2 351

推荐阅读更多精彩内容

  • 不足的地方请大家多多指正,如有其它没有想到的常问面试题请大家多多评论,一起成长,感谢!~ String可以被继承吗...
    启示录是真的阅读 2,929评论 3 3
  • 事务的嵌套概念 所谓事务的嵌套就是两个事务方法之间相互调用。spring事务开启 ,或者是基于接口的或者是基于类的...
    jackcooper阅读 1,415评论 0 10
  • 摘要 本文摘抄了Spring事务相关的一些理论,主要讲述事务的特性、事务的传播行为、事务的隔离规则。 关键词:事务...
    fad2aa506f5e阅读 229评论 0 0
  • Spring 事务传播特性和隔离级别 事务是处理逻辑原子性的保证,作为单个逻辑单元执行一系列操作,要么执行完成要么...
    自负的鱼阅读 8,949评论 1 33
  • 昨天有区块链内容平台的朋友问我,你们以前在简书写作,根本没有钻贝奖励,你为什么还写呢? 他的问题,问得我一愣。 是...
    千漫千寻阅读 980评论 14 34