事务的本质核心就是锁和并发。 事务只是让传统意义上难以理解的事情用一种更容易让你们理解的方式来表述的方法。事务比传统意义的锁和并发更容易让你们理解。数据库
(图1)编程
当锁定Bob帐户和锁定Smith帐户以后,只有一个请求(一个线程)能够进入到里面去操做帐户。线程2和线程3发现有锁,只能在外面等待。上面的三、四、5均可以用线程1来完成。只有线程1能够看到他们的中间状态(三、四、5)。线程2和线程3都看不到中间状态。在一个事务单元结束以前,其余的线程只能等待在锁外的。因此线程2和线程3只能看到要么Bob帐户中有钱,要么Smith帐户中有钱。并发
假设在从Bob帐户中减小100块的步骤结束后,就解除锁的状态,当线程2和线程3访问Bob的帐户和Smith的帐户,就会发现Bob和Smith的帐户都为0,就会出现数据不一致的状况(图2)。app
(图2)分布式
因此事务要保证的事情就是一致性,要么Bom有100块,要么Smith有100块(一致性)。Bob有100块或者Smith有100块的时候只要被线程2或线程3访问到,就必须是持久的,不能在回退到以前的状态(持久性)。这个过程就是一个事务单元。这个过程就能够保证A(一致性)I(原子性)D(持久性)。性能
(图3)spa
整个系统内会有三个事务单元,只有一个事务单元完成之后才可让其余的事务单元进入到里面拿到这把锁。每个事务单元在运行的过程当中的时候,看到的数据不会出现中间状态(Bob给Smith100块,Bob减小了钱,而Smith没有加上)。若是以这种方式考虑事务单元的关系,系统没有办法作到很清晰。事务单元的关系自己很是复杂。一些前辈就抽象了事务单元之间的关系(4.2章节)。.net
(图4)线程
把这四种状态映射到全部的事务单元之间的关系,都是这四种状态的重复(引自:事务处理)。 日志
(图5)
优势:不须要冲突控制。
缺点:慢速设备。若是一旦一个队列里的某一个请求很是慢,由于全部请求都不能并行,会致使系统性能很是低。
(图6)
假设有3个事务单元。事务单元1锁住了两行记录。事务单元2和3共享了一个事务记录。只要保证事务单元数据没有出现冲突,事务之间就不该该出现冲突。(好比:Bob给了Simth100块,李雷给了韩梅梅100块)。Bob给了Simth100块,李雷给了韩梅梅100块这两个事务单元彻底没有冲突。若是在彻底没有冲突的状况下,事务单元彻底能够并行起来。所以
(图7)
(图8)
结论:若是事务单元之间有可能发生冲突,就让它们串行。若是不发生冲突,彻底能够并行。实现这种方式只要在一个事务单元加锁就能够实现。只容许一个线程访问。这种加锁的方式,其它事务单元天然而然不会在上一个事务单元尚未结束前就能够访问。系统就能够经过加锁的方式来实现并发。同时又能够实现读读、写读、读写、读读。在保证数据一致的状况下,有没有更好的办法实现更高的速度呢?
事务单元之间的关系有四种场景:读写、写读、读读、写写。若是能够把它们分离开,就可让读彻底并行。读和读的场景彻底并行,而写读、读写、写写仍然串行。对于读多写少的状况下,能够更近一步的提高系统性能。传统数据库主流的作法就是写和读之间彻底分开(图9)。
(图9)
由于、读和读的场景彻底并行,而写读、读写、写写仍然串行的状况下,才会出现隔离性的不一样的隔离级别。
(图10)
把读的锁完全的去掉。读的时候若是不加锁,只有写的时候加锁(读的时候若是有个新的锁进来,就让它进入)。系统的并行读能够进一步提高。代价就是第一次读的时候,多是一条数据。第二次读的是另外的数据。这个场景下能够解决读写、读读的并行,但不能够解决写读、写写的并行,写写、写读、仍然是串行的。由于每次写的事务单元都是加锁的。
总结:隔离性(I) 自己就是对于传统数据库的一致性的一种破坏。最强的一致性必定是读写放入队列里串行的。
当加了写的操做的时候,系统彻底能够并发读。如今主流的数据库都在使用这种方式。这种方式能够作到写读不冲突、读读不冲突、读写不冲突。惟一冲突的就是写和写。这种方式并发度更高,实现起来更复杂。这个场景下能够解决读写、读读写读、的并行,但不能够写写的并行,写写仍然是串行的。(图11)。
(图11)
(图12)
当发生写的时候,数据库就会记录一个版本号。当读的时候也会记录一个版本号,当读的版本号大于写的版本号才能够。
逻辑时间戳(只是保证前后顺序):Oracle :SCN 。Innerdb : Trx_id 。
内存里面维持一个自增号,每一次写的时候就把自增号+1。这样就能够读到数据自己的最新值。若是读的时候在一个事务内读的话,内存里自增号也会+1。经过自增的ID号就能够保证事务自己的前后顺序。
(图13)
当Bob的帐户里没有100块的时候,事务就须要回滚。须要记录当前事务以前在作的全部操做的反向操做。这样的状况下。若是业务属性出现问题,就能够调用回滚的方式,来恢复到以前数据没有发生到任何更改的状态。
(图14)
当一个事务单元作完第四步(从Bob的帐户中减小100元)第5步(给Smith的帐户中增长100元)尚未来得及作的时候,整个系统崩溃了。必须须要再恢复回来。在进行数据恢复的时候尚未从故障中彻底恢复回来的时候,系统不可以被外部的其余应用访问的。 数据库系统自己对外暴露一个监听,为了防止数据库出现故障。
(图15)
相互等待,相互又维持了一个锁。
一、尽量不死锁(下降隔离级别:好比说读不加锁)。
二、碰撞检测(终止一边)(图16)
(图16)
三、等锁超时
仍是这个图(Bom给Smith100块的事务单元)。假设Bob帐户有100块,Smith帐户有没有钱,这样就能够把一个事务单元分红多个步骤:
第一个操做先检查Bob帐户有没有100元,发现Bob有100块,就会进入第二个操做(将Bob的钱取出100块)。Bob的钱就为0元。Smith的钱也是0。接着进入第三个步骤(将Bob的100块加到Smith的帐户中去)。Bob的钱就为0元,Smith的钱为100元。
任何系统内出现的故障,都有可能致使系统回滚。好比说在第二个版本会出现的问题是:若是Smith帐户不存在的话,必须到回滚到第一个版本的状态(Bob有100元,Smith有0元)。
若是在最后提交事务的时候超时了,事务就要回滚到第二步的状态(Bob有0元,Smith有0元)
前面两种回滚状态都须要面对同一个问题,都须要知道回滚到哪里去。数据库就会加入回滚段,数据库把这些回滚段记录到单独的地方去(如:日志表)。在版本2的时候记录一个回滚端(Bob有100元,Smith有0元)。若是第二个状态须要回滚,只须要把undo的信息(Bob有100元,Smith有0元)直接回溯到这个帐户上。
这就是原来事务处理自动化操做的一个逆向操做。
若是在第三个版本中,发现Commit没有成功,就会先回滚到上一个版本(先把Smith的钱减掉)。接着再回滚到上一个版本(Bob有100元,Smith有0元)。这种操做就是原子性的保证(要么所有成功,要么所有失败)。原子性操做不涉及到一致性相关问题的。原子性的语义只保证一个回滚段,这个回滚段能回滚到以前的版本。
若是有另外的一个事务进入到版本2的时候,把Smith的帐户进行了修改(假设把Smith的钱加了300块钱)。同时事务一出现了故障,进行回滚,回将Smth的钱改成0。这种状况(在一致性不保证的状况下)就会出现300块钱丢失的问题。可是从原子性的定义来讲,原子性不关心(care)这件事。
原子性只记录了一个undo日志回滚到以前的版本而已。
一致性的核心就是Can(happen before):
若是事务之间按照并列的关系来排列整个系统没有任何一致性问题。
假如两个事务同时发生的时候,就会有一致性问题(视点3)。:若是事务2出现问题以后进行回滚,两个事务就会出现冲突的状况(如:更新丢失、数据不一致问题)。
整个系统问题核心就是如何处理视点3(有读的请求,也有写的请求)(事务4)也就是如何处理不一样事务以前的读写并行,就是处理一致性的部分。
一个事务单元要保证一个事务单元所有成功之后才可见,这就是一致性的保证。若是说一个事务单元所有处理成功之后,下个事务才能进来,整个系统必定是一致性的。
可是这样作的话系统的并发度明显耗时。所以系统不的不选择另一个概念,就是隔离性。
若是全部的事务都是(happy-before)的关系的时候绝对可以保证事务单元的强一致性。
若是利用这种方式进行操做的时候,全部的事务单元理论上说都是串行的(序列化)。序列化读写(排他锁)串行就会很是严重的影响系统的性能。单位时间内只有一个事务单元可以进入。
性能差必定是认为是不可取的。
有没有更好的方式呢?:读写锁:
一、可重复读(Repeatable read):
它的核心是读写锁:一个很重要的概念是:读锁能不能被写锁升级。假如说对一个事务单元加一个读锁的状况下,若是有一个写写进来,以前的读锁要不要放开,让这个写进去。假如读锁不打开,不让写进去。只能作到读读并行。这种状况只能作到读读并行,不能完美的提高系统性能的。
二、读以提交(Read Committed):
就会出现幻读和不可重复读为何会产生。读已提交也是一个读写锁,不同的是
读锁能够被写锁升级。若是一个新写锁进来的时候,就能够将原来的读锁升级为写锁。 就会有一种新的模式:读写也能够并行。这样不少请求就能够往前提了。每一次将请求往前提的过程就能够认为都是增长的并行的状况。
在读已提交的时候为何会出现不可重复读。?
读写并行的话会出现下面的场景:第一次读取完成之后,读到一条数据(版本号为1),由于第二次写是并行的,因此第二次写会更新到这个数据。因而下个事务的读在进来的时候 就会发现原来事务新的数据已经被更改过了。第一次读数据和第二次读的数据以前数据读的版本是不一样的。因而就会出现不可重复读。由于读锁能够升级为写锁,就会出现同一个事务内的两次读可能会读到不一样版本的问题。
三、读未提交:只加写锁,读不加锁。
读读并行、读写并行、写读也能够并行。
所有的写是串行的,全部的读均可以并行。这种方式带来的问题。
可能读到写过程当中的数据。可能会读到内部没有提交完成的数据。由于读没有加锁。会读到中间状态的数据。
总结:
就是由于有读写锁,而后这些读写锁各类各样的组合就会出现不一样的隔离级别。由于有读写锁,读能不能升级,又被分出来两个隔离级别:读以提交,读未提交。
隔离性小结:
四个不一样的隔离级别是SQL92定义的标准的隔离性。
除了那四种标准的隔离级别以外,还有一种新的隔离级别出现:快照隔离级别(MVCC)。核心思路:无锁编程。Copy on Write.
快照读的核心方法(刚刚都是用锁来实现的):
事务开始以前的版本让你先读到。
每一次的写都要落到磁盘上,保证数据的持久性。事务完成以后,该事务对数据库的所作的更改便持久的保存在数据库之中。
可是,如何保证数据不丢?:RAID特性:若是将数据只写到1块A磁盘,若是A磁盘坏了,数据仍然不会丢失呢?就在Copy一份数据到另一个磁盘上。
一、磁盘的物理损坏
二、
事务:多个不一样的命令组装到一块儿的过程。
任何看起来相对简单的东西,背后其实要考虑的因素,远远超过你如今所想象的。
一、业务属性不匹配,须要回滚
二、系统的DOWN机:计算机就是个打字机。
若是走到第一步,当Lock的时候DOWN机了。
事务原子性操做只有一个标记:commit。若是commit以前全部的请求都要回滚。当commit完成的时候,以后的请求必须正常的提交完成而且必须可见。
数据库重启后进入recovery模式。会将提交后的事务单元继续完成提交。未提交事务单元回滚。在这期间,整个系统是不能被访问的。只有当全部recovery结束之后,将没有提交的返回回去。将提交后的继续完成。在这操做结束之后才会真正开始提供外部访问。(recovery过程也相似于原子操做,也必须保证ACID,只是实现方法不同)
若是recovery(恢复)的时候又挂了,在重启须要继续recovery。所以在recovery的时候也要记录日志,保证数据不会丢失。
一、减小锁的覆盖范围。(Myisam表锁 -->Innodb行锁)。将一个大锁变为多个小锁。
二、增长锁上可并行的线程数。
三、选择正确的锁类型:
一、读写比例比较高的状况 MVCC
二、
三、
四、调优
事务单元扩展
死锁扩展 - U锁
读锁。写锁分离
MVCC拾遗
一、分布式事务的目标
像传统单机事务同样的操做方式。可按需无限扩展
二、分布式事务与单机事务的相同点和不一样点
三、传统数据库的分布式事务
四、分布式事务面临的问题
二、分布式事务流行的解决方案 (Google Spanner、DRDS/TDDL)
4、参考连接: