深入浅出数据库事务

Posted Java识堂

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出数据库事务相关的知识,希望对你有一定的参考价值。

前言

说到数据库事务,很多人就会想事务的ACID即原子性,一致性,隔离性,持久性,以及事物的四个隔离级别,但是并不是很明白为什么要用这四个特性来保证事务,以及事务的隔离级别是怎么产生的?包括LZ之前看《高性能mysql》关于事务的介绍也是一脸蒙蔽,因为太抽象了。偶然在慕课网上看到《在线分布式数据库原理与实践》这个视频,讲的挺不错的。特地总结一波,分享一下,相信读完本文你可以轻松理解这些概念 



事务简介

我们在写Java程序,遇到并发问题时,会想到用锁来解决。数据库遇到并发问题怎么解决呢?答案就是事务,事务的本质就是锁和并发的结合体

 单个事务单元

举个例子Bob给smith转账100块,会有如下几个操作 


深入浅出数据库事务

这里需要对Bob账户和Smith账户进行加锁保证只有一个线程可以操作这些账户 


深入浅出数据库事务

例如,这里线程1进去以后,线程2和线程3就会在外面等待,这样就保证了只有线程1能看到中间状态,如从Bob账户减去100还没来得及给Smith账户加上,此时Bob账户为0,Smith账户也为0,而线程2和3只能看到要么Bob有100块,要么Smith有100块。这样事务就保证了一致性,即要么Bob有100块,要么Smith有100块,而不会有中间状态

一组事务单元

假如我们现在有3个事务单元 
Bob给Smith100块 
Smith给joe100块 
Smith给Bob100块 


深入浅出数据库事务

因为有对共同账户的锁定,所以在上一个事务没有完成时,下个事务只能排队等待,这样性能是很低的 


前人总结了事务单元之间的Happen-before关系,只有如下四种 
读写,写读,读读,写写 ,事务如何保证上面四种操作的逻辑顺序的同时用最快的速度完成?


排队法(序列化读写)


深入浅出数据库事务


将所有的请求放到一个队列里面,从队头执行到队尾、 
优势:不需要冲突控制


排他锁(针对同一个单元的访问进行控制)


原来我们是将所有的请求放在一个队列,能不能放在不同队列中呢?例如Bob给Smith100块,Joe给Lisa100块,这个2个不同的事务单元,完全可以并行起来,如何做呢?直接加锁就可以 


深入浅出数据库事务

深入浅出数据库事务


如果Bob给Smith100块,Smith给Bob100块,因为这2个事务有共享单元,所以不能并行

 

深入浅出数据库事务


读写锁


上面我们已经提到事务之间的Happen-before关系只有四种,读写,写读,读读,写写,如果把读锁和写锁分离开,就可以让读并行,对于读多写少的任务就可以提高并行度


MVCC


现在主流的数据库实现是MVCC(多版本并发控制) 
本质就是copy on write,能够做到写不阻塞读 
MVCC能够做到写读不冲突,读读不冲突,读写也不冲突,唯一冲突的就是写和写,这样系统并发读就可以非常高

MVCC 提供了时点(point in time)一致性视图。MVCC 并发控制下的读事务一般使用时间戳或者事务 ID去标记当前读的数据库的状态(版本),读取这个版本的数据。读、写事务相互隔离,不需要加锁。读写并存的时候,写操作会根据目前数据库的状态,创建一个新版本,并发的读则依旧访问旧版本的数据

一句话讲,MVCC就是用 同一份数据临时保留多版本的方式 ,实现并发控制


 深入单机事务

我们来看一下事务的ACID(原子性,一致性,隔离性,持久性)

 原子性

原子性:一个事务要么同时成功,要么同时失败 
以前面的Bob给Smith转账为例,我们换一个视角,从数据库角度看,数据库存储的版本有哪几个? 


深入浅出数据库事务


如上图列出了所有的状态,以第二个步骤为例,转账时发现Smith这个账户并不存在,则状态需要回滚到ver1,该怎么回滚呢?其实当每次操作时,数据库会记录回滚段(即上图的undo信息),当需要回滚时按照undo信息回滚即可,假如ver3事务提交超时,则先将ver3回滚到ver2,再将ver2回滚到ver1即可,这就是事务的原子性,即要么Bob有100块,Smith有0块,要么Bob有0块,Smith有100块


原子性的语意只保证记了一个回滚段,这个回滚段能回滚到之前的版本,接着来说一致性,为什么需要一致性呢?接着上面再举个例子 


深入浅出数据库事务


假如说执行到ver2的时候,有另外一个进程将Smith的钱加到300,那么当事务1回滚的时候,会将Smith的钱改为0,Smith的300块就不翼而飞了,但是从原子性的定义来说它并不关心这个事,它只负责记录undo日志能回滚就行

 一致性

一致性的核心是Can(happen before) 


深入浅出数据库事务


