面试官内心os:原来数据库事务是这样的

Original Video ContentExpand Video
  • 了解数据库事务的定义及其解决的问题
  • 事务的四大特性及实现方式
  • 经典转账场景分析,以及事务的原子性
  • 事务的隔离性及其实现
  • 四种隔离级别的详细解析
  • MVCC的基本思想和实现方式

信不信我可以几分钟就交费你数据库事务,数据库事务无非就是要解决一下几个问题:
啥叫数据库事务?
为啥要有事务?
事务的四大特性都是啥?
怎么实现的并发事务会带来什么问题?
怎么解决这些问题?
事务的隔离级别都有啥?
怎么实现这些隔离级别的?

看一个经典的转账场景,
A给B转账100块钱,你就得写两个操作:
B账户增加100块钱,
A账户减少100块钱。
那给B账户加钱成功了,但是A账户减钱的时候发现约不如失败了,那B不就平白乌乌多加了100块钱吗?

在这个转账场景中,这两个操作应该是一个整体,
要么都成功,要么都失败,一个成功了一个失败了,那数据就有问题了,
所以就需要有数据库事务。

所谓事务,就是一系列操作构成一个逻辑上的整体,
要么全部成功,要么全部失败。
那对刚才的例子加上事务:
如果说B账户增加100块钱执行成功了,
那A账户减少100块钱执行失败了,那就回滚,对B账户的所有操作。
把这两个操作就变成了一个不可分割的整体,要么全成功,要么全失败。
不会说存在一个没有执行成功的中间状态,这就是事务的原子性

原子性怎么实现的呢?可以通过on log回滚日志去实现。
比如说N个连续的操作失败了一个,通过on log给前面的操作全部回滚。
那事务提交之后如果数据库崩溃了,那数据丢失了怎么办?
所以还要保持事务的一个持久性

只要事务一提交,无论发生什么事,数据库的修改都不丢失。
持久性怎么实现的呢?通过redo log来实现。
每一次事务提交之后,就把这个修改写入redo log。
那即便数据没有持久化到磁盘中,也能读取redo log来恢复数据,
所以redo log其实保证了事务的持久性。
这也就是为啥说redo log让数据库有崩溃恢复的能力。

那就一定有人在想,
redo log自己本身也要持久化到磁盘中,他也是要刷盘的,
那要是数据库崩了,redo log也刷盘失败了,那数据不就丢了?
这个问题后面讲到日志再详细去聊。

在并发执行事务的时候,如果说事务A把商品库存从1修改为2,
事务B读取发现是准备扣点库存了,
但是事务A回滚了,又把商品库存回滚到1了,
那事务B再做扣点库存的操作不就会出问题吗?
这是脏读,一个事务读取到了另外一个事务没有提交的数据。
所以在并发执行事务的时候,多个事务之间应该相互隔离,
互不影响,否则那就会出问题,这就是事务的隔离性

那隔离性怎么实现呢?需要用MVCC(多版本并发控制)、加锁、去Pace Lock去实现,
那这就比较复杂了,后面就详细聊。
除了这个原子性、持久性、隔离性之外,事务还要保证一致性

对于刚才的这个转账场景来说,无论事务成功转账,
转账人和收款人的钱的总数应该是保持不变的。
如果说转账前后两个人一共100块钱,转账之后两个人总额变300了,那就有问题了。
那一致性怎么实现呢?其实只要保证了原子性、持久性、隔离性,一致性自然就实现了。

事务的并发执行会带来很多问题,刚才讲隔离性提到了脏读,
一个事务读取到其他事务未提交的数据。那么问题又来了:
当事务第一次读取为1,然后事务B修改为2,事务B提交,
然后事务A第二次读取的时候发现是2了,
那么同一个事务中前后两次读取的数据不一致。这就是不可重复读

那么问题又又来了:当事务第一次做范围查询的时候,
他查询ID大于100的数据有10条,然后事务B又插入了20条数据,
那事务A再范围查询发现ID大于100的有30条了,这就是幻读
幻读和不可重复读挺像的,但不可重复读强调的是多次读取的数据不一样,
幻读强调的是数据条数的一个增减。

怎么解决脏读、不可重复读、幻读的这些问题呢?
通过事务的隔离级别,数据库提供的四种隔离级别:

  1. 读未提交
  2. 读已提交
  3. 可重复读
  4. 串行化

