之前有个朋友问我,数据库如何保证的一致性?
我:这么小白的问题都不知道??
于是就绝交了。。
如果在分布式环境下讨论事务还是比较复杂的。
所以,先从本地事务开始聊一波。所谓本地事务,就是单数据源的事务场景。
事务的 ACID 都知道,分别是:
原子性(Atomicity):事务包含的所有操作要么全部成功,要么全部失败回滚到事务前的状态。
一致性(Consistency):事务前后的状态都是一致的,比如A给B转账,转账前两人账户总和是1千元,那转账之后也得是1千元。
隔离性(Isolation):事务之间相互隔离,数据读写不会互相干扰。
持久性(Durability):事务一旦成功提交,数据将会被修改,不会丢失。
需要注意的是,这4个特性,C 是目的,AID 都是手段!!
所以,重点需要分析的是 AID,
其中 AD 是紧密相连的,没有持久性就没有原子性。
持久化主要依赖磁盘,而写磁盘这个操作本事不是原子的,比如,可能存在写了一半机器断电的情况,这对一个事务来说就是中间的一种状态了。
一般解决办法有两种:
Commit Logging
首先,事务提交时需要记录更多的信息,比如数据修改前是什么,修改后是什么,数据在磁盘的哪个位置等等
其次,标记状态,标记事务当前处于什么状态,“已提交”还是“已提交未完成”
最后,故障恢复时根据前面记录的信息就可以对事务进行恢复了。
Shadow Paging
修改数据时,先 Copy 一份影子数据去修改,修改完成后再修改数据指向的位置,这步操作本身就是原子的。但是这种方案并发能力比较有限,所以一般不会使用。
Commit Logging 也是有问题的,它的问题在于,只有事务提交了并且标记好“已提交”的事务状态后才允许实际修改数据。
这个问题就大了,这是一种阻塞式操作,对数据库的性能影响很大。
于是就得想办法解决这个问题,仔细想想,Commit Logging 之所以有上述限制,也是可以理解的,如果允许提前修改数据,数据库崩溃后这个被修改的数据就成为错误数据了。
那假如有办法让这个被修改的数据回滚到被修改前的状态,这个问题就解决了。
没错了,这个就是 Write-Ahead Logging 的解题思路了。
在 Write-Ahead Logging 里,Commit Logging 记录的日志叫做 Redo Log,个人觉得翻译成续做日志更合适点。
在这个基础上又加了一个Undo Log,回滚日志,就是为了解决刚刚 Commit Logging 存在的那个问题。
有了 Undo Log,就可以提前修改数据,不用担心找不到回家的路了。
Write-Ahead Logging 在崩溃恢复时会经历三个阶段,
首先,看看哪些事务是没有正常完成的
然后,对其中那部分已提交但没有正常完成的事务,根据 Redo Log 执行续作。
最后,剩下的那部分就是需要回滚的事务了,根据 Undo Log 执行回滚。
上面说的是数据库如何实现的原子性和持久性,继续看看数据库如何实现的隔离性
这个需要锁来保证,一般数据库提供了三种锁:
锁类型 | 说明 |
写锁 | 也叫排它锁,如果一个事务加了写锁,那其它事务既不能加写锁,也不能加读锁。就是告诉其它事务,坑我已经占了,你再等等。 |
读锁 | 也叫共享锁,如果一个事务加了读锁,其它事务也可以加读锁,但是不能加写锁。就是告诉其它事务,是兄弟(读锁)就过来,不然(写锁)就等等吧。 |
范围锁 | 范围锁与其它锁的区别在于,一般锁锁住的是具体几行记录,而范围锁锁住的则是一个范围,比如 amount < 10 的这个区间被锁住后,这个区间不仅不能修改数据,也不能增删数据。 |
数据库的隔离级别一般会提供多种,方便使用者根据实际情况进行调整。
隔离级别 | 问题 |
可串行化 通过加范围锁避免幻读 | |
可重复读 加上读锁不释放(贯穿事务生命周期),阻塞其它事务的写锁,从而避免不可重复度问题 | 幻读 在当前事务里,可能被其它事务的增删影响到,两次读到的记录数量不一致 |
读已提交 加上读锁,读完释放,所以无法解决可重复读问题 | 不可重复读 在当前事务里,可能被其它事务的修改影响到,两次读到的数据状态不一致 |
读未提交 因为不加读锁,不受约束,另一个事务加上写锁未提交的数据也能读到 | 脏读 在当前事务里,可能读取到另一个事务未提交的数据 |
上面说的都是一个事务读被另一个事务写影响到隔离性的情况,针对这种情况,有一种“多版本并发控制”的优化方案。
MVCC 就是说,现在一条记录有多个版本了,这个版本号用的是事务ID,是全局有序递增的。
如果是可重复读的隔离级别,读版本号小于等于当前事务id的最大的那条数据就行了,
如果是读已提交的隔离级别,读版本号最大的那条记录。
需要注意,MVCC 是对读+写的优化,对其它场景不适用。
有了 AID 的保证,C 其实就不是问题了。
从此,友谊的小船又可以扬帆起航了。。
欢迎关注我微信公众号《倔强的文哥》(一个表面冷酷,内心热乎的互联网码农),不定时分享各种Java技术经验、面试热题、Python实用小技巧。