【小资说库】第10期 数据库事务的隔离性保证机制-事务隔离级别底层的控制机制。

举报
灵犀晨 发表于 2020/08/12 15:43:55 2020/08/12
【摘要】 上一期我们聊到了事务的脏读、不可重复读、幻读及隔离级别。实现这些隔离级别的底层机制是什么呢?这一期我们来扒一扒。并发控制的主要技术有封锁(locking)(编者注:是指悲观锁)、时间戳(timestamp)、乐观控制法(optimistic sheduler)(编者注:是指乐观锁)和多版本并发控制(multi-version concurrency control,MVCC)等。封锁(悲观锁...

上一期我们聊到了事务的脏读、不可重复读、幻读及隔离级别。实现这些隔离级别的底层机制是什么呢?这一期我们来扒一扒。


并发控制的主要技术有封锁(locking)(编者注:是指悲观锁)、时间戳(timestamp)、乐观控制法(optimistic sheduler)(编者注:是指乐观锁)和多版本并发控制(multi-version concurrency control,MVCC)等。

封锁(悲观锁)

锁的共存关系

封锁是实现并发控制的一个非常重要的技术。基本的封锁类型有两种:排他锁(X锁)和共享锁(S锁)

排它锁又称写锁,若事务T1对数据对象A加上X锁,则只允许T1读取和修改A,其他任何事务都不能再对A加任何类型的锁直到T1释放A上的锁为止。

共享锁又称读锁,若事务T1对数据对象A加上S锁,则事务T1可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁。

锁间的共存关系如下表所示:(x表示是排它锁(Exclusive),s表示共享锁(Share),-表示不加锁。Y表示可以共存,N表示不能共存)

上图表示可以共存的锁,如,第二行表示,一个事务T1给某数据加了X锁,则事务T2就不能再给那数据加X锁了,同时也不能再加S锁了,只有到T1事务提交完成之后,才可以。默认来说,当sql脚本修改更新某条记录的时候,会给该条记录加X锁,读的话加的是S锁。


封锁协议

在运用 排他锁 和 共享锁 对数据对象加锁时,还需要约定一些规则,例如何时申请 排他锁 或 共享锁、持锁时间、何时释放等。称这些规则为封锁协议(Locking Protocol)。对封锁方式规定不同的规则,就形成了各种不同的封锁协议。不同的封锁协议对应不同的隔离级别。

在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同:

A. Read uncommited:允许脏读取,但不允许丢失修改。

   对应一级封锁协议:事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。 

B. Read Committed:允许不可重复读取,但不允许脏读取和丢失修改。这可以通过“瞬间共享读锁”和“排他写锁”实现。

对应二级封锁协议:一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后即可释放S锁(瞬间S锁)。

C. 可重复读取(Repeatable Read):禁止不可重复读取和脏读取和丢失修改,但是有时可能出现幻影数据。这可以通过“共享读锁”和“排他写锁”实现。

对应三级封锁协议:一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放

D. 序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。

 四级封锁协议是对三级封锁协议的增强,其实现机制也最为简单,直接对事务中所读取或者更改的数据所在的表加表锁,也就是说,其他事务不能读写该表中的任何数据。这样所有的脏读,不可重复读,幻读,都得以避免!


活锁&死锁

并发控制会造成活锁和死锁。

活锁指的是T1封锁了数据R,T2同时也请求封锁数据R,T3也请求封锁数据R,当T1释放了锁之后,T3会锁住R,T4也请求封锁R,则T2就会一直等待下去。解决这种活锁等待的处理办法是采用“先来先服务”策略。


死锁就是我等你,你又等我,双方就会一直等待下去,比如:T1封锁了数据R1,正请求对R2封锁,而T2封住了R2,正请求封锁R1,这样就会导致死锁。

死锁这种没有完全解决的方法,只能尽量预防。预防的方法有:①一次封锁法,指的是一次性把所需要的数据全部封锁住,但是这样会扩大了封锁的范围,降低系统的并发度;②顺序封锁法,指的是事先对数据对象指定一个封锁顺序,要对数据进行封锁,只能按照规定的顺序来封锁,但是这个一般不大可能的。

另外,系统如何判断出现死锁呢,毕竟出现死锁不能一直干等下去,要及时发现死锁同时尽快解决出现的死锁,诊断和判断死锁有两种方法,一是超时法,二是等待图法。超时法就是如果某个事物的等待时间超过指定时限,则判定为出现死锁;等待图法指的是如果事务等待图中出现了回路,则判断出现了死锁。对于解决死锁的方法,只能是撤销一个处理死锁代价最小的事务,释放此事务持有的所有锁,同时对撤销的事务所执行的数据修改操作必须加以恢复。


行级锁&表级锁

最后,说下行级锁和表级锁。

行级锁是一种排他锁,防止其他事务修改此行;在使用以下语句时,Oracle会自动应用行级锁:INSERT、UPDATE、DELETE、SELECT … FOR UPDATE [OF columns] [WAIT n | NOWAIT]; SELECT … FOR UPDATE语句允许用户一次锁定多条记录进行更新 使用COMMIT或ROLLBACK语句释放锁


表级锁又分为5类:

行共享 (ROW SHARE) – 禁止其他事务使用排他锁锁定表。

行排他(ROW EXCLUSIVE) – 禁止其他事务使用排他锁和共享锁 

共享锁(SHARE) - 锁定表,对记录只读不写,多个用户可以同时在同一个表上应用此锁。

共享行排他(SHARE ROW EXCLUSIVE) – 比共享锁更多的限制,禁止其他事务使用共享锁及更高的锁。 

排他(EXCLUSIVE) – 限制最强的表锁,仅允许其他用户查询该表的行。禁止修改和锁定表。


以上内容参考自:


http://blog.sina.com.cn/s/blog_548bd2090100ir7k.html

https://www.cnblogs.com/ismallboy/p/5574006.html


乐观控制法(乐观锁)

https://www.jianshu.com/p/d2ac26ca6525

https://blog.csdn.net/xlgen157387/article/details/47906553

上面两篇帖子对悲观锁和乐观锁的概念介绍、使用场景及对比介绍的比较全面了,大家可以查阅。


mysql乐观锁和悲观锁详解这篇贴中也通过MySQL的举例对悲观锁和乐观锁进行了生动说明,其中有我们比较喜欢的总结信息:

乐观锁,简单地说,就是应用系统层面上做并发控制,去加锁。
实现乐观锁常见的方式:版本号version

应用程序在数据表中增加版本号字段,每次对一条数据做更新之前,先查出该条数据的版本号,每次更新数据都会对版本号进行更新。在更新时,把之前查出的版本号跟库中数据的版本号进行比对,如果相同,则说明该条数据没有被修改过,执行更新。如果比对的结果是不一致的,则说明该条数据已经被其他人修改过了,则不更新,客户端进行相应的操作提醒。


悲观锁,简单地说,就是从数据库层面上做并发控制去加锁。
悲观锁的实现方式有两种:共享锁(读锁)和排它锁(写锁)

时间戳

时间戳就是在数据库表中单独加一列时间戳,比如“TimeStamp”,每次读出来的时候,把该字段也读出来,当写回去的时候,把该字段加1,提交之前 ,跟数据库的该字段比较一次,如果比数据库的值大的话,就允许保存,否则不允许保存,这种处理方法虽然不使用数据库系统提供的锁机制,但是这种方法可以大大提高数据库处理的并发量,因为这种方法可以避免了长事务中的数据库加锁开销(操作员A 和操作员B操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。

这样看来时间戳也是一种乐观控制法。

查询了一些其他博主的博客,也佐证了时间戳是实现乐观锁的一种办法这个观点。部分博客贴在下面。

https://blog.csdn.net/crazy_scott/article/details/90452113

https://blog.csdn.net/sanshi_lxl/article/details/83740635

https://blog.csdn.net/webdesman/article/details/6360633?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-9.add_param_isCf&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-9.add_param_isCf


MVCC

实现数据库的并发访问控制,最简单的方式就是加锁访问。由于加锁会将读写操作串行化,所以不会出现不一致的状态。但是,读操作会被写操作阻塞,大幅降低读性能。MVCC(Multiversion Concurrency Control多版本并发控制)能解决采用锁带来的写操作堵塞读操作的并发问题。MVCC是通过使用数据的多个版本保证并发读写不冲突的一种机制。不同的数据库会有不同的实现。(参见https://www.jdon.com/repository/database-mvcc.html )

MVCC的两种不同实现方式

第一种实现方式是将数据记录的多个版本保存在数据库中,当这些不同版本数据不再需要时,垃圾收集器回收这些记录。这个方式被PostgreSQL和Firebird/Interbase采用,SQL Server使用的类似机制,所不同的是旧版本数据不是保存在数据库中,而保存在不同于主数据库的另外一个数据库tempdb中/

第二种实现方式只在数据库保存最新版本的数据,但是会在使用undo时动态重构旧版本数据,这种方式被Oracle和MySQL/InnoDB使用。


有关MySQL和PG的MVCC机制介绍可参考下面的两篇博客。

Mysql中MVCC的使用及原理详解

postgresql系列_MVCC机制以及锁机制理解


总结

经过上面的多方查阅和梳理,我们可以做如下总结。

  • 控制数据库事务并发控制的机制有悲观控制法和乐观控制法。

  • 悲观控制法对应的是数据库的锁机制。悲观锁只需要用户或应用程序设置了DBMS所采用的隔离级别,DBMS后台就会通过锁来实现隔离级别,避免脏读、不可重复读或幻读。

  • 乐观锁可以由应用程序通过给数据库表加version字段或时间戳的方式实现,也可以由数据库的MVCC机制实现。


留个疑问在本期,待后续考证:数据库既支持锁机制,也支持MVCC机制,那如何设置来选择这两种不同的机制呢?

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。