精华内容
下载资源
问答
  •  事务是最小的逻辑执行单元,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务具有四个重要特性,即原子性(Atomicity)、一致性(Consistency)、隔离性 ...

    摘要:

      事务是最小的逻辑执行单元,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务具有四个重要特性,即原子性(Atomicity)、一致性(Consistency)、隔离性 (Isolation)和持久性 (Durability)。本文首先叙述了数据库中事务的本质及其四大特性(ACID)的内涵,然后重点介绍了事务隔离性的动机和内涵,并介绍了数据库为此所提供的事务隔离级别以及这些事务隔离级别能解决的事务并发问题。介于并发安全与并发效率的平衡,我们一般不会一味地提高事务隔离级别来保证事务并发安全性,而是通过结合其他机制(包括笔者提到的乐观锁和悲观锁机制)来解决数据库事务并发问题。

    版权声明与致谢:

      本文原创作者:书呆子Rico 
      作者博客地址:http://blog.csdn.net/justloveyou_/

      本文关于脏读、不可重复读和幻读的解释举例来源于博文《数据库事务隔离级别》。

    一. 事务概述
      一般而言,用户的每次请求都对应一个业务逻辑方法,并且每个业务逻辑方法往往具有逻辑上的原子性。此外,一个业务逻辑方法往往包括一系列数据库原子访问操作,并且这些数据库原子访问操作应该绑定成一个整体,即要么全部执行,要么全部不执行,通过这种方式我们可以保证数据库的完整性。也就是说,事务是最小的逻辑执行单元,是数据库维护数据一致性的基本单位。

      总的来说,事务是一个不可分割操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务具有四个重要特征,即原子性(Atomicity)、一致性(Consistency)、隔离性 (Isolation)和持久性 (Durability)。

    (1). 原子性(Atomicity)

      原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。 因此,事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不会对数据库有任何影响,也就是说,事务是应用中不可再分的最小逻辑执行体。

    (2). 一致性(Consistency)

      一致性是指事务执行的结果必须使数据库从一种一致性状态变到另一种一致性状态,也就是说,一个事务执行之前和执行之后数据库都必须处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

    (3). 隔离性 (Isolation) — 与事务并发直接相关

      隔离性是指并发执行的事务之间不能相互影响。也就是说,对于任意两个并发的事务 T1 和 T2,在事务 T1 看来,T2 要么在 T1 开始之前就已经结束,要么在 T1 结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。关于事务的隔离性下文会重点探讨。

    (4). 持久性 (Durability)

      持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。换句换说,事务一旦提交,对数据库所做的任何改变都要记录到永久的存储器中(通常就是保存到物理数据库)。

    二. 事务隔离性的内涵
      以上介绍完了事务的基本概念及其四大特性(简称ACID),现在重点来说明下事务的隔离性。我们知道,当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作以保证各个线程获取数据的准确性。也就是说,事务的隔离性主要用于解决事务的并发安全问题,那么事务的隔离性解决了哪些具体问题呢?

    1、事务并发带来的问题

    (1). 脏读

      脏读是指在一个事务处理过程中读取了另一个事务未提交的数据。比如,当一个事务正在多次修改某个数据,而当这个事务对数据的修改还未提交时,这时一个并发的事务来访问该数据,就会造成数据的脏读。看下面的例子:

      公司发工资了,领导把5000元打到singo的账号上,但是该事务并未提交,而singo正好去查看账户,发现工资已经到账,是5000元整,非常高兴。可是不幸的是,领导发现发给singo的工资金额不对,是2000元,于是迅速回滚了事务,修改金额后,将事务提交,最后singo实际的工资只有2000元,singo空欢喜一场。

      出现的上述情况就是我们所说的脏读,即对于两个并发的事务(事务A:领导给singo发工资、事务B:singo查询工资账户),事务B读取了事务A尚未提交的数据。特别地,当隔离级别设置为 Read Committed 时,就可以避免脏读,但是仍可能会造成不可重复读。特别地,大多数数据库的默认级别就是Read committed,比如Sql Server , Oracle。

    (2). 不可重复读

      不可重复读是指:对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔该数据被另一个事务修改并提交了。例如,事务 T1 在读取某一数据,而事务 T2 立马修改了这个数据并且提交事务,当事务T1再次读取该数据就得到了不同的结果,即发生了不可重复读。不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。看下面的例子:

      singo拿着工资卡去消费,系统读取到卡里确实有2000元,而此时她的老婆也正好在网上转账,把singo工资卡的2000元转到另一账户,并在singo之前提交了事务,当singo扣款时,系统检查到singo的工资卡已经没有钱,扣款失败,singo十分纳闷,明明卡里有钱,为何……

      上述情况就是我们所说的不可重复读,即两个并发的事务(事务A:singo消费、事务B:singo的老婆网上转账),事务A事先读取了数据,事务B紧接着更新了数据并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。当隔离级别设置为Repeatable read时,可以避免不可重复读。这时,当singo拿着工资卡去消费时,一旦系统开始读取工资卡信息(即事务开始),singo的老婆就不可能对该记录进行修改,也就是singo的老婆不能在此时转账。特别地,MySQL的默认隔离级别就是 Repeatable read。

    (3). 幻读

      幻读是事务非独立执行时发生的一种现象,即在一个事务读的过程中,另外一个事务可能插入了新数据记录,影响了该事务读的结果。例如,事务 T1 对一个表中所有的行的某个数据项执行了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。这时,操作事务 T1 的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。幻读和不可重复读都是读取了另一条已经提交的事务(这点与脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是数据记录插入/删除问题,二者关注的问题点不太相同。看下面的例子:

      singo的老婆工作在银行部门,她时常通过银行内部系统查看singo的信用卡消费记录。有一天,她正在查询到singo当月信用卡的总消费金额为80元,而singo此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录并提交了事务,随后singo的老婆将singo当月信用卡消费的明细打印到A4纸上,却发现消费总额为1080元,singo的老婆很诧异,以为出现了幻觉,幻读就这样产生了。当隔离级别设置为Serializable(最高的事务隔离级别)时,不仅可以避免脏读、不可重复读,还可以避免幻读。但同时代价也花费最高,性能很低,一般很少使用,因为在该级别下并发事务将串行执行。

    2、小结

      总的来说,事务的隔离性主要用于解决事务并发安全问题。上面提到的脏读、不可重复读和幻读三个典型问题都是在事务并发的前提下发生的,不同的是三者的问题关注点略有不同。脏读关注的是事务读取了另一个事务未提交的数据;不可重复读关注的是同一事务中对同一个数据项多次读取的结果互不相同;幻读更侧重于数据记录的插入/删除问题,比如同一事务中对符合同一条件的数据记录的多次查询的结果互不相同。更进一步地说,不可重复读关注的是数据的更新带来的问题,幻读关注的是数据的增删带来的问题。

    三. 数据库的事务隔离级别
      不同数据库的事务隔离级别不尽相同。比如我们在上一节提到,MySQL数据库支持下面的四种隔离级别,并且默认为 Repeatable read 级别;而在Oracle数据库中,只支持Serializable 级别和 Read committed 这两种级别,并且默认为 Read committed 级别。MySQL数据库为我们提供了四种隔离级别,分别为:

    Serializable (串行化):最高级别,可避免脏读、不可重复读、幻读的发生;
    Repeatable read (可重复读):可避免脏读、不可重复读的发生;
    Read committed (读已提交):可避免脏读的发生;
    Read uncommitted (读未提交):最低级别,任何情况都无法保证。

            

               

      从上图中可以看出,以上四种隔离级别中最高的是 Serializable级别,最低的是 Read uncommitted级别。当然,隔离级别越高,事务并发就越安全,但执行效率也就越低。比如,Serializable 这样的级别就是以锁表的方式(类似于Java多线程中的锁)保证并发事务的串行执行,但这时执行效率也降到了最低,所以,选用何种隔离级别实质上是一种并发安全与并发效率的平衡,应该根据实际情况而定。特别地,在MySQL数据库中,默认的事务隔离级别为 Repeatable read(可重复读),下面我们看看如何在MySQL数据库中操作事务的隔离级别。

    1). MySQL默认事务隔离级别查看

      在MySQL数据库中,我们可以通过以下方式查看当前事务的隔离级别:

        select @@tx_isolation;
    1
                        

    2). MySQL事务隔离级别修改

      在MySQL数据库中,我们可以分别通过以下两种方式设置事务的隔离级别,分别为:

        set  [glogal | session]  transaction isolation level 隔离级别名称;

        set tx_isolation='隔离级别名称';
    1
    2
    3
                      

    3). 使用JDBC对设置数据库事务的隔离级别

      设置数据库的隔离级别一定要是在开启事务之前。特别地,使用JDBC对数据库的事务设置隔离级别时,我们应该在调用Connection对象的setAutoCommit(false)方法之前调用Connection对象的setTransactionIsolation(level)去设置当前链接的隔离级别如下所示:

                

      至于参数level,可以使用Connection接口的字段,如以下代码所示:

                   

      特别地,通过这种方式设置事务隔离级别只对当前链接有效。对于使用MySQL命令窗口而言,一个窗口就相当于一个链接,当前窗口设置的隔离级别只对当前窗口中的事务有效;对于JDBC操作数据库来说,一个Connection对象相当于一个链接,而对于Connection对象设置的隔离级别只对该Connection对象有效,与其他链接Connection对象无关。

    四. 数据库并发控制
      也许大家已经听说过,锁分两种,一个叫 悲观锁,一种称之为 乐观锁。事实上,无论是悲观锁还是乐观锁,都是人们定义出来的概念,是一种解决问题的思想。因此,不仅仅在数据库系统中有乐观锁和悲观锁的概念,像memcache、hibernate、tair等都有类似的概念。比如,在线程并发处理中, Synchronized内置锁 就是悲观锁的一种,也称之为 独占锁,加了synchronized关键字的代码基本上就只能以单线程的形式去执行了,它会导致其他需要该资源的线程挂起,直到前面的线程执行完毕释放所资源;而 乐观锁是一种更高效的机制,它的原理就是每次不加锁去执行某项操作,如果发生冲突则失败并重试,直到成功为止,其实本质上不算锁,所以很多地方也称之为 自旋。

      在解决数据库的事务并发访问问题时,虽然将事务串形化可以保证数据在多事务并发处理下不存在数据不一致的问题,但串行执行使得数据库的处理性能大幅度地下降,常常是我们接受不了的。所以,一般来说,我们常常结合事务隔离级别和其它并发机制来保证事务的并发,以此来兼顾事务并发的效率与安全性。事实上,大多数数据库的隔离级别都会设置为 Read Committed(只能读取其他事务已提交的数据),然后由应用程序使用乐观锁/悲观锁机制来解决其他事务并发问题,比如不可重复读问题。特别地,乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

      特别地,乐观锁的理念是:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性;而悲观锁的理念是假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。针对于不同的业务场景,应该选用不同的并发控制方式。所以,不要把乐观并发控制和悲观并发控制狭义的理解为DBMS中的概念,更不要把他们和数据中提供的锁机制(行锁、表锁、排他锁、共享锁)混为一谈。需要指出的是,在DBMS中,悲观锁正是利用数据库本身提供的锁机制来实现的。

      Ps:更多关于 synchronized 关键字 的介绍, 请移步我的博文《Java 并发:内置锁 Synchronized》。

    1、乐观锁

      乐观锁,虽然名字中带“锁”,但是乐观锁并不锁住任何东西,而是在提交事务时检查这条记录是否被其他事务进行了修改:如果没有,则提交;否则,进行回滚。相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。如果并发的可能性并不大,那么乐观锁定策略带来的性能消耗是非常小的。乐观锁采用的实现方式一般是记录数据版本。

      数据版本是为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。一般地,实现数据版本有两种方式,一种是使用版本号,另一种是使用时间戳。

    2、悲观锁

      悲观锁,正如其名,它指的是对数据被外界修改持保守(悲观)态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现往往依靠数据库提供的锁机制,也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据。悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。和乐观锁相比,悲观锁则是一把真正的锁了,它通过SQL语句“select for update”锁住select出的那批数据,这时如果其他事务来更新这批数据时会等待。

      悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

    3、小结

      悲观锁和乐观锁都是一种解决并发控制问题的思想。特别地,在数据库并发控制方面,悲观锁与乐观锁有以下几点区别:

    思想:在事务并发环境中,乐观锁假设不会发生并发冲突,因此只在提交操作时检查是否违反数据完整性;而悲观锁假定会发生并发冲突,会屏蔽一切可能违反数据完整性的操作。

    实现:悲观锁是利用数据库本身提供的锁机制来实现的;而乐观锁则是通过记录数据版本实现的;

    应用场景:悲观锁主要用于数据争用激烈的环境或者发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中;而乐观锁主要应用于并发可能性并不太大、数据竞争不激烈的环境中,这时乐观锁带来的性能消耗是非常小的;

    脏读: 乐观锁不能解决脏读问题,而悲观锁则可以。

      总的来说,悲观锁相对乐观锁更安全一些,但是开销也更大,甚至可能出现数据库死锁的情况,建议只在乐观锁无法工作时才使用。

    五. 更多
      更多关于 synchronized 关键字 的介绍, 请移步我的博文《Java 并发:内置锁 Synchronized》。

      更多关于 Java 并发编程 方面的内容,请关注我的专栏 《Java 并发编程学习笔记》。本专栏全面记录了Java并发编程的相关知识,并结合操作系统、Java内存模型和相关源码对并发编程的原理、技术、设计、底层实现进行深入分析和总结,并持续跟进并发相关技术。
    --------------------- 
    作者:书呆子Rico 
    来源:CSDN 
    原文:https://blog.csdn.net/justloveyou_/article/details/70312810 
    版权声明:本文为博主原创文章,转载请附上博文链接!

    展开全文
  • 图:数据库页的存储布局页是磁盘与主存间传输数据的最小单元,也是内存中进行缓存的单元 页头(page header)包含页内空间管理的一些信息,比如空闲空间字节数、最大空闲区域大小等 页槽Slot Array的作用: ...
    事务信息系统-并发控制与恢复的理论, 算法与实践

    页的存储结构

    图:数据库页的存储布局页是磁盘与主存间传输数据的最小单元,也是内存中进行缓存的单元
    页头(page header)包含页内空间管理的一些信息,比如空闲空间字节数、最大空闲区域大小等

    页槽Slot Array的作用:
    varchar等变长类型字段的更新操作等,可能导致记录在页内移动,如果外部直接以数据记录的物理地址进行引用,记录移动时处理非常复杂,所以在中间添加一个隔离层Slot Array。Slot Array中存放页内数据的实际地址,以页号+槽号形成RID,外部均以RID引用数据记录
    数据在页内移动时只需要更新Slot Array中的实际地址即可,页间移动可以使用一个Forwarding RID实现,例如上图中的示例
    对于包含BLOB等长字段类型的记录,简单技巧可以将列的实际数据存储在其他多个页中,而在记录实际数据位置存储页号列表

    一次磁盘IO可以读取一个或多个连续的页,可以一次读取的固定数目的页称作块(block),在数据库理论中块和页等同对待
    数据库系统通常以区间(extent)为单位预先分配磁盘空间(一次预先分配一个或多个区间),区间是多个连续页。注意区间和块的区别
    存储层需要保存必要的元数据,例如将页号转换成物理磁盘地址、空闲空间管理等。通常使用区间表(extent table)来完成页号与物理磁盘地址的转换

    计算模型 Computational Models
    页模型page model
    也称作读写模型read/write model,是一种简单模型。例如事务t使用页模型定义类似如下:t=r(x)w(x)r(y)r(u)w(y),其中r表示读取数据页,w表示写数据页,对于存储层而言一个事务就是这样一个读写序列或偏序
    页模型通过简明优雅的方式抓住并发控制与恢复的实质,可以描述很多系统实现中的重要问题,局限在于仅以低层页的读写操为元素,没有表达数据访问操作的语义

    对象模型object model
    对象模型在页模型的基础上考虑高层操作,在对象模型中事务由ADT(abstract data type)操作构成,最终仍归于低层页的读写
    下面是一个对象模型示例图:

    图:对象模型示例示例场景说明如下:

        * 事务t1中先执行一个select语句,找出住在城市Austin的人员,然后执行一条insert语句,插入一条住在城市Austin的人员信息
        * 假设表中只有一个索引,位于城市这个字段,使用的B+ tree结构,索引结构只有2层

    ADT操作以及页的读写说明如下:

       1. Search('Austin'): select语句使用城市的索引查找符合Austin的索引项,得到RID列表
              * r(r): 读取B+ tree索引根节点页
              * r(l): 读取B+ tree索引叶节点页
       2. Fetch(x), Fetch(y): 根据RID列表加载数据x、y所在的数据页,得到数据记录x、y
              * r(p), r(q): 分别读取RID x和y所在的数据页p和q
       3. Store(z): 插入记录z
              * r(f): 读取存储层的元数据页f,其中记录了空闲空间信息
              * r(p): 从页f上得到页p具有足够空间插入记录z,这里读取这个页p
              * w(p): 把记录z写入页p,并将页p写回磁盘
              * r(r), r(l), w(l): 更新城市字段的索引,读取索引根节点页r、叶节点页l,更新叶节点页l并写回磁盘

    对象模型事务t是一棵树,用事务标识符标记根节点,用被调用的操作名称、参数标记非叶节点,用页模型的读写操作标记叶节点
    如果将事务的操作序列看成全序,则这个事务只能串行执行;如果将其看作偏序,则其中某些操作可以并行执行

    经典并发问题
    脏读dirty-read、不一致读inconsistent-read、幻象读phantom-read参考Transaction, Lock, Isolation Level
    更新丢失lost-update:

    图:丢失更新问题

    页模型上的并发控制 - 调度协议
    页模型的可串行性理论有终态可串行性、视图可串行性、冲突可串行性等,由于判定终态可串行性和视图可串行性的复杂度太高,因此商业数据库基本都采用冲突可串行性CSR类进行判定
    概念:
    历史history:指一个完整、已经结束的事务,即事务要么使用commit提交了,要么使用abort终止了
    调度schedule:指未完成的事务,即历史的一个前缀

    图:Transaction Scheduler图:事物调度器 Transaction Scheduler
    client 1、client 2等代表不同事物的操作序列
    事务管理器TM主要任务是事物登记,管理trans、commit、abort、active列表,维护ready-to-execute列表等
    调度器从TM接收输入调度,将其转化为可串行化的输出调度
    调度器分为乐观的、悲观的

    死锁处理
    一般基于等待图waits-for graph WFG概念,WFG中存在环路时即发生死锁
    死锁检测:持续检测continuous detection、周期检测periodic detection
    死锁处理方法一:允许发生死锁,从死锁中选出牺牲者消除WFG环路;处理方法二:死锁预防,即不允许死锁情况发生

    两阶段封锁协议 - two-phase locking protocol - 2PL
    对每个事物,加锁阶段严格区别于紧接着的释放阶段,该封锁协议就是两阶段的
    即可以明确的将事务分成前后两个部分,在前面部分只存在加锁,不存在锁释放,而在后面部分则只存在锁释放操作,没有加锁操作。商业数据库考虑性能问题不会严格按照2PL协议实现。SQL92不同的隔离级别本身就是性能与可串行性之间的一个取舍平衡,另外页模型并没有考虑到语义方面,商业数据库部分结合语义方面之后也能对封锁协议进行改造优化以换取性能

    图:2PL下锁增长和缩减阶段示意图图:2PL下锁增长和缩减阶段示意图

    2PL协议具有C2PL、S2PL、SS2PL几种变体
    下面是一个2PL调度器的调度示例:

    图:调度示例图:2PL调度器调度示例
    顶部的s是输入,底部3行是经过2PL调度器的一种可行的输出
    下表1、2、3等表示不同的事物t1、t2、t3等,wl表示加写锁,wu表示释放写锁,rl表示加读锁,ru表示释放读锁,c表示事物结束(commit)
    中间部分锁的持有时序图中,虚线部分表示锁冲突时的等待时间

    保守2PL - Conservative 2PL - C2PL
    在事物开始时对所有需要访问的数据获取锁。这个协议只能在有限的应用场景下使用,他不存在死锁问题,因为事物要么等待不能开始,要么就已经得到了全部所需的锁了

    图:C2PL下锁增长和缩减阶段示意图图:C2PL下锁增长和缩减阶段示意图

    严格2PL - Strict 2PL - S2PL
    事物一直持有已经获得的所有写锁,直到事物终止

    图:S2PL下锁增长和缩减阶段示意图图:S2PL下锁增长和缩减阶段示意图(假设事物中的锁都是写锁)

    强2PL - Strong 2PL - SS2PL
    事物获得的所有锁(包括读锁和写锁)被一直保持直到事物终止。这种情况下锁的增长和缩减阶段与S2PL相同,不过不局限于写锁

    有序共享2PL - Ordered Sharing 2PL - O2PL
    在2PL中,如果对同一个数据项申请的锁,与这个数据项上面已经存在的锁不相容时,就只能等待
    有序共享相容性规则:同一个数据项上的两个锁无论是否冲突,只要锁操作与相应的数据操作均以相同的顺序执行就可以被不同事物同时持有。这个规则以固定的顺序对数据项进行操作为前提,从而放宽了锁相容性规则,理论上证明是可行的,但他必须强制数据访问的顺序,需要额外的数据结构和运行时代价,实际中比较少采用

    利他锁 - Altruistic Locking - AL
    是2PL的一种扩展协议。某些事物时间可能很长,持有大量的锁,这样可能阻塞大量其他短事物。如果短事物访问的仅仅是长事物已经处理过的数据项的子集,则长事物将这部分数据项捐赠给其他事物访问

    非两阶段封锁协议
    有只写封锁树 write-only tree locking WTL、读写封锁树read/write tree locking RWTL等,他们只能在对数据项的访问遵从特定的顺序的情况下使用,比如对数据项的访问呈树状方式
    WTL在封锁规则的基础上添加了两条附加规则:只有在持有父项数据写锁的情况下,才能获取子项数据的写锁;事物对释放某个数据项的写锁之后,不能在获得该数据项的写锁。这确保了数据更新处理将沿着树的某一路径从根节点向叶节点进行,锁的范围可以限制在某个子树范围内。锁的申请和释放并不是两阶段的,沿着数的路径向子树进行时,父项的锁可以释放,即锁的申请和释放可以交替进行
    WTL协议是无死锁的,对RWTL协议规则稍作修改可以导出DAG封锁协议DAG locking protocol

    时间戳排序协议 - timestamp ordering protocol - TO
    不使用封锁,基本规则是对不同事物冲突的操作,严格按照事物开始时间的先后关系进行调度
    在封锁协议中通过锁机制检测冲突,在TO中也需要有额外的信息用于检测冲突。例如Ti在Tj之前开始,调度器先对Tj输出了一个调度qj(x),但后来 Ti发送过来一个pi(x)的操作,与qj(x)冲突(即按照TO规则,pi(x)必须在qj(x)之前执行,但在pi(x)之前qj(x)已经被调度输出了),所以调度器需要记录信息以检测这种冲突并作出相应处理(例如阻塞Ti,等待Tj结束后再开始)

    SGT协议 - Serialization Graph Tester - 可串行化图检测器
    维护一张SGT图,图中并发的每个事物拥有一个节点,事物Ti的操作与Tj冲突,如果Tj中发生冲突的操作已经输出了,则由Tj向Ti添加一条有向边,Ti进入等待状态。如果事物Ti的某个操作导致SGT图中存在环路,则输出是不可串行化的,事物Ti需要取消
    实际中SGT协议需要维护的额外数据量以及运算量都比较大,对于调度器而言无法接收,所以很少采用

    对象模型上的并发控制,搜索结构上的并发控制
    ......

    多版本并发控制 - multiversion concurrency control
    单版本时数据库中的每个数据项只有一个副本,而多版本时则会为数据项同时维护多个版本,而多版本对于client是透明的,对client而言数据库仍然只维护了数据项的一个副本。通过对并发控制算法的扩展调整,多版本同样可以达到可串行性目标,他仅仅是并发控制的另一种处理方式。他的优点是可以降低封锁协议中锁的使用,极大的提高并发处理性能,并给数据恢复等方面带来好处

    多版本时间戳排序协议 - multiversion timestamp ordering protocol - MVTO
    本质上类似FIFO方式处理并发操作。每个事物按照事物开始时的时间戳排序,基于这个顺序处理调度和冲突
    下面对的规则描述中,Ti表示第i个事物;ts(Ti)表示第i个事物的时间戳;Ri(X)表示Ti读取数据项X;Xk则表示数据项X的某一个版本,他是由事物Tk写入的;ts(Xk)表示数据项的Xk这个版本的时间戳,他就是事物Tk的时间戳,即ts(Xk)=ts(Tk)。下面是MVTO规则:

       1. Ri(X)将被转化为Ri(Xk),即需要明确读取数据项X的哪一个版本,其中Xk应该为ts(Xk)<ts(Ti)的一个最新版本。另外Xk可能是已提交的版本,也可能是未提交的版本
       2. 处理Wi(X)时,如果已经存在一个Rj(Xk)操作,并且时间戳关系为ts(Xk)<ts(Ti)<ts(Tj),则Wi(X)被拒绝,Ti将被取消,因为这样存在冲突,将造成不可串行化的结果。否则,Wi(X)被转化为Wi(Xi)并执行,即为数据项X生成一个新的版本Xi
       3. 可选规则:延迟提交。这是在不允许脏读的情况下用来避免产生脏读的一个机制。如果事物Ti读取了Tj写入的某个数据项,即存在Ri(Xj)操作,则事物Ti的提交操作Ci需要被推迟到事物Tj成功提交之后再执行。如果事物Tj失败则事物Ti也失败(或重做)

    下面是MVTO协议下的一个执行示例,可以使用上面的规则进行解释:

    图:MVTO协议下的执行示例

        * 事物t3中因为存在r3(x2)操作,即读取了x2这个未提交的版本(按照规则1,他应该读取x2的),所以他的提交被延迟(虚线表示等待),在t2提交之后再提交
        * t4被终止是因为规则2的作用,因为t5中存在r5(y2)这个操作,如果让w4(y4)成功,则事物t5可能出现不一致读的情况,不是一个可串行化的调度。图来自原书,图中应该有个错误,r5(y2)不可能出现在w2(y2)前面的,他应该位于w2(y2)与w4(y4)之间才对
        * 最后看一下t1,在读取y的时候,根据MVTO的规则1,应该读取y0这个版本。这样在没有加锁的情况下实现了事物t1的一致性读,但他并没有利用锁阻塞t2、t4(两阶段封锁协议的情况下他们的写操作与t1的读锁冲突),而是让t2、t4并行执行。根据MVTO规则,t1不能有写x、y的操作,否则按照规则2应该取消t1。这样从理论上看存储层还是可以保证x、y的可串行性结果的,但存在一个疑问是:如果t1有其他写操作,比如 w1(u1),而u1是基于x、y的运算而来,即u1=f0(u)f1(x)f2(y),如何确保数据项u的可串行性结果呢?还有对外部而言t1实现了对 x、y的一致性读,但w2(y2)成功了,外部基于x、y得来的计算结果可能就是一个不正确的值,如何确保一致性?

    另外还有多版本两阶段封锁协议 - multiversion 2PL locking protocol - MV2PL、只读多版本协议 - read-only multiversion protocol - ROMV等

    暂态版本化 - Transient Versioning
    暂态版本化的做法是将数据项的最新版本放在数据记录位置上,数据项以前的旧版本都放在版本池version pool中,版本池可以驻留内存或者磁盘。这样当访问最新版本数据时没有性能损失,版本池也方便对旧版本的垃圾回收。有些系统中把版本池跟用于恢复的日志项合并在一起。版本池也叫做回滚段rollback segment

    图:暂态版本化的存储组织图:暂态版本化的存储组织
    数据项的每个版本包括两个额外的字段,一个是创建时间戳,另一个是指向前一版本的指针

        * 对于新增的数据项,还没有旧版本,因此在当前版本中指针为空,例如图中的1135
        * 删除的数据仍然保留在当前版本位置上,但会打上删除标记,例如图中的1141
        * 同一个数据项的各个版本组成一个链表

    另一种方法是在数据项的当前版本中维护一个小的版本选择表,其中包含每个版本的时间戳以及一个指向该版本位置的指针
    如果一个数据项的旧版本再也不会被当前活动的事务使用到,则这些旧版本就成为垃圾,可以被回收掉

    关系数据库的并发控制
    面向谓词的并发控制 - Predicate-Oriented Concurrency Control
    封锁协议是针对页、数据项加锁,而面向谓词的并发控制是针对操作加锁,更具体的是针对操作的谓词加锁。例如:
        select name from persons where age>30
    则将age>30与锁关联
    面向谓词的并发控制是比较早提出来的一种想法,后来也经过了不少研究,他的问题在于检测两个谓词是否兼容是一个NP完全问题,成本太高,但在特定场景下,例如B+树上的并发控制,具有实际意义

    实现和实用性问题
    通用的锁管理器数据结构

    图:锁管理器的数据结构
    锁管理器需要达成的目标:

       1. 请求锁时可以检验冲突
       2. 释放锁时,需要将资源授予被该锁阻塞的其他锁
       3. 事务终止时释放该事务的所有锁

    锁管理器的数据结构常驻内存,由resource ID索引的哈希表存放已经加锁以及需要加锁的资源ID
    每个哈希表项指向一个资源控制块RCB,RCB中的hash chain用于解决hash冲突
    对于共享锁,多个锁可以同时被同一资源持有,多个锁请求(例如冲突的写锁)可能正在等待被授予该资源上,这样在同一个资源上将形成一个已持有的共享锁列表以及等待被授予的排他锁与共享锁队列,因此在RCB上会指向一个锁控制块LCB的链接表,即上图中的FirstInQueue, NextInQueue链表
    为了达成第3点,为每个活动事务维护一个事务控制块TCB,同样使用链表将该事务的所有锁链接起来,即上图中的LCB chain构成的链表

    锁的数据结构将是数据库中访问最频繁的地方之一,因此需要控制这个内存数据结构的大小来避免性能问题,所以一般数据库中存在锁粒度升级的处理方式。如果一个事务已经持有了大量细粒度的行锁,数据库可能会将锁粒度升级为页锁或者表锁,以减小锁管理器的内存空间
    使用了不同粒度的锁,就存在不同粒度之间相容性的判断问题,所以引入了意向锁,参考数据库系统 - 面向应用的方法
    展开全文
  • 原子性(atomicity): 事务的最小工作单元,要么全成功,要么全失败。 一致性(consistency): 事务开始和结束后,数据库的完整性不会被破坏。 隔离性(isolation): 不同事务之间互不影响,四种隔离级别为RU(读未提交...

    事务的四大特性(ACID)

    1. 原子性(atomicity): 事务的最小工作单元,要么全成功,要么全失败。
    2. 一致性(consistency): 事务开始和结束后,数据库的完整性不会被破坏。
    3. 隔离性(isolation): 不同事务之间互不影响,四种隔离级别为RU(读未提交)、RC(读已提交)、RR(可重复读)、SERIALIZABLE (串行化)。
    4. 持久性(durability): 事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失。

    事务的隔离级别

    读未提交(Read UnCommitted/RU)

    又称为脏读,一个事务可以读取到另一个事务未提交的数据。这种隔离级别岁最不安全的一种,因为未提交的事务是存在回滚的情况。

    读已提交(Read Committed/RC)

    又称为不可重复读,一个事务因为读取到另一个事务已提交的修改数据,导致在当前事务的不同时间读取同一条数据获取的结果不一致。

    举个例子,在下面的例子中就会发现SessionA在一个事务期间两次查询的数据不一样。原因就是在于当前隔离级别为 RC,SessionA的事务可以读取到SessionB提交的最新数据。

    发生时间 SessionA SessionB
    1 begin;
    2 select * from user where id=1;(张三)
    3 update user set name=‘李四’ where id=1;(默认隐式提交事务)
    4 select * from user where id=1;(李四)
    5 update user set name=‘王二’ where id=1;(默认隐式提交事务)
    6 select * from user where id=1;(王二)

    可重复读(Repeatable Read/RR)

    又称为幻读,一个事物读可以读取到其他事务提交的数据,但是在RR隔离级别下,当前读取此条数据只可读取一次,在当前事务中,不论读取多少次,数据任然是第一次读取的值,不会因为在第一次读取之后,其他事务再修改提交此数据而产生改变。因此也成为幻读,因为读出来的数据并不一定就是最新的数据。

    举个例子:在SessionA中第一次读取数据时,后续其他事务修改提交数据,不会再影响到SessionA读取的数据值。此为可重复读

    发生时间 SessionA SessionB
    1 begin;
    2 select * from user where id=1;(张三)
    3 update user set name=‘李四’ where id=1; (默认隐式提交事务)
    4 select * from user where id=1;(张三)
    5 update user set name=‘王二’ where id=1;(默认隐式提交事务)
    6 select * from user where id=1;(张三)

    串行化(Serializable)

    所有的数据库的读或者写操作都为串行执行,当前隔离级别下只支持单个请求同时执行,所有的操作都需要队列执行。所以种隔离级别下所有的数据是最稳定的,但是性能也是最差的。数据库的锁实现就是这种隔离级别的更小粒度版本。

    发生时间 SessionA SessionB
    1 begin;
    2 begin;
    3 update user set name=‘李四’ where id=1;
    4 select * from user where id=1;(等待、wait)
    5 commit;
    6 select * from user where id=1;(李四)

    事务和MVCC原理

    不同事务同时操作同一条数据产生的问题

    示例:

    发生时间 SessionA SessionB
    1 begin;
    2 begin;
    3 查询余额 = 1000元
    4 查询余额 = 1000元
    5 存入金额 100元,修改余额为 1100元
    6 取出现金100元,此时修改余额为900元
    8 提交事务(余额=1100)
    9 提交事务(余额=900)
    发生时间 SessionA SessionB
    1 begin;
    2 begin;
    3 查询余额 = 1000元
    4 查询余额 = 1000元
    5 存入金额 100元,修改余额为 1100元
    6 取出现金100元,此时修改余额为900元
    8 提交事务(余额=1100)
    9 撤销事务(余额恢复为1000元)

    上面的两种情况就是对于一条数据,多个事务同时操作可能会产生的问题,会出现某个事务的操作被覆盖而导致数据丢失。

    LBCC 解决数据丢失

    LBCC,基于锁的并发控制,Lock Based Concurrency Control。

    使用锁的机制,在当前事务需要对数据修改时,将当前事务加上锁,同一个时间只允许一条事务修改当前数据,其他事务必须等待锁释放之后才可以操作。

    MVCC 解决数据丢失

    MVCC,多版本的并发控制,Multi-Version Concurrency Control。

    使用版本来控制并发情况下的数据问题,在B事务开始修改账户且事务未提交时,当A事务需要读取账户余额时,此时会读取到B事务修改操作之前的账户余额的副本数据,但是如果A事务需要修改账户余额数据就必须要等待B事务提交事务。

    MVCC使得数据库读不会对数据加锁,普通的SELECT请求不会加锁,提高了数据库的并发处理能力。借助MVCC,数据库可以实现READ COMMITTED,REPEATABLE READ等隔离级别,用户可以查看当前数据的前一个或者前几个历史版本,保证了ACID中的I特性(隔离性)。

    InnoDB的MVCC实现逻辑

    InnoDB存储引擎保存的MVCC的数据

    InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现的。一个保存了行的事务ID(DB_TRX_ID),一个保存了行的回滚指针(DB_ROLL_PT)。每开始一个新的事务,都会自动递增产 生一个新的事务id。事务开始时刻的会把事务id放到当前事务影响的行事务id中,当查询时需要用当前事务id和每行记录的事务id进行比较。

    下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的。

    SELECT

    InnoDB 会根据以下两个条件检查每行记录:

    1. InnoDB只查找版本早于当前事务版本的数据行(也就是,行的事务编号小于或等于当前事务的事务编号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
    2. 删除的行要事务ID判断,读取到事务开始之前状态的版本,只有符合上述两个条件的记录,才能返回作为查询结果。

    INSERT

    InnoDB为新插入的每一行保存当前事务编号作为行版本号。

    DELETE

    InnoDB为删除的每一行保存当前事务编号作为行删除标识。

    UPDATE

    InnoDB为插入一行新记录,保存当前事务编号作为行版本号,同时保存当前事务编号到原来的行作为行删除标识。

    保存这两个额外事务编号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。

    MVCC只在REPEATABLE READ和READ COMMITIED两个隔离级别下工作。其他两个隔离级别都和 MVCC不兼容 ,因为READ UNCOMMITIED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。

    MVCC 在mysql 中的实现依赖的是 undo log 与 read view 。

    undo log

    根据行为的不同,undo log分为两种:insert undo logupdate undo log

    • insert undo log:

    insert 操作中产生的undo log,因为insert操作记录只对当前事务本身课件,对于其他事务此记录不可见,所以 insert undo log 可以在事务提交后直接删除而不需要进行purge操作。

    purge的主要任务是将数据库中已经 mark del 的数据删除,另外也会批量回收undo pages

    数据库 Insert时的数据初始状态:

    img

    • update undo log:

      update 或 delete 操作中产生的 undo log。因为会对已经存在的记录产生影响,为了提供 MVCC机制,因此update undo log 不能在事务提交时就进行删除,而是将事务提交时放到入 history list 上,等待 purge 线程进行最后的删除操作。

      数据第一次被修改时:

      img

    当另一个事务第二次修改当前数据:

    img

    为了保证事务并发操作时,在写各自的undo log时不产生冲突,InnoDB采用回滚段的方式来维护undo log的并发写入和持久化。回滚段实际上是一种 Undo 文件组织方式。

    ReadView

    对于 RU(READ UNCOMMITTED) 隔离级别下,所有事务直接读取数据库的最新值即可,和 SERIALIZABLE 隔离级别,所有请求都会加锁,同步执行。所以这对这两种情况下是不需要使用到 Read View 的版本控制。

    对于 RC(READ COMMITTED)RR(REPEATABLE READ) 隔离级别的实现就是通过上面的版本控制来完成。两种隔离界别下的核心处理逻辑就是判断所有版本中哪个版本是当前事务可见的处理。针对这个问题InnoDB在设计上增加了ReadView的设计,ReadView中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids

    对于查询时的版本链数据是否看见的判断逻辑:

    • 如果被访问版本的 trx_id 属性值小于 m_ids 列表中最小的事务id,表明生成该版本的事务在生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
    • 如果被访问版本的 trx_id 属性值大于 m_ids 列表中最大的事务id,表明生成该版本的事务在生成 ReadView 后才生成,所以该版本不可以被当前事务访问。
    • 如果被访问版本的 trx_id 属性值在 m_ids 列表中最大的事务id和最小事务id之间,那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

    举个例子:

    READ COMMITTED 隔离级别下的ReadView

    每次读取数据前都生成一个ReadView (m_ids列表)

    时间 Transaction 777 Transaction 888 Trasaction 999
    T1 begin;
    T2 begin; begin;
    T3 UPDATE user SET name = ‘CR7’ WHERE id = 1;
    T4
    T5 UPDATE user SET name = ‘Messi’ WHERE id = 1; SELECT * FROM user where id = 1;
    T6 commit;
    T7 UPDATE user SET name = ‘Neymar’ WHERE id = 1;
    T8 SELECT * FROM user where id = 1;
    T9 UPDATE user SET name = ‘Dybala’ WHERE id = 1;
    T10 commit;
    T11 SELECT * FROM user where id = 1;

    这里分析下上面的情况下的ReadView

    时间点 T5 情况下的 SELECT 语句:

    当前时间点的版本链:

    img

    此时 SELECT 语句执行,当前数据的版本链如上,因为当前的事务777,和事务888 都未提交,所以此时的活跃事务的ReadView的列表情况 m_ids:[777, 888] ,因此查询语句会根据当前版本链中小于 m_ids 中的最大的版本数据,即查询到的是 Mbappe。

    时间点 T8 情况下的 SELECT 语句:

    当前时间的版本链情况:

    img

    此时 SELECT 语句执行,当前数据的版本链如上,因为当前的事务777已经提交,和事务888 未提交,所以此时的活跃事务的ReadView的列表情况 m_ids:[888] ,因此查询语句会根据当前版本链中小于 m_ids 中的最大的版本数据,即查询到的是 Messi。

    时间点 T11 情况下的 SELECT 语句:

    当前时间点的版本链信息:

    img

    此时 SELECT 语句执行,当前数据的版本链如上,因为当前的事务777和事务888 都已经提交,所以此时的活跃事务的ReadView的列表为空 ,因此查询语句会直接查询当前数据库最新数据,即查询到的是 Dybala。

    总结: 使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView。

    REPEATABLE READ 隔离级别下的ReadView

    在事务开始后第一次读取数据时生成一个ReadView(m_ids列表)

    时间 Transaction 777 Transaction 888 Trasaction 999
    T1 begin;
    T2 begin; begin;
    T3 UPDATE user SET name = ‘CR7’ WHERE id = 1;
    T4
    T5 UPDATE user SET name = ‘Messi’ WHERE id = 1; SELECT * FROM user where id = 1;
    T6 commit;
    T7 UPDATE user SET name = ‘Neymar’ WHERE id = 1;
    T8 SELECT * FROM user where id = 1;
    T9 UPDATE user SET name = ‘Dybala’ WHERE id = 1;
    T10 commit;
    T11 SELECT * FROM user where id = 1;

    时间点 T5 情况下的 SELECT 语句:

    当前版本链:

    img

    再当前执行select语句时生成一个ReadView,此时 m_ids 内容是:[777,888],所以但前根据ReadView可见版本查询到的数据为 Mbappe。

    时间点 T8 情况下的 SELECT 语句:

    当前的版本链:

    img

    此时在当前的 Transaction 999 的事务里。由于T5的时间点已经生成了ReadView,所以再当前的事务中只会生成一次ReadView,所以此时依然沿用T5时的m_ids:[777,999],所以此时查询数据依然是 Mbappe。

    时间点 T11 情况下的 SELECT 语句:

    当前的版本链:

    img

    此时情况跟T8完全一样。由于T5的时间点已经生成了ReadView,所以再当前的事务中只会生成一次ReadView,所以此时依然沿用T5时的m_ids:[777,999],所以此时查询数据依然是 Mbappe。

    MVCC总结:

    所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用 READ COMMITTDREPEATABLE READ 这两种隔离级别的事务在执行普通的 SEELCT 操作时访问记录的版本链的过程,这样子可以使不同事务的 读-写写-读操作并发执行,从而提升系统性能。

    在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。在 READ COMMITTED 中每次查询都会生成一个实时的 ReadView,做到保证每次提交后的数据是处于当前的可见状态。而 REPEATABLE READ 中,在当前事务第一次查询时生成当前的 ReadView,并且当前的 ReadView 会一直沿用到当前事务提交,以此来保证可重复读(REPEATABLE READ)。

    摘自:三太子敖丙的文章

    展开全文
  • 原子性(automicity):一个事务必须被看作一个不可分割的最小单元。对于事务里的操作要么全部成功,要么全部失败,不可能执行其中一部分。这就是事务的原子性。 一致性(consistency):数据库总是从一个一致性的状态...

    数据库模型:

    这里写图片描述

    ACID:

    原子性(automicity):一个事务必须被看作一个不可分割的最小单元。对于事务里的操作要么全部成功,要么全部失败,不可能执行其中一部分。这就是事务的原子性。
    一致性(consistency):数据库总是从一个一致性的状态转换到另外一个一致性的状态。在事务没有提交之前,事务中做出的修改不会被保存到数据库中。
    隔离性(isolation):通常来说,一个事务做的修改,对于其他事务是不可见的。当然这取决于隔离的级别。
    持久性(durability):一旦事务提交,数据将会被永久地保存在数据库中,永远保存在数据库。

    数据库隔离的级别:

    1、未提交可读:A事务修改某条记录,未提交;B事务可以读取A事务修改后的该条记录数据值。
    2、提交可读(不可重复读):A事务第一次读取某条记录后,B事务修改该条记录且提交,A事务再次读取该条记录,A事务2次读取的值不同。
    3、可重复读:A事务第一次读取某条记录后,B事务修改该条记录且提交,A事务再次读取该条记录,A事务2次读取的值相同。
    该级别无法防住“幻读”,即A事务第一次读取某个范围记录中,B事务在该范围新增一条记录且提交,A事务再次读取该范围记录时,A事务第2次读取的范围值会多一行。
    4、可串行化:强制事务串行执行,避免了幻读的问题。该方式是在每一行都加锁,所以可能会存在大量的超时和锁竞争,一般很少用这个级别,
    只用必须保证数据的一致性且可以接受不需要并发的场景才会使用。

    死锁:

    是指两个或多个事务在同一个资源上的相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就会产生死锁,
    多个事务锁定同一个资源也会产生死锁。
    比如下面两个事务:
    事务1:
    START TRANSACTION;
    UPDATE T_ACCOUNT SET ACCOUNT_NO = 55555 WHERE ID = 3;
    UPDATE T_ACCOUNT SET ACCOUNT_NO = 33333 WHERE ID = 4;
    COMMIT;

    事务2:
    START TRANSACTION;
    UPDATE T_ACCOUNT SET ACCOUNT_NO = 33333 WHERE ID = 4;
    UPDATE T_ACCOUNT SET ACCOUNT_NO = 55555 WHERE ID = 3;
    COMMIT;

    如果凑巧,两个事务都执行了第一条UPDATE语句,更新了一行数据且同时锁定了该行数据,接着同时去UPDATE第二条语句会发现对方被锁定,等待获得锁,进而进入死循环。
    为了解决这个问题,数据库系统实现了各种死锁检测和死锁超时检测机制。越复杂的系统,比如InnoDB存储引擎,越能检测出死锁的循环依赖,并返回一个错误。这种解决
    方式很有效,否则会出现非常慢的查询。还有一种解决方式,就是在查询的时候达到锁的超时时间的设定后放弃锁请求,这种方式通常来说不太好。InnoDB目前处理死锁的
    方式是:将持有最少行级排它锁的事务进行回滚。

    MYSQL采用自动提交模式。如果不是显示开始一个事务,则每个查询都被当做一个事务执行提交操作。在当前连接中,可以通过设置AUTOCOMMIT变量来启用或禁用自动提交模式。
    查看自动提交是否开启:SHOW VARIABLES LIKE ‘AUTOCOMMIT’;
    1或者ON表示开启,0或者OFF表示关闭:SET AUTOCOMMIT = ‘1’;

    设置全局的事务隔离级别:SET TRANSACTION ISOLATION LEVEL READ COMMIT;
    设置当前会话的事务隔离级别:SET SESSION TRANSACTION ISOLATION LEVEL READ COMMIT;

    InnoDB的多版本并发控制(MVCC):

    MVCC是行级锁的一个变种,但它在很多情况下避免了加锁的操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
    MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个
    事务对同一张表,同一时刻看到的数据可能是不一样的。
    InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际
    的时间值,而是系统版本号。没开始一个事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号作为比
    较。下面看一下可重复读隔离级别下,MVCC如何操作的:
    1、SELECT
    InnoDB会根据一下两个条件检查记录
    a.InnoDB只会查找版本早于当前事务版本的行(也就是行的系统版本号小于或等于当前事务的系统版本号),这样可以确保事务读取的行,要么在事务开始前已经存在,要
    么是事务自身插入或者修改的。
    b.行的删除版本号要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始前未被删除。
    2、INSERT
    InnoDB为新插入的每一行保存当前系统版本号作为行版本号。
    3、DELETE
    InnoDB为删除的每一行保存当前系统版本号作为删除标志。
    4、InnoDB为插入一行新纪录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标志。

    使用这2个额外系统版本号,使大多数读操作都可以不用加锁。这使大多数读操作都可以不用加读锁。这样设计使得读数据操作更简单,性能更好,并且能保证读取的准确性。
    不足之处是需要额外的存储空间,需要做更多的行检查工作。MVCC只在不可重复读和可重复读两个隔离级别下工作。

    在文件系统中,MYSQL将每个数据库(也可称schema)保存为数据目录下的一个子目录。创建表时,MYSQL会在数据库子目录下创建一个和表同名的.frm文件保存表的定义。例如创建
    一个名为MyTable的表,MYSQL会将MyTable.frm文件中保存该表的定义。因为MYSQL使用文件系统的目录和文件来保存数据库和表的定义,大小写敏感和具体的平台有关。
    查询表的基本信息:

    select * from information_schema.TABLES where information_schema.TABLES.TABLE_SCHEMA = '库名' and information_schema.TABLES.TABLE_NAME = '表名';

    Name:表名
    Engine:表的存储引擎类型。
    Row_format:行的格式。对于MyISAM表,可选的值为Dynamic、Fixed、Compressed。Dynamic的长度是可变的,一般包含可变长度的字段,比如VARCHAR或BLOB。Fixed的行长度则是
    固定的,只包含固定长度的列,如CHAR、INTEGER。Compressed的则只在压缩表中存在。
    Rows:表中的行数。对于MyISAM和其他一些引擎,该值是精确的,但对于InnoDB,该值是估计值。
    Avg_row_length:平均每行包含的字节数。
    Data_length:表数据的大小(以字节为单位)。
    Max_data_length:表的最大容量,该值和存储引擎有关。
    Index_length:索引的大小(以字节为单位)。
    Data_free:对于MyISAM表,表示已分配但目前没有使用的空间。这部分空间包括了之前删除的行,以及后续可以被INSERT利用到的空间。
    Auto_increment:下一个AUTO_INCREMENT的值。
    Create_time:表的创建时间。
    Update_time:表的最后修改时间。
    Check_time:使用CHECK TABLE命令或者工具最后一次检查表的时间。
    Collation:表的默认字符集和字符排列规则。
    Checksum:如果开启,保存的是整个表的实时校验和。
    Create_options:创建表指定的其他选项。
    Comment:该列包含了一些其他的额外信息。

    展开全文
  • 1.mysql架构介绍 mysql的架构大致可划分为四层: ...原子性(automicity):一个事务必须被看作一个不可分割的最小单元。对于事务里的操作要么全部成功,要么全部失败,不可能执行其中一部分。这就是事...
  • 一、事务 1. 事务的概念 事务是不可分割的最小工作单位。 所谓事务是用户定义的一个数据操作序列,这些操作可作为一个完整的工作单元,要么全部执行,要么全部不执行,是一个不可分割的工作...二、并发控制 ...
  • 4. java并发编程基础 1. 进程和线程 什么是进程: ...线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程...
  • 简介 线程(thread)是操作系统能够进行运算调度的最小单位...一个进程的最小执行单元就是线程。 系统中的进程线程模型是这样的: 进程从操作系统获得基本的内存空间,所有的线程共享着进程的内存地址空间。当...
  • 现代操作系统调度的最小单元是线程,也加轻量级进程,在一个进程里可以创建多个线程,这些线程拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。 使用多线程的原因 更多的处理器核心 更...
  • 线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(有一个程序计数器,它的作用是存放下一条指令所在单元的地址的地方),寄存器集合...
  •  事务是最小的逻辑执行单元,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务具有四个重要特性,即原子性(Atomicity)、一致性(Consistency)、隔离性 ...
  • linux:16线程并发

    2019-11-30 16:59:10
    线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 2....
  • 数据库事物并发机制

    2019-05-27 13:50:55
    事务是最小的逻辑执行单元,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。 事务具有四个重要特性,即原子性(Atomicity)、一致性(Consistency)、隔离性 ...
  • 并发学习(一)

    2020-12-21 06:30:35
    进程概念:进程是程序的一次执行过程,是系统程序运行的基本单位,也是资源分配的最小单位。进程是动态的,系统允许一个程序即是一个进程从创建到销毁的过程。 线程概念:线程与进程相似,但线程是比进程更小的执行...
  • 线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。那个时候为了考试我...
  • ​ 进程:正在进行中的程序,程序的一次执行,线程引入之前,进程是资源持有的最小单位,也是程序运行的最小单元。 ​ 线程:就是进程中一个负责程序执行的控制单元(执行路径)。 ​ 进程间通信方式有: 消息传递 ...
  • Python并发之多线程threading(1)

    万次阅读 2018-10-11 22:39:08
    线程是应用程序中工作的最小单元,它被包含在进程之中,是进程中的实际运作单位。 当不同的线程需要操作共享数据时,当两个或以上对共享内存的操作发生在并发线程中,并且至少有一个可以改变数据,又没有同步机制的...
  • 本文章主要总结Java多线程/高并发编程的知识点,供自己学习用。 一、基础知识 ...即进程中的一个执行任务(控制单元),负责当前进程中程序的执行。线程是cpu调度的最小单位。 一个进程至少有一个线程,一...
  • 线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(有一个程序计数器,它的作用是存放下一条指令所在单元的地址的地方),寄存器集合...
  • 线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(有一个程序计数器,它的作用是存放下一条指令所在单元的地址的地方),寄存器集合...
  • 二、线程的简介线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(有一个程序计数器,它的作用是存放下一条指令所在单元的地址的地方),...
  • 为了有效利用多核处理器的优势,将进程进一步细分,允许一个进程里存在多个线程,这多个线程还是共享同一片内存空间,但cpu调度的最小单元变成了线程。那协程又是什么东西,以及与线程的差异性?协程,可以看作是轻量...
  • 事务控制

    2020-01-30 13:59:40
    DBS运行的最小逻辑工作单元是“事务”,所有对数据库的操作,都要以事务作为一个整体单位来执行或撤销。 事务必须满足的ACID属性: 1.原子性:不可分割 2.一致性:数据一致状态 3.隔离性:避免并发事务相互干扰 4....
  • 在Go语言中,每一个并发的执行单元叫作一个goroutine(协程)。如果你使用过操作系统或者其它语言提供线程,那么你可以简单地把goroutine类比作一个线程,但实际上两者有本质区别。 一. 概念 1.1 协程和线程区别...

空空如也

空空如也

1 2 3 4 5 ... 18
收藏数 354
精华内容 141
关键字:

并发控制的最小单元