Spring事物隔离

一、Spring的事务支持(注解事务、声明事务、编程事务、事务的传播机制)

spring支持编程式事务,和声明式事务。编程式事务就是用个事务类TransactionTemplate来管理事务,这个一般现在没人傻到干这个事儿了;声明式事务分成在xml里配置个AOP来声明个切面加事务,一般现在也没人傻到干这个了;大部分情况下,都是用@Transactional注解

注意:

这个@Transactional注解呢,根据阿里编码规范,一般建议加在方法级别,就是要事务的方法就加事务,不要事务的方法就别加事务,否则很多一个数据库操作的方法,你还加事务,你这不是。。。?可能有同学注意到我们项目阶段一,都是傻乎乎的在类上加了事务对吧?对,其实不是我傻,是我装傻,我当时为了快速开发,所以都给扔类上了,类里所有方法都开启了事务,但是后续我们会慢慢优化这些细节的。

 

另外这个注解一般要加rollbackFor,就是指定哪些异常类型才要回滚事务。

 

在项目中,@Transactional(rollbackFor=Exception.class),如果类加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。

 

在@Transactional注解中如果不配置rollbackFor属性,那么事物只会在遇到RuntimeException的时候才会回滚,加上rollbackFor=Exception.class,可以让事物在遇到非运行时异常时也回滚

还有比较重要的,就是有个isolation属性,你可以自己手动调整事务的隔离级别,但是这个一般不调整,记住,别乱调整事务隔离级别,一般可重复读+mysql mvcc机制跑的很好,你别瞎折腾。

 

另外一个重要的事务属性,就是propagation,事务的传播行为,我们就重点先来聊一下事务的传播行为,这个在项目里可能确实是要用到的。其实说白了,这个事务的传播机制,就是说,一个加了@Transactional的事务方法,和嵌套了另外一个@Transactional的事务方法的时候,包括再次嵌入@Transactional事务方法的时候,这个事务怎么玩儿?

二、事物的基本特性(ACID)

1Atomic:原子性,就是一堆SQL,要么一起成功,要么都别执行,不允许某个SQL成功了,某个SQL失败了,这就是扯淡,不是原子性。

2Consistency:一致性,这个是针对数据一致性来说的,就是一组SQL执行之前,数据必须是准确的,执行之后,数据也必须是准确的。别搞了半天,执行完了SQL,结果SQL对应的数据修改没给你执行,那不是坑爹么。

3Isolation:隔离性,这个就是说多个事务在跑的时候不能互相干扰,别事务A操作个数据,弄到一半儿还没弄好呢,结果事务B来改了这个数据,导致事务A的操作出错了,那不就搞笑了。

4Durability:持久性,事务成功了,就必须永久对数据的修改是有效的,别过了一会儿数据自己没了,不见了,那就好玩儿。

三、事物基本原理

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:

  1. 获取连接 Connection con = DriverManager.getConnection()
  2. 开启事务con.setAutoCommit(true/false);
  3. 执行CRUD
  4. 提交事务/回滚事务 con.commit() / con.rollback();
  5. 关闭连接 conn.close();

使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。
那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子

  1. 配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
  2. spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
  3. 真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。

四、事物的隔离级别

1)读未提交,Read Uncommitted:这个很坑爹,就是说某个事务还没提交的时候,修改的数据,就让别的事务给读到了,这就恶心了,很容易导致出错的。这个也叫做脏读。

2)读已提交,Read Committed(不可重复读):这个比上面那个稍微好一点,但是一样比较尴尬,就是说事务A在跑的时候, 先查询了一个数据是值1,然后过了段时间,事务B把那个数据给修改了一下还提交了,此时事务A再次查询这个数据就成了值2了,这是读了人家事务提交的数据啊,所以是读已提交。这个也叫做不可重复读,就是所谓的一个事务内对一个数据两次读,可能会读到不一样的值。

3)可重复读,Read Repeatable:这个就是比上面那个再好点儿,就是说事务A在执行过程中,对某个数据的值,无论读多少次都是值1;哪怕这个过程中事务B修改了数据的值还提交了,但是事务A读到的还是自己事务开始时这个数据的值。

(4)串行化:幻读,不可重复读和可重复读都是针对两个事务同时对某条数据在修改,但是幻读针对的是插入,比如某个事务把所有行的某个字段都修改为了2,结果另外一个事务插入了一条数据,那个字段的值是1,然后就尴尬了。第一个事务会突然发现多出来一条数据,那个数据的字段是1。如果要解决幻读,就需要使用串行化级别的隔离级别,所有事务都串行起来,不允许多个事务并行操作。

 

MySQL的默认隔离级别是Read Repeatable,就是可重复读,就是说每个事务都会开启一个自己要操作的某个数据的快照,事务期间,读到的都是这个数据的快照罢了,对一个数据的多次读都是一样的。

 

六、我们聊下MySQL是如何实现Read Repeatable?

因为一般我们都不修改这个隔离级别,但是你得清楚是怎么回事儿,MySQL是通过MVCC机制来实现的,就是多版本并发控制,multi-version concurrency control

innodb存储引擎,会在每行数据的最后加两个隐藏列,一个保存行的创建时间,一个保存行的删除时间,但是这儿存放的不是时间,而是事务id,事务idmysql自己维护的自增的,全局唯一。

 事务id,在mysql内部是全局唯一递增的,事务id=1,事务id=2,事务id=3

 

id name 创建事务id 删除事务id

张三 120 122

2 李四 119

2 小李四 122

 

事务id=121的事务,查询id=1的这一行的时候,一定会找到创建事务id <= 当前事务id的那一行,select * from table where id=1,就可以查到上面那一行

事务id=122的事务,将id=1的这一行给删除了,此时就会将id=1的行的删除事务id设置成122

事务id=121的事务,再次查询id=1的那一行,能查到吗?能查到,要求创建事务id <= 当前事务id,当前事务id < 删除事务id

事务id=121的事务,查询id=2的那一行,查到name=李四

事务id=122的事务,将id=2的那一行的name修改成name=小李四

事务id=121的事务,查询id=2的那一行,答案是:李四,创建事务id <= 当前事务id,当前事务id < 删除事务id

 

在一个事务内查询的时候,mysql只会查询创建时间的事务id小于等于当前事务id的行,这样可以确保这个行是在当前事务中创建,或者是之前创建的;同时一个行的删除时间的事务id要么没有定义(就是没删除),要么是必当前事务id大(在事务开启之后才被删除);满足这两个条件的数据都会被查出来。

 

那么如果某个事务执行期间,别的事务更新了一条数据呢?这个很关键的一个实现,其实就是在innodb中,是插入了一行记录,然后将新插入的记录的创建时间设置为新的事务的id,同时将这条记录之前的那个版本的删除时间设置为新的事务的id

 

现在get到这个点了吧?这样的话,你的这个事务其实对某行记录的查询,始终都是查找的之前的那个快照,因为之前的那个快照的创建时间小于等于自己事务id,然后删除时间的事务id比自己事务id大,所以这个事务运行期间,会一直读取到这条数据的同一个版本。

 

记住,聊到事务隔离级别,必须把这套东西给喷出来,尤其是mvcc,说实话,市面上相当大比重的java程序员,对mvcc是不了解的,这个东西很简单,结构大家居然不知道,真是相当大的差异!

posted @ 2020-03-14 01:23  C小海  阅读(278)  评论(0编辑  收藏  举报