并发事务正确性的准则 可串行化_《数据密集型应用系统设计》读书笔记 - 第七章 事务...

95537e0a3f757005a6010872864ba57f.png

这一章主要讲的是单机数据库系统中,多步骤事务的实现方式。

一、ACID

事务所提供的保证就是ACID,即下面四点:

原子性

  • 也可以叫做“可终止性”,指事务一旦中途被终止,可以回退到事务发生前的状态,而不是停留在中间态
  • 中间态不可被外部读取,否则就是“脏读”

一致性

  • 从某个有效状态开始,事务的操作如果符合约束条件,那么结果应该依然是有效状态
  • 比如银行转账,单个账户的余额和已转出金额要时刻保持一致,不能有中间态
  • 这点更多地需要在应用层面保证,而不是数据库本身

隔离性

  • 又叫做“可串行化”,指多个事务应该相互隔离
  • 即使多个事务底层是并行运行的,但外面看起来要像它们是串行运行的一模一样
  • 很少数据库用串行(强隔离)的方法,因为性能太差,大多数数据库都是用“弱隔离”的方式

持久性

  • 事务一旦提交成功,那么数据就不会丢失
  • 不存在绝对的持久性

二、弱隔离

为了保证性能,多个事务一般是并行运行的,这就需要实现事务间的“弱隔离”,下面是实现弱隔离的方法:

2.1、读-提交

定义:

  • 读-提交保证两点:没有脏写和脏读
  • 脏读:读到了尚未提交的数据
  • 脏写:覆盖了尚未提交的数据

实现方法:

  • 防止脏读:可以用行级锁,但性能会很差,所以一般是数据库维护两个版本的数据:旧值和尚未更新的值
  • 防止脏写:行级锁,保证只有一个事务可以拿到对象的写锁

2.2、快照级别隔离

读-提交虽然可以解决大部分事务并发的问题,但依然有部分问题无法解决:

读倾斜(read skew)

比如A账户转账给B账户,存在一个中间态:A账户扣钱了,但B账户还没有收到钱。对于事务来讲,不应该读到这个中间态。

解决方法

使用快照级别隔离,保证单个事务读取到的是某个确定时间点的数据

实现方式

多版本并发控制(MVCC),即保存对象的多个不同的提交版本,根据事务id确定读到的应该是哪个版本的数据。

如何确定?(可见性原则)

  • 正在进行中的事务,尚未提交的数据绝对不可见
  • 更后面的事务,做出的任何修改都不可见(即使已经提交)
  • 被终止的事务任何修改绝对不可见

2.3、防止丢失更新

读-提交和快照隔离都只是解决了只读事务的问题,没有解决写事务隔离的问题(虽然读-提交中解决了脏写,但脏写只是写事务并发的一个特例)

更新丢失的定义

两个写事务,第二个写事务的结果没有包含第一个写事务修改后的值,比如两个自增某个字段的事务。

解决方法:

原子写操作

大多数数据库都实现了 Update 的原子化,即对于一个读-修改-写这样的操作,在读之前就加上独占锁(而不是在写之前)

显式加锁

数据库不支持内置原子操作的话,那么就用一个字段来显式加锁

原子比较和设置

写入前先确定,数据的最新值是否和事务开始时一致,如果不一致,则说明事务执行期间发生了改动,那么更新失败。

2.4、写倾斜(write skew)和幻读

定义

一些“读-修改-写入”这样的事务,写入的东西可能是由读的结果决定的(比如申请会议室,要先读会议室有没有被占用,再决定写入是否成功),这时查询的结果可能是“幻读”,即这个查询结果已经被其它事务改变了,这时就会产生写倾斜。

解决方法

实体化冲突:把幻读的冲突问题实体化为数据库的表,具体做法就是把数据的读写关系维护在一张表中,人工解决幻读的问题,这不是推荐的做法,推荐用可串行化隔离。

2.5、弱隔离级别总结

弱隔离级别可以做到高性能,但依然存在并发下的边际问题,括号中是解决方法:

并发读:脏读(锁)、读倾斜(快照级别隔离)、幻读(可串行化隔离)

并发写:脏写(锁)、写倾斜(可串行化隔离)


三、可串行化隔离

上面说的全都是“弱隔离级别”,它无法彻底解决写倾斜、幻读这样的边缘条件,所以我们需要更加严格的“可串行化隔离”,实现方式如下:

3.1、串行执行事务

不解释,就是实现上把每个事务真正地串行执行,简单粗暴,这样就能保证绝对不会有并发冲突,现在之所以可以这样做,是因为:

  • 内存越来越便宜,把数据加载到内存中,串行执行事务的速度大大提高。
  • 只读事务(比如数据分析相关的事务),可以运行在一致性快照上,不需要阻塞主线程。

串行执行事务有性能问题,但是有一些方法优化:

存储事务的过程

串行执行事务的数据库,如果等到某个事务经过多次IO才提交,会严重拖慢性能(这个事务一直占用着线程),所以串行执行事务的数据库一般都不支持IO式的多语句事务。

一个事务有很多步骤,而这些步骤可能是和用户IO产生的,比如选座订票系统,这时就可以把事务过程存储下来,事务提交时再一起发送给数据库。这中间需要一个存储过程的语言,比如T-SQL

分区

对数据进行合理的分区,可以大大提高串行运行事务的性能,即保证每个事务只在单个分区内运行,这样就只会阻塞那个分区内的事务线程。而跨分区事务,可以支持但是性能会比较差。

3.2、两阶段加锁(two-phase locking, 2PL)

  • 锁分为两种:共享锁和独占锁;
  • 获取共享锁时,如果对象的独占锁已经被拿走了,那么需要等待独占锁释放;
  • 获取独占锁时,如果对象有其它任何锁被拿走,那么需要等待释放;
  • 如果事务要读取对象,那么必须获得对象的共享锁(这样就保证了对象不会在事务期间被别的事务改动);
  • 如果事务要修改对象,那么必须获得独占锁(保证不会有脏写);
  • 如果事务先读后改,那么读时获得的共享锁升级为独占锁,步骤和获取独占锁一致。

“两阶段加锁”这个名字的由来就是,事务有两个阶段,先获取一些锁,然后释放它们,不会交替进行。

两阶段加锁的性能问题

“两阶段加锁”的核心思想是,如果两个事务试图做任何可能会引发竞争条件的事情,那么其中一个必须等另一个完成,这就会大大降低性能,并且可能发生死锁。

3.3、可串行化的快照隔离

“两阶段加锁”实际上是一种“悲观锁”,也就是只要有竞争条件的可能(哪怕实际没有),就会绝对串行执行。

可串行化的快照隔离是一种最新的算法,实际上是一种“乐观锁”,它假设没有冲突,然后到提交阶段检查是否产生了冲突,如果有,那么终止事务并且重试。

那么如何检查是否产生了冲突呢?这需要看事务是不是包含了过期的信息

检查是否读取了过期的MVCC对象

如果一个事务包含读写,并且在提交时,发现它读的对象已经被其它修改过了,那么就需要让这个事务提交失败。(避免上文提到的写倾斜)

检查写是否影响了之前的读

对于写入操作,如果检查发现写入会影响其它事务的读结果,那么需要通知其它事务,从而终止或者重试。

可串行化快照隔离的性能

性能比两阶段加锁更高,因为不需要等待其他事务所持有的锁释放;与串行执行相比,更能利用多个CPU核心,也可以在多个分区上运行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值