数据库学习之(4)理解数据库的事务及隔离级别

本篇讲诉数据库中事务的四大特性(ACID),并且将会详细地说明事务的隔离级别。

 一、数据库事务正确执行的四个基本要素

   如果一个数据库声称支持事务的操作,那么该数据库必须要具备以下四个特性:

⑴ 原子性(Atomicity)

  原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

⑵ 一致性(Consistency)

  一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。

  拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。

⑶ 隔离性(Isolation)

  隔离性:事务相互隔离,当多个事务并发执行时,任一事务的更新操作直到其提交的整个过程,对其他事务都是不可见的。

  隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。

  即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。

  关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。

⑷ 持久性(Durability)

  持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。因系统重启而丢失数据会破坏持久性。

  例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务以及正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成,否则就会造成我们看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。  

二、事务并发处理带来的问题

   以上介绍完事务的四大特性(简称ACID),现在重点来说明下事务的隔离性,当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性,在介绍数据库提供的各种隔离级别之前,我们先看看事务并发处理带来的问题:

1、脏读Dirty read(不符合一致性

  脏读是指在一个事务处理过程里读取了另一个已经修改但未提交的事务中的数据。

  例如,事务A对数据进行了修改,但是还没有提交,这时事务B读取这个数据,然后事务A回滚,那么事务B取的数据无效。

  总结:一个事务读到了另一个事务未提交的修改数据,稍后改数据因为事务的回滚而无效。

2、不可重复读NonRepeatable Read(不符合隔离性

  不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。

  例如事务T1在读取某一数据,而事务T2立马修改了这个数据并且提交事务给数据库,事务T1再次读取该数据就得到了不同的结果,发生了不可重复读。

  不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

  在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。

  总结:一个事务两次读同一个数据中间,该数据被另一事务所修改。

3、虚读(幻读)Phantom Read(不符合隔离性

  幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读。

  幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数,针对的insert操作)。

  总结:事务两次读中间被插入或删除了记录,造成两次读到的记录数不同。

4、更新丢失(Update lose)

   更新丢失(Update lose):两个事务同时操作相同数据,后提交的事务会覆盖先提交的事务处理结果。通过乐观锁就可以解决。

  总结:一个事务对数据的修改被另一个事务对数据的修改所覆盖,相当于该事务未执行。

三、数据库事务隔离级别

   现在来看看MySQL数据库为我们提供的四种隔离级别:

  ① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。

  ② Repeatable read (可重复读):可避免脏读、不可重复读的发生。

  ③ Read committed (读已提交):可避免脏读的发生。

  ④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。

1、Read uncommitted(读未提交)(可能造成脏读)

  公司发工资了,领导把5000元打到singo的账号上,但是该事务并未提交,而singo正好去查看账户,发现工资到账5000元整,非常高兴。可是不幸的是,领导发现发给singo的工资金应该是2000元,于是迅速回滚了事务(将5000元回滚),修改金额后(修改为2000元),将事务提交,最后singo实际的工资只有 2000元,singo空欢喜一场。
  出现上述情况,即我们所说的脏读 ,两个并发的事务,“事务A:领导给singo发工资”,“事务B:singo查询工资账户”,事务B读取了事务A尚未提交的数据。

  当隔离级别设置为Read uncommitted(读未提交)时,就可能出现脏读,如果我们此时将隔离级别提升为Read committed(读已提交),便可避免脏读。

2、Read committed(读已提交)(可能造成不可重复读)

  singo拿着工资卡去消费,系统读取到卡里确实有2000元,而此时她的老婆也正好在网上转账,把singo工资卡的2000元转到另一账户,并在 singo之前提交了事务,当singo扣款时,系统检查到singo的工资卡已经没有钱,扣款失败,singo十分纳闷,明明卡里有钱,到底是啥情况呢?
  出现上述情况,即我们所说的不可重复读 ,两个并发的事务,“事务A:singo消费”、“事务B:singo的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
  当隔离级别设置为Read committed(读已提交)时,避免了脏读,但是可能会造成不可重复读(即不能读到相同的数据内容)。

  大多数数据库的默认级别就是Read committed(读已提交),比如Sql Server , Oracle,此时如果将隔离级别提升为Repeatable read(可重复读),可以避免脏读和不可重复读的发生。

3、Repeatable read(可重复读)(可能造成幻读)

  当隔离级别设置为Repeatable read(可重复读)时,可以避免不可重复读。
  有A、B两个会话,分别开启两个事务,B查看余额是100元钱,然后A向B转了500元钱,A 提交事务,B再去查看,发现依旧是100元钱,B只能结束当前事务,在开启一个新事务,才能查询到数据的变化,这样便避免了不可重复读。如果我们设置了Seriizable(序列化),就相当于锁表,某一时间内只允许一个事务访问该表。
  虽然Repeatable read避免了不可重复读,但还有可能出现幻读 。
  比如singo的老婆工作在银行部门,她时常通过银行内部系统查看singo的信用卡消费记录。有一天,她正在查询到singo当月信用卡的总消费金额 (select sum(amount) from transaction where month = 本月)为80元,而singo此时正好在外面胡吃海塞后在收银台买单,消费1000元,即新增了一条1000元的消费记录(insert transaction … ),并提交了事务,随后singo的老婆将singo当月信用卡消费的明细打印到A4纸上,却发现消费总额为1080元,singo的老婆很诧异,以为出现了幻觉,幻读就这样产生了。
  注:Mysql的默认隔离级别就是Repeatable read

4、Serializable(序列化)

  Serializable(序列化)是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读。

5、事务隔离级别总结

隔离级别 脏读(Dirty read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
读未提交(Read uncommitted) 可能 可能 可能
读已提交(Read committed) 不可能 可能 可能
可重复读(Repeatable read) 不可能 不可能 不可能
序列化(Serializable) 不可能 不可能 不可能

四、关于数据库对隔离级别的操作

  在MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read (可重复读);而在Oracle数据库中,只支持Serializable (串行化)级别和Read committed (读已提交)这两种级别,其中默认的为Read committed级别。

  在MySQL数据库中查看当前事务的隔离级别:

    select @@tx_isolation;

  在MySQL数据库中设置事务的隔离级别:

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

    set tx_isolation=’隔离级别名称;’

记住:设置数据库的隔离级别一定要是在开启事务之前!

  如果是使用JDBC对数据库的事务设置隔离级别的话,也应该是在调用Connection对象的setAutoCommit(false)方法之前。调用Connection对象的setTransactionIsolation(level)即可设置当前链接的隔离级别,至于参数level,可以使用Connection对象的字段:

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

五、封锁协议

1、封锁

  封锁就是事务T在对某个数据对象操作之前,先向系统发出请求对其加锁。加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其他事务不能更新此数据对象。基本的锁类型有两种:排他锁(又称写锁,X锁)和共享锁(又称读锁,S锁),此处再加一个更新锁和意向锁。

  • 排他锁(写锁,X锁):若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能在对A加任何类型的锁,直到T释放A上的锁为止。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。
  • 共享锁(读锁,S锁):若事务T对数据对象A加上S锁,则只允许T读A但不能修改A,其他事务只能再对A加S锁而不能加X锁,直到T释放A上的S锁为止。
  • 更新锁(U锁):更新锁在修改操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象。因为使用共享锁时,修改数据的操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为排它锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个事务申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为排它锁。这时,这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改前直接申请更新锁,在数据修改的时候再升级为排它锁,就可以避免死锁。
  • 意向锁:对多粒度树中的结点加意向锁,则说明该结点的下层结点正在被加锁;对任一结点加锁时,必须先对它的上层结点加意向锁。
  • 乐观锁:
  • 悲观锁:

2、封锁协议(*)

  在运用X锁和S锁这两种基本封锁对数据对象加锁时,还需要约定一些规则。例如,何时申请X锁或S锁、持锁时间、何时释放等。这些规则称为封锁协议。通常使用三级封锁协议来在不同程度上解决并发操作的不正确调度带来的丢失修改、不可重复读和读“脏”数据等不一致性问题。

  一级封锁协议
  一级封锁协议是指,事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。

  一级封锁协议可以防止丢失更新的问题。事务T1想要对数据R进行修改,那么先得加X锁。这时事务T2也想对数据R进行修改,那得先给数据R上X锁。但是由于数据R已经被上了X锁,此时事务T2无法对其进行上X锁,因此只能一直等待,直到数据R上的X锁被释放,才能给它上X锁,才进行修改操作。如此,解决了丢失更新的问题。

  二级封锁协议
  二级封锁协议是指,在一级封锁协议基础上增加事务T在读数据R之前必须先对其加S锁,读完后即可释放S锁。二级封锁协议出防止了丢失修改,还可以进一步防止读“脏”数据。

  三级封锁协议
  三级封锁协议是指,在一级封锁协议的基础上增加事务T在读数据R之前必须先对其加S锁,直到事务结束才释放。三级封锁协议出防止了丢失修改和读“脏”数据外,还可以进一步防止了不可重复读。

六、关于锁的一些知识点

1、锁的定义和分类

锁定义:
  锁是计算机协调多个进程和线程并发访问某一资源的机制。

锁分类:
  (1) 按性能分类:乐观锁和悲观锁。
  (2) 按操作分类:读锁和写锁。

    读锁:一个会话给表加了读锁,没有释放,那么当前会话及其他会话就无法写,只能读。
    写锁:一个会话给表加了写锁,没有释放,那么当前会话可以对表读和写,其他会话不能读也不能写。    
  (3) 按粒度分类:表锁和行锁。

    MyISAM在执行查询语句(SELECT)前会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的所有表加写锁。

    InnoDB行锁: 只对当前行加锁,一个会话开启事务,然后进行更新,另一个会话也要对这行数据进行更新,那么会等前面的会话提交才能更新。  

2、事务隔离与锁的关系

【隔离级别】与【并发下事务的问题】的关系:通过设置隔离级别,就相应的解决并发情况下带来的问题。

 【事务隔离】与【锁】的关系:事务隔离主要就是对事务的读写之间进行隔离,通过什么来隔离?通过锁来实现隔离

 【事务隔离级别】与【锁】的关系:通过对事务的读写操作加锁情况的不同,划分出不同的事务隔离级别

【锁】与【事务在并发下的问题】的关系:通过对事务的读写操作加锁,能解决事务在并发下的问题。

3、关于锁的例子

当隔离级别可重复读时:
  第一次查询会有快照,比如事务A更新了数据,但是事务B还没有执行查询操作,然后B查询,那么B查询的数据是事务A更新后的,以后每次查询,都是查询这次的快照。比如金额原先为100元,事务A将之更新为200,事务B查询的时候,是200,如果事务B先查询,事务A后更新,那么事务B查询到的还是100。但即使查询的数据是100(事务B已经更新成了200),如果事务A对金额进行加200的操作,那么结果是400,不是300,所以说,可重复读取不将查询结果(100)拿来当作变量运算的话,其执行结果是正确的。

幻读,也是在其他事务修改数据后,才进行读取,才能读到其他事务已提交的数据。

参考:https://blog.csdn.net/qq_32625839/article/details/82223098
https://www.cnblogs.com/fjdingsd/p/5273008.html

https://blog.csdn.net/weixin_30531261/article/details/79479895

原文地址:https://www.cnblogs.com/Harriett-Lin/p/10789402.html