如图,当多个事务单元执行时,视点关系有三种,一个事务在另一个事务之前或者之后发生(视点1和视点2),和两个事务同时发生(视点3),上文已经说到当事务1执行到ver2时,事务2对Smith账户进行了修改,当事务1回滚的时候会造成数据不一致的情况,为了避免这种情况,所以事务1会在操作时加锁,这样就会将视点3的请求上移到视点1,因为视点3获取不到Bob和Smith的锁,被迫等待。


这样做其实就是将所有的请求排队的过程,当然不是一个队列,因为会将锁下退到每个数据之上的,上文已经提到,一致性能保证看到系统内的所有更改,但是如果这样做,系统的并发是上不来的。例如,如果有一个事务锁定了这2个账号,其他所有的对这个2个账号的操作都不可能并行,只能等在外面。这样系统不得不选择另外一个概念,隔离性。

隔离性

隔离性:以性能为理由,对一致性的破坏 
事务的隔离级别有4个SERIALIZABLE(可串行化),REPEATABLE READ(可重复读),READ COMMITTED(提交读),READ UNCOMMITTED(未提交读)


排他锁


如果要保证一致性,只要保证事务的happen-before关系即可,但是当要保证对一个事务单元的绝对的强一致性,只有将所有的事务排队,这就是隔离级别中的可串性化(SERIALIZABLE),用排他锁保证单位时间只能有一个事务进来,当然性能非常低 


深入浅出数据库事务


读写锁


读写锁有2个隔离级别,可重复读(REPEATABLE READ),提交读(READ COMMITTED),这2个隔离级别是怎么产生的?


读写锁有一个很重要的概念(或者选择),读锁能不能别写锁升级,即当对一个事务单元加了一个读锁的时候,如果有新的写进来,这个读要不要放开,让写进去。


当读锁不能被写锁升级时,只能做到读读可并行,即可重读级别,这样并不能完美的提高系统性能,于是有另外一个隔离级别出来,这个隔离级别就是提交读


提交读这个隔离级别,读锁可以被写锁升级,如当2个读针对一个事务单元加了一个读锁的时候,一个新的写来了,允许写请求将读锁升级为写请求,这样可以做到读读并行,读写并行(写读还不能哦) 


深入浅出数据库事务
深入浅出数据库事务

说一下可重复读和提交读的区别,如果事务有2个并行的读,第二个人可以读到之前一个人读到的数据,这就叫可重复读。但如果读写可以并行,会出现如下情况,第一次读到版本号为1的数据,第二次写是并行的,可以更新到这个数据,如果再次读这个数据,可能读到的数据版本是不同的,于是就会出现不可重复读。


只加写锁,不加读锁


未提交读这个隔离级别就是只加写锁,不加读锁,这样可以做到读读并行,读写并行,写读并行 


读读并行,读写并行 


深入浅出数据库事务


写读并行(由上一步转化) 



写读并行(由上一步转化) 



问题:可能读到写过程中的数据,因为读没有加锁,只加了一个写锁,所以可能读到内部没有提交完成的数据,所以一般不用这个隔离级别,因为会读到中间状态

持久性

事务完成以后,该事务对数据库所做的更改便持久的保存在数据库之中


后记

最后附上《高性能MySQL》中对隔离级别的定义,加深理解


READ UNCOMMITTED(未提交读)

在READ UNCOMMITTED级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读(Dirty Read)。这个级别会导致很多问题,从性能上来说,READ UNCOMMITTED不会比其他的级别好太多,但却缺乏其他级别的很多好处,除非真的有非常必要的理由,在实际应用中一般很少使用。


READ COMMITTED(提交读)

大多数数据库系统的默认隔离级别都是READ COMMTTED(但MySQL不是)。READ COMMITTED满足前面提到的隔离性的简单定义:一个事务开始时,只能”看见”已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候叫做不可重复读(nonrepeatble read),因为两次执行同样的查询,可能会得到不一样的结果


REPEATABLE READ(可重复读)

REPEATABLE READ解决了脏读的问题。该隔离级别保证了在同一个事务中多次读取同样记录结果是一致的。但是理论上,可重复读隔离级别还是无法解决另外一个幻读(Phantom Read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row)。InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)解决了幻读的问题。


SERIALIZABLE(可串行化)

SERIALIZABLE是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。简单来说,SERIALIZABLE会在读取每一行数据都加锁,所以可能导致大量的超时和锁争用问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。


隔离级别

脏读可能性

不可重复读可能性

幻读可能性

加锁读

READ UNCOMMITTED

Yes

Yes

Yes

No

READ COMMITTED

No

Yes

Yes

No

REPEATABLE READ

No

No

Yes

No

SERIALIZABLE

No

No

No

Yes


以上是关于深入浅出数据库事务的主要内容,如果未能解决你的问题,请参考以下文章

深入浅出数据库事务

深入理解 Spring 事务原理

深入理解 Spring 事务原理

深入理解 Spring 事务原理

深入理解 Spring 事务原理

深入理解 Spring 事务原理