手把手网站开发,莫名接到网站建设电话,摄影网站都有什么,网站关键词优化有用吗01数据库并发控制的作用
1.1 事务的概念
在介绍并发控制前#xff0c;首先需要了解事务。数据库提供了增删改查等几种基础操作#xff0c;用户可以灵活地组合这几种操作#xff0c;实现复杂的语义。在很多场景下#xff0c;用户希望一组操作可以做为一个整体一起生效首先需要了解事务。数据库提供了增删改查等几种基础操作用户可以灵活地组合这几种操作实现复杂的语义。在很多场景下用户希望一组操作可以做为一个整体一起生效这就是事务。事务是数据库状态变更的基本单元包含一个或多个操作例如多条SQL语句。
经典的转账事务就包括三个操作1检查A账户余额是否足够。2如果足够从A扣减100块。3B账户增加100块。
事务有个基本特性这一组操作要么一起生效要么都不生效事务执行过程中如遇错误已经执行的操作要全部撤回这就是事务的原子性。
如果失败发生后部分生效的事务无法撤回那数据库就进入了不一致状态与真实世界的事实相左。例如转账事务从A账户扣款100块后失败了B账户还未增加款项如果A账户扣款操作未撤回这个世界就莫名奇妙丢失了100块。原子性可以通过记日志更改前的值来实现还有一些数据库将事务操作缓存在本地如遇失败直接丢弃缓存里的操作。
事务只要提交了它的结果就不能改变了即使遇到系统宕机重启后数据库的状态与宕机前一致这就是事务的持久性。
数据只要存储非易失存储介质宕机就不会导致数据丢失。因此数据库可以采用以下方法来保证持久性1事务完成前所有的更改都保证存储到磁盘上了。或2提交完成前事务的更改信息以日志的形式存储在磁盘重启过程根据日志恢复出数据库系统的内存状态。一般而言数据库会选择方法2原因留给读者思考。
数据库为了提高资源利用率和事务执行效率、降低响应时间允许事务并发执行。但是多个事务同时操作同一对象必然存在冲突事务的中间状态可能暴露给其它事务导致一些事务依据其它事务中间状态把错误的值写到数据库里。需要提供一种机制保证事务执行不受并发事务的影响让用户感觉当前仿佛只有自己发起的事务在执行这就是隔离性。
隔离性让用户可以专注于单个事务的逻辑不用考虑并发执行的影响。数据库通过并发控制机制保证隔离性。由于隔离性对事务的执行顺序要求较高很多数据库提供了不同选项用户可以牺牲一部分隔离性提升系统性能。这些不同的选项就是事务隔离级别。
数据库反映的是真实世界真实世界有很多限制例如账户之间无论怎么转账总额不会变等现实约束年龄不能为负值性别最多只能有男、女、跨性别者三种选项等完整性约束。事务执行不能打破这些约束保证事务从一个正确的状态转移到另一个正确的状态这就是一致性。
不同与前三种性质完全由数据库实现保证一致性既依赖于数据库实现原子性、持久性、隔离性也是为了保证一致性也依赖于应用端编写的事务逻辑。
1.2 事务并发控制如何保证隔离性
为了保证隔离性一种方式是所有事务串行执行让事务之间不互相干扰。但是串行执行效率非常低为了增大吞吐减小响应时间数据库通常允许多个事务同时执行。因此并发控制模块需要保证事务并发执行的效果与事务串行执行的效果完全相同serializability以达到隔离性的要求。
为了方便描述并发控制如何保证隔离性我们简化事务模型。事务是由一个或多个操作组成所有的操作最终都可以拆分为一系列读和写。一批同时发生的事务所有读、写的一种执行顺序被定义为一个schedule例如 T1、T2同时执行一个可能的schedule: T1.read(A),T2.read(B),T1.write(A),T1.read(B),T2.write(A) 如果并发事务执行的schedule效果与串行执行的scheduleserial schedule等价就可以满足serializability。一个schedule不断调换读写操作的顺序总会变成一个serializable schedule但是有的调换可能导致事务执行的结果不一样。一个schedule中相邻的两个操作调换位置导致事务结果变化那么这两个操作就是冲突的。冲突需要同时满足以下条件
这两个操作来自不同事务至少有一个是写操作操作对象相同
因此常见的冲突包括
读写冲突。事务先A读取某行数据、事务B后修改该行数据和事务B先修改某行事务、事务A后读该行记录两种schedule。事务A读到的结果不同。这种冲突可能会导致不可重复读异象和脏读异象。写读冲突。与读写冲突产生的原因相同。这种冲突可能会导致脏读异象。写写冲突。两个操作先后写一个对象后一个操作的结果决定了写入的最终结果。这种冲突可能会导致更新丢失异象。
数据库只要保证并发事务的schedule保持冲突操作的执行顺序不变只调换不冲突的操作可以成为serial schedule就可以认为它们等价。
这种等价判断方式叫做conflict equivalent两个schedule的冲突操作顺序相同。例如下图的例子T1 write(A)与T3 read(A)冲突且T1先于T3发生。T1 read(B)和 T2 write(B)冲突且T2先于T1因此左图事务执行的schedule与T2,T1,T3串行执行的serial schedule右图 等价。左图的执行顺序满足conflict serializablity。 再分析一个反例T1 read(A)与T2 write(A)冲突且T1先于T2T2 write(A)与T2 write(A)冲突且T2先于T1。下图这个个schedule无法与任何一个serial schedule等价是一个不满足conflict serializablity的执行顺序会造成更新丢失的异象。 总体来说serializability是比较严格的要求为了提高数据库系统的并发性能很多用户愿意去降低隔离性的要求以寻求更好的性能。数据库系统往往会实现多种隔离级别供用户灵活选择关于事务隔离级别可以参看这篇文章。
并发控制的要求清楚了如何实现呢后文将依据冲突检测的乐观程度一一介绍并发控制常见的实现方法。
02基于两阶段锁的并发控制
2.1 2PL
既然要保证操作按正确的顺序执行最容易想到的方法就是加锁保护访问对象。数据库系统的锁管理器模块专门负责给访问对象加锁和释放锁保证只有持有锁的事务才能操作相应的对象。锁可以分为两类S-Lock和X-LockS-Lock是读请求使用的共享锁X-Lock是写请求使用的排他锁。它们的兼容性如下操作同一个对象只有两个读请求相互兼容可以同时执行读写和写写操作都会因为锁冲突而串行执行。 2PLTwo-phase locking是数据库最常见的基于锁的并发控制协议顾名思义它包含两个阶段
阶段一Growing事务向锁管理器请求它需要的所有锁存在加锁失败的可能。阶段二Shrinking事务释放Growing阶段获取的锁不允许再请求新锁。
为什么加锁和放锁要泾渭分明地分为两个阶段呢
2PL并发控制目的是为了达到serializable如果并发控制不事先将所有需要的锁申请好而是释放锁后还允许再次申请锁可能出现事务内两次操作同一对象之间其它事务修改这一对象如下图所示进而无法达到conflict serializable出现不一致的现象下面的例子是lost update。 2PL可以保证conflict serializability因为事务必须拿到所有需要的锁才能执行。例如正在执行的事务A与事务B冲突事务B要么已经执行完要么还在等待。因此那些冲突操作的执行顺序与BA或AB串行执行时冲突操作执行顺序一致。
所以数据库只要采用2PL就能保证一致性和隔离性了吗来看一下这个例子 以上执行顺序是符合2PL的但T2读到了未提交的数据。如果此时T1回滚则会引发级联回滚T1的更改不能被任何事务看到。因此数据库往往使用的是加强版的S(trong)S(trict)2PL它相较于2PL有一点不同shrinking阶段只能在事务结束后再释放锁完全杜绝了事务未提交的数据被读到。
2.2 死锁处理
并发事务加锁放锁必然绕不开一个问题--死锁事务1持有A锁等B锁事务2持有B锁等A锁。目前解决死锁问题有两种方案
Deadlock Detection
数据库系统根据waits-for图记录事务的等待关系其中点代表事务有向边代表事务在等待另一个事务放锁。当waits-for图出现环时代表死锁出现了。系统后台会定时检测waits-for图如果发现环则需要选择一个合适的事务abort。
Deadlock Prevention
当事务去请求一个已经被持有的锁时数据库系统为防止死锁杀死其中一个事务一般持续越久的事务保留的优先级越高。这种防患于未然的方法不需要waits-for图但提高了事务被杀死的比率。
2.3 意向锁
如果只有行锁那么事务要更新一亿条记录需要获取一亿个行锁将占用大量的内存资源。
我们知道锁是用来保护数据库内部访问对象的这些对象根据大小可能是属性Attribute、记录Tuple、页面Page、表Table相应的锁可分为行锁、页面锁、表锁没人实现属性锁对于OLTP数据库最小的操作单元是行。对于事务来讲获得最少量的锁当然是最好的比如更新一亿条记录或许加一个表锁就足够了。
层次越高的锁如表锁可以有效减少对资源的占用显著减少锁检查的次数但会严重限制并发。层次越低的锁如行锁有利于并发执行但在事务请求对象多的情况下需要大量的锁检查。数据库系统为了解决高层次锁限制并发的问题引入了意向Intention锁的概念
Intention-Shared (IS)表明其内部一个或多个对象被S-Lock保护例如某表加IS表中至少一行被S-Lock保护。Intention-Exclusive (IX)表明其内部一个或多个对象被X-Lock保护。例如某表加IX表中至少一行被X-Lock保护。SharedIntention-Exclusive (SIX)表明内部至少一个对象被X-Lock保护并且自身被S-Lock保护。例如某个操作要全表扫描并更改表中几行可以给表加SIX。读者可以思考一下为啥没有XIX或XIS
意向锁和普通锁的兼容关系如下所示 意向锁的好处在于当表加了IX意味着表中有行正在修改。
1这时对表发起DDL操作需要请求表的X锁那么看到表持有IX就直接等待了而不用逐个检查表内的行是否持有行锁有效减少了检查开销。2这时有别的读写事务过来由于表加的是IX而非X并不会阻止对行的读写请求先在表上加IX再去记录上加S/X事务如果没有涉及已经加了X锁的行则可以正常执行增大了系统的并发度。
3303基于Timing OrderT/O的并发控制
为每个事务分配timestamp并以此决定事务执行顺序。当事务1的timestamp小于事务2时数据库系统要保证事务1先于事务2执行。timestamp分配的方式包括1物理时钟2逻辑时钟3混合时钟。
3.1 Basic T/O
基于T/O的并发控制读写不需加锁 每行记录都标记了最后修改和读取它的事务的timestamp。当事务的timestamp小于记录的timestamp时不能读到”未来的”数据需要abort后重新执行。假设记录X上标记了读写两个timestampWTS(X)和RTS(X)事务的timestamp为TTS可见性判断如下读
TTS WTS(X)该对象对该事务不可见abort事务取一个新timestamp重新开始。TTS WTS(X)该对象对事务可见更新RTS(X) max(TTS,RTS(X))。为了满足repeatable read事务复制X的值。为了防止读到脏数据可以在记录上做特殊标记读请求需等待事务提交后再去读。
写
TTS WTS(X) || TTS RTS(X)abort事务重新开始。TTS WTS(X) TTS RTS(X)事务更新XWTS(X) TTS。
这里之所以要求TTS RTS(X)是为了防止如下情况读请求的时间戳为rts已经读过X时间戳设为RTS(X)rts如果新事务的TTS RTS(X)并且更新成功则rts读请求再来读一次就看到新的更改了违反了repeatable read因此这是为了避免读写冲突。记录上存储了最后的读写时间可以保证conflict serializable
这种方式也能避免write skew例如初始状态X和Y两条记录X-3Y5XY 0RTS(X)RTS(Y)WTS(X)WTS(Y)0。事务T1的时间戳为TTS11事务T2的时间戳TTS22。 它缺陷包括
长事务容易饿死因为长事务的timestamp偏小大概率会在执行一段时间后读到更新的数据导致abort。读操作也会产生写写RTS。
04基于ValidationOCC的并发控制
执行过程中每个事务维护自己的写操作Basic T/O在事务执行过程中写就将数据写入DB和相应的RTS/WTS提交时判断自己的更改是否和数据库中已存在的数据冲突如果不冲突才写入DB。OCC分为三个阶段
Read Write Phase即读写阶段事务维护读的结果和即将提交的更改以及写入记录的RTS和WTS。Validation Phase检查事务是否与数据库中的数据冲突。Write Phase不冲突就写入冲突就abort,restart。
Read Write Phase结束进入Validation Phase相当于事务准备完成进入提交阶段了进入Validation Phase的时间被选做记录行的时间戳来定序。不用事务开始时间是因为事务执行时间可能较长导致后开始的事务可能先提交这会加大事务冲突的概率较小时间戳的事务后写入数据库肯定会abort。
Validation过程
假设当前只有两个事务T1和T2并修改了相同数据行T1的时间戳 T2的时间戳即validation顺序T1 T2对用户而言T1先发生于T2则有如下情况
1T1在validate阶段T2还在Read Write Phase。此时只要T1和T2已经发生的读写没有冲突就可以提交。
如果WS(T1) ∩ (RS(T2) ∪ WS(T2)) ∅说明T2和T1写的记录无冲突validation通过可以写入。否则T2与T1之间存在读写冲突或写写冲突T1需要回滚。读写冲突T2读到了T1写之前的版本T1提交后它可能读到T1写的版本不可重复读。写写冲突T2有可能在旧版本基础上更新再次写入造成T1的更新丢失。
2T1完成validate阶段进入write阶段直到提交完成这已经是不可逆的了。T2在T1进入write phase之前的读写肯定和T1的操作不冲突因为T1 validation通过了。T2之后继续的读写操作有可能冲突与T1要提交的操作因此T2进入validate阶段
如果WS(T1) ∩ RS(T2) ∅说明T2没读到T1写的记录validation通过T2可以写入。为什么不验证WS(T2)了呢WS(T1)已经提交了且它的时间戳小于WS(T2)WS(T2)里之前的一部分肯定没有冲突之后的一部分因为没有读过T1的写入的对象写进去也没问题不会覆盖WST1的写否则T2与T1之间存在读写冲突和写写冲突T2需要回滚。读写冲突T2读到了T1写之前的版本T1提交后它可能读到T1写的版本不可重复读。写写冲突T2有可能在旧版本基础上更新再次写入造成T1的更新丢失。
05基于MVCC的并发控制
数据库维护了一条记录的多个物理版本。事务写入时创建写入数据的新版本读请求依据事务/语句开始时的快照信息获取当时已经存在的最新版本数据。它带来的最直接的好处是写不阻塞读读也不阻塞写读请求永远不会因此冲突失败例如单版本T/O或者等待例如单版本2PL。对数据库请求来说读请求往往多于写请求。主流的数据库几乎都采用了这项优化技术。
MVCC是读和写请求的优化技术没有完全解决数据库并发问题它需要与前述的几种并发控制技术组合才能提供完整的并发控制能力。常见的并发控制技术种类包括MV-2PLMV-T/O和MV-OCC它们的特点如下表 MVCC还有两个关键点需要考虑多版本数据的存储和多余多版本数据的回收。
多版本数据存储方式大致可以分为两类
1Append only的方式新旧版本存储在同一个表空间例如基于LSM-Tree的存储引擎。
2主表空间记录最新版本数据前镜像记录在其它表空间或数据段例如InnoDB的多版本信息记录在undo log。多版本数据回收又称为垃圾回收GC那些没有机会再被任何读请求获取的旧版本记录应该被及时删除。
06总结
本文依据冲突处理的时机乐观程度依次介绍了基于锁在事务开始前预防冲突、基于T/O在事务执行中判断冲突和基于Validation在事务提交时验证冲突的事务并发控制机制。
不同的实现适用于不同的workload并发冲突小的workload当然适合更乐观的并发控制方式。而MVCC可以解决只读事务和读写事务之间相互阻塞的问题提高了事务的并发读被大多数主流数据库系统采用。
原文链接 本文为云栖社区原创内容未经允许不得转载。