锁管理器
为了解决这个问题,多数数据库使用锁和/或数据版本控制 。这是个很大的话题,我会集中探讨锁,和一点点数据版本控制 。
悲观锁
原理是:
- 如果一个事务需要一条数据
- 它就把数据锁住
- 如果另一个事务也需要这条数据
- 它就必须要等第一个事务释放这条数据
这个锁叫排他锁 。
共享锁是这样的:
- 如果一个事务只需要读取数据A
- 它会给数据A加上『共享锁』并读取
- 如果第二个事务也需要仅仅读取数据A
- 它会给数据A加上『共享锁』并读取
- 如果第三个事务需要修改数据A
- 它会给数据A加上『排他锁』,但是必须等待另外两个事务释放它们的共享锁 。
文章插图
锁管理器是添加和释放锁的进程,在内部用一个哈希表保存锁信息(关键字是被锁的数据),并且了解每一块数据是:
- 被哪个事务加的锁
- 哪个事务在等待数据解锁
但是使用锁会导致一种情况,2个事务永远在等待一块数据:
文章插图
在本图中:
- 事务A 给 数据1 加上排他锁并且等待获取数据2
- 事务B 给 数据2 加上排他锁并且等待获取数据1
在死锁发生时,锁管理器要选择取消(回滚)一个事务,以便消除死锁 。这可是个艰难的决定:
- 杀死数据修改量最少的事务(这样能减少回滚的成本)?
- 杀死持续时间最短的事务,因为其它事务的用户等的时间更长?
- 杀死能用更少时间结束的事务(避免可能的资源饥荒)?
- 一旦发生回滚,有多少事务会受到回滚的影响?
哈希表可以看作是个图表(见上文图),图中出现循环就说明有死锁 。由于检查循环是昂贵的(所有锁组成的图表是很庞大的),经常会通过简单的途径解决:使用超时设定 。如果一个锁在超时时间内没有加上,那事务就进入死锁状态 。
锁管理器也可以在加锁之前检查该锁会不会变成死锁,但是想要完美的做到这一点还是很昂贵的 。因此这些预检经常设置一些基本规则 。
两段锁
实现纯粹的隔离最简单的方法是:事务开始时获取锁,结束时释放锁 。就是说,事务开始前必须等待确保自己能加上所有的锁,当事务结束时释放自己持有的锁 。这是行得通的,但是为了等待所有的锁,大量的时间被浪费了 。
更快的方法是两段锁协议(Two-Phase Locking Protocol,由 DB2 和 SQL Server使用),在这里,事务分为两个阶段:
- 成长阶段:事务可以获得锁,但不能释放锁 。
- 收缩阶段:事务可以释放锁(对于已经处理完而且不会再次处理的数据),但不能获得新锁 。
文章插图
这两条简单规则背后的原理是:
- 释放不再使用的锁,来降低其它事务的等待时间
- 防止发生这类情况:事务最初获得的数据,在事务开始后被修改,当事务重新读取该数据时发生不一致 。
多说几句
当然了,真实的数据库使用更复杂的系统,涉及到更多类型的锁(比如意向锁,intention locks)和更多的粒度(行级锁、页级锁、分区锁、表锁、表空间锁),但是道理是相同的 。
我只探讨纯粹基于锁的方法,数据版本控制是解决这个问题的另一个方法 。
版本控制是这样的:
- 每个事务可以在相同时刻修改相同的数据
- 每个事务有自己的数据拷贝(或者叫版本)
- 如果2个事务修改相同的数据,只接受一个修改,另一个将被拒绝,相关的事务回滚(或重新运行)
推荐阅读
- 如何挑选黑豆
- 不是夫妻合租房犯罪吗
- 医保断缴三个月余额清零?员工可自愿放弃社保?这些谣言别再信了
- 手串颗数大有讲究,千万别戴错了
- “禁止长时间停车”,到底指的是几分钟?交警:最后再说一遍
- 太热了!别再披头散发了,这4款发型够美够清凉
- 没钱还信用卡有什么解决办法
- 信用卡过期卡怎么处理
- 信用卡逾期被注销怎么还钱
- 如何找回抖音私聊记录