【小资说库】第10期 数据库事务的隔离性保证机制-事务隔离级别底层的控制机制。
上一期我们聊到了事务的脏读、不可重复读、幻读及隔离级别。实现这些隔离级别的底层机制是什么呢?这一期我们来扒一扒。
并发控制的主要技术有封锁(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
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机制介绍可参考下面的两篇博客。
总结
经过上面的多方查阅和梳理,我们可以做如下总结。
控制数据库事务并发控制的机制有悲观控制法和乐观控制法。
悲观控制法对应的是数据库的锁机制。悲观锁只需要用户或应用程序设置了DBMS所采用的隔离级别,DBMS后台就会通过锁来实现隔离级别,避免脏读、不可重复读或幻读。
乐观锁可以由应用程序通过给数据库表加version字段或时间戳的方式实现,也可以由数据库的MVCC机制实现。
留个疑问在本期,待后续考证:数据库既支持锁机制,也支持MVCC机制,那如何设置来选择这两种不同的机制呢?
- 点赞
- 收藏
- 关注作者
评论(0)