深入浅出数据库事务
前言
说到数据库事务,很多人就会想事务的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会在读取每一行数据都加锁,所以可能导致大量的超时和锁争用问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。
文章转载自公众号:Java识堂