数据库的四种隔离和七种传播机制
一、spring事务简介以及事务的四个性质
spring事务简介:在数据库中的事务是指一系列不可拆分的数据库操作,这样说有些抽象,举个例子来了解一下。以
在ATM机上存钱为例:你把钱放在ATM机中存入后由ATM机识别并更新ATM机中的存储金额和更新你的银行卡账户余
额信息是一个不可拆分的操作,你存钱账户没更新对你有亏损,你没有存钱账户余额反而增加对银行有亏损。所以这
两个操作是一个不可拆分的一组操作,要么全部成功,要么全部失败。有很多业务上的逻辑有这种特性,而事务就帮
我们解决了这样的问题。spring事务是对事务的封装,帮助我们省略了一些步骤,并且给我们提供了传播机制。这样
能对数据库事务更好的控制。需要注意的是:不管是自己手动实现的事务,还是利用spring提供的事务。但是想要利用
事务的前提是需要有数据库引擎的支持,常用的数据库引擎有:innodb,bdb,myisam ,memory等。其中innodb和bdb
支持事务而myisam等不支持事务。
原子性:spring事务是指对数据库的一系列操作变成一个不可拆分的操作,所谓spring事务的原子性就是指在事务的
执行结果只有两种,要么全部成功,要么全部失败,不存在部分成功,部分失败的情况。
一致性:是指数据更新前后数据是具有完整性的,如果事务提交成功,则事务到一个新的完整(一致性)状态,否则回
到原来的完整(一致性)状态
隔离性:当多个用户并发的访问数据库时,不同事务之间互不影响,假设两个用户操作数据库的同一张表,但是对数
据库的操作是有同步顺序的,要么是一个用户先执行事务,要么这个用户在其他用户执行完事务之后再执行自己的事务
持久性:一旦事务提交成功之后,对数据库的改变就是永久的。
二、数据库的行锁的三种方式
1:Record Lock:单个行记录上的锁。
2:Gap Lock:间隙锁,锁定一个范围,而非记录本身。
3:Next-Key Lock:结合 Gap Lock 和 Record Lock,锁定一个范围,并且锁定记录本身。主要解决的问题是 RR 隔离级别下的幻读。
间隙锁的目的就是为了阻止多个事务将记录插入到同一范围内从而导致幻读。注意了,如果走唯一索引,那么 Next-Key Lock 会降级为 Record Lock。前置条件为事务隔离级别为 RR 且 SQL 走的非唯一索引、主键索引。如果不是则根本不会有 Gap 锁!
三、事务的隔离级别
数据库事务的隔离级别有4个,由低到高依次为Read uncommitted、Read committed、Repeatable read、
Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。
在介绍各种隔离级别之前,先简单说说数据库事务并发操作中可能出现的常见问题:脏读,不可重复读,幻读。
脏读:所谓脏读,可以理解为我们读到了错误的数据,比如一个事务先插入一条数据,由于发生异常事务又回滚了,
这时候新插的(实际不存在的)数据可能被其他事务读取。本身这个数据是不存在,这就是脏读现象。
不可重复读:一个事务中的两次读取(select)操作结果不一致。一个事务读取一个数据之后,该事务没有结束,后续
还有相同条件的读取操作,此时另一个事务对数据进行了更改,这时候之前的事务再次读取的时候,发现数据和第一
次读取的不一样。不可重复读的重点在于其他事务对数据update操作。
幻读:一个事务首先按条件查询一变数据库,这时候如果允许可重复读的情况下,不会对查询的数据进行操作,但是
无法控制其他事务对数据库的数据的insert(插入)操作和delete(删除)操作。所以再次查询数据的时候,可能就会发现符
合条件的数据变多了。
不可重复读和脏读的区别:不可重复读的重点在于对数据的update(更新),而幻读的重点在于对数据的(insert)插入操
作和delete(删除)操作。
1、Read uncommitted(读未提交):这是数据库事务中级别最低的隔离级别。由于一个事务在提交之前的结果可以被
其他事务所见,所以会导致脏读,不可重复读,幻读等问题。
2、Read committed(读提交):这种隔离机制保证了只有一个数据提交了之后,结果才能被其他事务所见。这是一般
的数据库默认的事务隔离级别,比如Sql Server , Oracle。但这种隔离级别可以解决脏读问题但是可能会导致不可重
复读的问题。一个事务的两次读取可能导致不同的结果。
3、Repeatable read(可重复读):在这种机制下,当一个事务在对数据库的事务进行操作的时候,其他事务不能对这
部分数据进行操作,解决了不可重复读的问题,这是MySQL默认的隔离级别。但可能会导致幻读的问题,因为在一个
事务在访问数据库数据的时候,不能控制其他事务对数据库的数据的插入删除问题。
4、Serializable(序列化):Serializable 是最高的事务隔离级别,在该级别下,事务顺序执行,不仅可以避免脏读、不
可重复读,还避免了幻像读,但是代价也花费最高,性能很低,失去了数据库异步执行的效率,这就相当于cpu的利
用问题,明明双核cpu,你非要他们按顺序执行,而且对于数据库的操作相当于机械I/O,效率是很低的,所以一般不采
用这种隔离机制。
四、Mysql下的MVCC
1:Mysql默认的RR采用多版本并发控制(MVCC,Multi-Version Concurrency Control,多版本并发控制)来控制事务的并
发操作。MVCC是一种乐观锁机制,在操作的时候不会真正的给数据加锁,而是通过版本号的机制来更改的支持并发
的读写。这个版本号是通过undo日志来实现的,我们可以简单理解为在每一行数据的最后两列有两个隐藏列,分别代
表事务的新建版本号和删除版本号。每次事务在读取的时候会与两个版本号有关系。
1⃣️可以读取到自己事务版本号>=新建版本号的数据(保证事务启动的时候数据已存在或者是当前事务创建的数据)
2⃣️可以读取到自己事务版本号<删除版本号的数据(保证事务启动的时候数据未删除)
2:mvcc并不能解决幻读的问题。(采用mvcc+next-key锁)
快照读:Mysql的innodb引擎在事务的select的时候会采取当前读模式,之后在当前事务再次读取的时候会读取之前的快
照的数据,即使其他事务更新数据,也不会对当前事务有影响。这时候不会有幻读的问题。
当前读:在当前事务进行写操作,包括删除,更新。这时候会采取当前读的模式。这时候会出现幻读的问题。
假设有这样一个表
create table test_table
`id` int(11) primary key auto_increment,
`code` int, KEY `xid` (`xid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; // 插入三条数据
insert into test(xid) values (1) // id = 1,code = 1
insert into test(xid) values (5) // id = 2,code = 5
insert into test(xid) values (8) // id = 3,code = 8
case1:两个事务执行如下命令会导致幻读
事务one:
1:select * from test_table where id between 1 and 5
2:update test_table set code = 3 where id = 2
3:select * from test_table where id between 1 and 5
事务 two:insert into test_table(code) values 4
这是因为在事务one执行的时候进行写操作,会进行当前读。旧版本的快照失效,再次读取的时候就会多读出一条数据。
case2:两个事务执行如下命令不会导致幻读事务
事务one:
1:select * from table where code between 1 and 5 for update(这里的code为表的普通索引)
2:insert into test_table(code) values 2
3:select * from table where code between 1 and 5 for update
事务 two:insert into test_table(code) values 4
这是因为在事务one执行的时候,因为加上了for update。并且走的是普通索引。读取的时候也会加上锁。不会采用mvcc的快照读或者当前度,这时候会有Next-Key Lock。锁住code的[1,5]的范围,事务two执行的时候就会被阻塞。事务one再次执行select不会读出事务two插入的值。
四、spring事务的传播行为
1) PROPAGATION_REQUIRED ,默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,
那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的
业务场景。
2)PROPAGATION_SUPPORTS ,从字面意思就知道,supports,支持,该传播级别的特点是,如果上下文存在事
务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在
transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。
应用场景较少。
3)PROPAGATION_MANDATORY , 该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的
传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被
调用,就必须有事务包含的情况,就可以使用这个传播级别。
4)PROPAGATION_REQUIRES_NEW ,从字面即可知道,new,每次都要一个新事务,该传播级别的特点是,每次都
会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
这是一个很有用的传播级别,举一个应用场景:现在有一个发送100个红包的操作,在发送之前,要做一些系统的初始
化、验证、数据记录操作,然后发送100封红包,然后再记录发送日志,发送日志要求100%的准确,如果日志不准确,
那么整个父事务逻辑需要回滚。
怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW 级别的事务传播控制就可以完成。发送
红包的子事务不会直接影响到父事务的提交和回滚。
5)PROPAGATION_NOT_SUPPORTED ,这个也可以从字面得知,not supported ,不支持,当前级别的特点就是上
下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
这个级别有什么好处?可以帮助你将事务极可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理
事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核
心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况。所以这个
事务这个级别的传播级别就派上用场了。用当前级别的事务模板抱起来就可以了。
6)PROPAGATION_NEVER ,该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而
PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个
级别上辈子跟事务有仇。
7)PROPAGATION_NESTED ,字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,
则嵌套事务执行,如果不存在事务,则新建事务。嵌套事务可以理解为父事务与子事务的关系,当父事务要执行子事务
的时候,会有一个安全点。当子事务执行结束之后,父事务会回到安全点执行其他逻辑操作。这里的安全点可以理解为
线程切换的安全点。需要注意的是子事务的回滚不会触发父事务回滚。子事务是和父事务一起提交的。所以父事务的回
滚会导致子事务回滚。
参考博客:
https://www.cnblogs.com/WJ-163/p/6023054.html
https://blog.csdn.net/qq_41376740/article/details/81835713
https://www.cnblogs.com/wj0816/p/8474743.html
https://juejin.im/post/5cd8283ae51d453a907b4b29
感谢以上博主!!!