spring 事务

spring注解

Java事务导引

什么是事务?

事务是逻辑上的一组操作(或动作),使得数据库从一种状态转换成另外一种状态,且保证操作要么都执行,要么都不不执行。

在 MySql 中想要支持事务就必须使用 innodb 存储引擎。它是事务性数据库的首选引擎,给 MySql 提供了具有 “事务、回滚和崩溃修复能力” ,并支持 ACID 事务、行级锁定。在 InnoDb 表中,所有的语句都是需要 commit 后,才会在真实数据库中生效。

数据库中操作一个事务是怎么完成的?

首先我们启用事务日志,这样更完整表述,大概是这么个流程:
1)客户端编写SQL语句执行,数据包完成,发送到服务器;
2)服务器端解析数据包,生成SQL语句,发送到数据库关系引擎(innodb)
3)分析编译SQL,预申请物理内存,可能将部分数据先取出放到内存中;
4)事务中的SQL按顺序执行,开始记录事务序列号;
5)对涉及的逻辑表、数据页、行加相关锁,对访问的内存页加锁;
6)执行SQL,操作的数据若没在内存,继续从磁盘中读取数据到内存;
7)首先在内存中修改数据,修改后,内存中的日志记录下来;
8)事务中若还有SQL,继续执行,重复步骤5、6、7
9)提交(commit)事务,内存中的日志记录写入磁盘中的日志文件;
10)释放所有锁,释放对这部分内存数据的访问;允许其他进程释放这些内存数据;
11)此时事务结束,发送消息给客户端;

事务的原则性内容

事务必须服从ISO/IEC所指定的ACID原则,ACID的具体内涵如下:

  • 原子性(Atomicity): 即不可分割性,事务要么全部被执行,要么就全部不被执行。
  • 一致性(Consistency): 事务的执行使得数据库从一种正确状态转换成另一种正确状态
  • 隔离性(ISolation): 在事务正确提交之前,它可能的结果不应显示给如何其他事务
  • 持久性(Durability): 事务正确提交后,其结果将永久保存在数据库中

疑问?
数据库 事务在提交之前数据是存在哪里的?
InnoDB
MySQL探秘(三):InnoDB的内存结构和特性

事务与Java的关系?

java事务机制和原理就是确保数据库操作的ACID特性

  • Java事务的产生
    • 程序操作数据库的需要,在Java编写的程序或系统中,实现ACID的操作。
  • Java事务实现范围
    • 通过 JDBC 相应方法间接实现对数据库的增、删、改、查,把事务转移到Java程序代码中进行控制;
    • 确保事务要么全部执行成功,要么撤销不执行。

事务的实现方式

  • 事务类型
    • JDBC事务:用 Connection 对象控制,包括手动模式和自动模式;
    • JTA(Java Transation API) 事务:与实现无关的,与协议无关的API;
    • 容器事务:应用服务器提供的,且大多是基于JTA完成(通常基于JNDI的,相当复杂的API实现),由各个中间件厂商提供。
  • 三种事务的差异
    • JDBC事务:控制的局限性在一个数据库连接内,但是其使用简单。
    • JTA事务:功能强大,可跨越多个数据库在多DAO,使用比较复杂。
    • 容器事务:主要指的是J2EE应用服务器提供的事务管理,局限于EJB

事务接口架构

transactioninterface.png

spring 事务属性

事务属性范围

  • 传播行为
  • 隔离规则
  • 回滚规则
  • 事务超时
  • 是否只读

事务属性定义

public interface TransactionDefinition { /** * 返回事务的传播行为 */ int getPropagationBehavior(); /** * 返回事务的隔离级别,事务管理器根据它来控制 * 另外一个事务可以看到本事务内的哪些数据 * @return */ int getIsolationLevel(); /** * 返回事务必须在多少秒内完成 * @return */ int getTimeout(); /** * 判断事务是否只读,事务管理器能够根据这个 * 返回值进行优化,确保事务是只读的 * @return */ boolean isReadOnly(); }

数据读取类型说明

  • 脏读:(针对未提交数据)如果一个事务中对数据进行了更新,但事务还没提交,另一个事务可以 “看到” 该事务没有提交的更新结果,这样造成的问题就是如果第一个事务回滚,那么第二个事务在此之前所"看到"的数据就是一笔脏数据。
  • 不可重复读:(针对其他提交前后,读取数据本身的对比)不可重复读取是指同一个事务在整个事务过程中对同一笔数据进行读取,每次读取结果都不同。如果事务1在事务2的更新操作之前读取一次数据,在事务2的更新操作之后在读取同一笔数据一次,两次结果是不同的,所以Read Uncommitted 也无法避免不可重复读取的问题。
  • 幻读:(针对其他提交前后,读取数据条数的对比) 幻读是指同样一笔查询在整个事务过程中多次执行后,查询所得的结果集是不一样的。幻读针对的是多笔记录。在Read Uncommitted隔离级别下, 不管事务2的插入操作是否提交,事务1在插入操作之前和之后执行相同的查询,取得的结果集是不同的,所以,Read Uncommitted同样无法避免幻读的问题。