读未提交是最低的一种隔离级别,每个事务都能看到其他事务未提交的数据。
读未提交它不能解决脏读、不可重复读、幻读的问题,
所以实际根本就不会用读未提交。
就是说每个事务只能读到其他事务已经提交的数据,这就可以解决脏读的问题,
因为其他事务没提交的你读不到,但它无法解决不可重复读和幻读的问题。

可重复读是数据库的一个默认的隔离级别。
可重复读的要求是,当你第一次读取的时候会生成一份数据快照。
生成快照之后,其他事务的修改对当前事务来说就是不可见的,
因为你每次都读这个快照,就能保证在同一个事务内两次读到的数据其实是一样的,
那就解决不可重复读的问题。
你重复读它也不会读到已经变了的那些数据,但它还是无法解决幻读的问题。

串行化是数据库效果最高的一个隔离级别。
串行化的要求是,事务不能并发执行,只能一个接一个的顺序执行,
你都无法并发执行了,那那些并发问题自然就不存在了,
所以串行化是可以解决所有的并发问题,但性能很差。
并发量高的情况下可能就会导致大量的超时和锁竞争的问题,
通常根本就不会用这个隔离级别。

所以可以看到,事务的隔离级别越高就越能保证数据的一致性和完整性,
执行效率也越低;
事务的隔离级别越低,执行的效率也就越高,但数据的一致性就越差。
那现在有这四种隔离级别能解决问题了,
那怎么实现这几种隔离级别呢?

首先,读未提交应该怎么实现?
不用实现,你根本就不用管,因为多个并发事务同时执行天然就是读未提交,
所以根本就不用管。但写的时候还是要加锁的,
你不能同时写一个数据。

那串行化应该怎么实现的?这就很简单了,你直接加锁就行了,
只有拿到锁的事务才能执行这样事务就串行化执行了。

那读已提交、可重复读应该怎么实现的?这就涉及到很多复杂的东西,
MVCC(多版本并发控制)是一个非常复杂的概念,也是一项很重要的技术,
所以下一期我会详细讲解。

MVCC就是维护数据的多个版本,比如说某个数据第一个版本是1,
然后有事务修改为了2,又产生了第二个版本;
又有事务修改成为3,又产生了第三个版本。
那问题来了,现在有个事务它需要读这个数据,那它读到的是哪个版本?
是版本1、版本2还是版本3?
那它读到哪个版本取决于READ VIEW

每次读取的时候会生成一个快照,叫READ VIEW
READ VIEW主要是用来判断数据版本对当前事务的可见性,
通过READ VIEW就能在一堆数据版本中找到哪个版本对当前事务是可见的。
你能读哪个版本,然后把这个版本的数据给它返回。
这就是MVCC的一个基本思想。

讲完MVCC后,来看看可重复读和读已提交这两个隔离级别到底是怎么实现的。
可重复读怎么实现的呢?可重复读的意思是:
每次会读到和第一次相同的数据,即便后面有新事务修改的数据,
还是会读到和第一次相同的数据,所以这就解决了不可重复读的问题。
那么问题来了,怎么每次让它都能读到相同的数据?

可重复读第一次查询时,它会生成一个READ VIEW。
刚才说过,READ VIEW是判断数据版本对当前事务是否可见的。
所以可重复读每次都会附用第一次生产的READ VIEW,也就是说你生成READ VIEW的时候,
哪些版本的数据对当前事务可见就已经固定下来了,
后续每次通过这个同一个READ VIEW去判断数据的可见性,
那你能看到的数据版本都是一样的,所以每次读到的都是同一个数据版本,
无论其他事务再怎么改,你看不到。

这就解决了不可重复读的问题,所以可重复读就是通过MVCC,
加重复使用第一次的READ VIEW去实现的。
除此之外,可重复读为了避免幻读问题还要加锁,这下期再聊吧!
这个就比较复杂了。

再看一下读已提交是怎么实现的,读已提交就是说事务只能读取到其他事务已经提交的数据。
只要其他事务提交了,你就能读到,
读已提交的本质就是每次查询都会生成一个新的READ VIEW,
每次读都形成新的READ VIEW,每次就能读到最新的已经提交的数据版本。

对并发编程、学习、找工作有任何疑问可以进入主页点击小电或者充电问答查看哦。