脏读、幻读和不可重复读?为啥?
脏读、不可重复读和幻读的区别

脏读

时刻 事务A 事务B 说明
TO X=1 - 初始数据
T1 X=2,但是未提交 - -
T2 - 读入X=2 这个时候事务 B 读取了事务 A 未提交的数据,而后面事务 A,可能回滚,引发严重数据库数据一致性问题
T3 回滚 - 此时事务A回滚,导致事务 B 完全在一个错误的数据下运行
T4 - 处理业务逻辑 采用错误的 X=2 处理
T5 - 提交事务 此时完全在一个错误的数据下完成提交

为了避免这个问题,往往使用【读写提交】的隔离级别

时刻 事务A 事务B 说明
TO X=1 - 初始数据
T1 X=2,但是未提交 - -
T2 - 读入 X=1 这个时候事务B不能读入事务A未提交的数据,所以只能读到X=1
T3 回滚 - -
T4 - 处理业务逻辑 采用正确的 X=1 处理逻辑
T5 - 提交事务 提交正确

这里我们用了【读写提交】完成了这些逻辑,但是读写提交依旧会产生一些问题,让我们看看这样的场景

时刻 事务A 事务B 说明
T1 老公查询账户余额1000源 - -
T2 - 老婆购物花费 800 元 -
T3 - 老婆提交事务 -
T4 老公请客吃饭,买单 500元,被告知余额不足 - 没钱买单

幻读

时刻 事务A 事务B 说明
T1 老婆查询当月银行账户支出各条数据,10条共计1000元 - 初始状态
T2 - 老公消费800元 老公此时消费
T3 - 老公提交消费事务 事务被提交
T4 老婆打印账单 11 条,共计 1800元 - 前后差异,老婆会以为 800元是幻读

我们看到老婆在查询之后,老公启动了消费,并先于老婆之前打印账单记录,所以在 T4 时刻,打印了 1800 元 11 条记录,这个时候老婆就会去质疑这 800 元是不是幻读的,上面和不可重复读很接近,但是我们需要注意的是,不可重复 读是针对同一条记录,而幻读是针对删除和插入记录的

为了避免这个问题我们可以采用序列化的隔离层,序列化就意味着所有的操作都会按顺序执行,不会出现【脏读、不可重读、幻读】的情况

四种隔离级别的比较

项目 脏读 不可重读 幻读
脏读
读写提交 ×
可重复读 × ×
序列化 × × ×

这就是数据库隔离层的情况,上面只讨论了在多并发环境下数据安全性的问题,没涉及到它们之间的性能问题,一般而言,性能从 脏读->读写分离->可重复读->序列化 是直线下降的,更多的时候我们使用读写提交边可以了,也不是所有的数据库支持所有的隔离级别,比如 Oracle 数据库只支持读写提交和序列化,它的默认隔离级别为读写提交,而 MySQL 数据库的默认隔离级别为可重复读。

如何测试MySQL的事务?

首先我们打开2个数据库连接客户端,具体步骤如下:
1)通过执行 select @@autocommit 命令可以查看到它的默认值是 1(自动提交事务);
2)在两个客户端分别执行: set @@autocommit = 0 改为手动提交;
3)在第一个客户端执行 select * from xxxxx where id=1 for update 语句,可以查询出来;
4)在第二个客户端执行 select * from xxxxx where id=1 for update 语句,这时你会发现查询不出来,出现 “SELECT * from distribute_lock WHERE business_code=“demon” FOR UPDATE” 提示,只有在第一个窗口执行了 commit 命令才能查询出数据;

事务传播行为

查看

spring 事务状态

通过事务管理器获取 TransactionStatus实例

/** * Spring 事务状态接口 * 通过调用 PlatformTransactionManger 的 getTransaction() * 获取事务状态实例 */ public interface TransactionStatus { /** * 是否是新的事务 * @return */ boolean isNewTransaction(); /** * 是否有恢复点 * @return */ boolean hasSavepoint(); /** * 设置为只回滚 */ void setRollbackOnly(); /** * 是否为只回滚 */ boolean isRollbackOnly(); /** * 是否已完成 */ boolean isCompleted(); }

扩展阅读

可能是最漂亮的Spring事务管理详解