1.说明
前面讲了几个隔离级别,有这样的问题
1.隔离级别很难理解,并且各db实现方法不一致
2.很难界定自己的应用代码应该在哪个隔离级别下才是安全的
3.并发竞争很难发现以及复现
这一节就讲解串行化,顾名思义,就是结果和串行化执行一样。
往往通过下面三种方式实现
1.真的是串行执行
2.两阶段锁 2PL(2 phase locking)
3.序列化快照隔离 SSI(Serializable Snapshot Isolation)
下面讲解这三种方式
2.真.串行化
最简单的就是真的串行化,优劣如下
有时比支持并发的系统表现更好,以为不用管理锁
但是吞吐量会有限制
由于需要最大程度利用单线程,事务的组织结构会略有变化,用到下面的存储过程
存储过程
比如一个定机票的场景,涉及多步操作:1.查找路线价格和坐位 2.选坐位 3.支付以及订单
这些操作要在一个事务里面完成,但是如果每一步都让用户选择,会浪费资源(线程资源限制,在等待),且影响了其他事务的执行(因为真串行化一次一个)
为了避免网络IO,延迟以及希望sql能够事先编译好,db提出了存储过程
存储过程这里就不讲了,参照refer
存储过程的优缺点:
优点:
1.使得真串行化变得可行,提高了吞吐,减少了并发
缺点:
1.不同db存储过程语言不一样,并且发展慢
2.db跑的代码难以管理,不像自己的应用代码
3.因为db比业务sever更底层,所以如果存储过程写的不好,影响更大
真串行化小结
虽然真串行化可行,但是有如下限制
1.要求每个事务短而且快
2.要求数据集全部载入内存中才行
3.单CPU的话,写的吞吐量必须低
分区的话,写事务的跨区操作会有限制
3. 2阶段锁(2PL)
近30年,2PL是广泛应用于串行化中的方法(备注:2PL不是2PC即2阶段提交)
两阶段锁:
读锁会block住写锁
写锁会block住其他写操作和读锁
注意这个和上一节的快照隔离是不一样的,体现在原则上,区别如下
快照隔离要求写不影响读,读不影响写
2PL中,读写互相影响
实现2PL
主要用锁,共享锁和独占锁,机制如下:
1.读事务去拿共享锁
2.写操作必须拿独占锁
3.如果一个事务先读后写,则需要升级共享锁为独占锁
4.事务拿到锁之后,必须一直持有直到提交或者放弃。
"两阶段锁"中,"两阶段"的意思就是:第一个阶段是上锁,第二个阶段是释放锁
2PL的表现
性能是主要瓶颈,部分原因是锁多了,并发量降低了
由于串行化,前面的事务占有锁可能会使得后面的事务block住
因此,2PL有不确定性的延迟
并且死锁的触发会比读提交的隔离级别更频繁
文中提到了Predicate locks以及Index-range locks,有点晦涩这里就不展开了
3. Serializable Snapshot Isolation (SSI)
前面的两种方法中,
2PL性能不好
真.串行化 拓张兴不好
这里提出SSI,相较于快照隔离,提供了完全的串行化,只有轻微的性能损失
和快照隔离不同的是
快照隔离用悲观锁的思想,假定所有事务都有可能出错,所以要等到安全再执行。
SSI用乐观锁的思想,事务都会continue,希望every thing will be ok,最终提交时再检测是否需要丢弃
这在竞争少的情况下表现优良,在竞争多的情况下表现相对较差(因为频繁丢弃事务)
无论如何,只要足够稀疏,事务竞争不会太高,乐观思想比悲观思想表现的更好
3.1 在过时的假定下的决策
结合上一节讲的幻读的例子
医院值班时,至少要一个医生,此时两个值班医生Alice和Bob都在值班,但是都想请假,会执行事务逻辑如下
1.当前值班医生一共有几个
2.如果大于一个(因为算上自己),那么自己就可以请假了
这样两个事务同时执行,最终可能导致医院没有值班医生。
这种场景,主要体现在一个事务后续的写依赖于前面查询得到的决策
安全起见,db假设前置查询的结果如果有变化,后续的写操作也会变得invalid.
因此,在快照隔离的基础上,结合乐观锁的思想,SSI提供出冲突检测的算法,来判断一个事务提交时,是否需要丢弃
主要分为两个case
1.事务read时,由于MVCC原因忽略了某些写操作,等提交时,之前忽略的写操作已经commit了
2.当前事务的读操作的结果,被后续其他事务的写操作影响了
这里可能并不是很好理解,下面会有demo说明,结合图片理解
3.2 检测旧的MVCC读
简单来说,就是一个事务读取时,忽略掉的MVCC的旧版本的数据,在该事务提交前提交了,最终之前忽略掉的写却生效了
还是按照医生值班的例子
方框处的记录在事务42提交时生效,但是事务43读取数据时,这个方框记录由于MVCC原因,被忽视掉了
解决方式如下
db记录一个事务由于MVCC可见性原因而忽略掉其他事务的写操作
当该事务准备提交时,看忽略的写操作是否已经commit
若是,则该事务丢弃
为什么要等到commit时采取检测呢,这样做避免了不必要的丢弃:
1.如果事务43是read only的呢,并不用丢弃。谁能知道后面它会基于此进行写操作呢
2.说不准事务42也会被丢弃,因此不会影响事务43呢
3.3 检测写操作影响了之前的读
和上面的问题类似,直接画图
上面的查询中,有index-range锁在entry 1234上,后面事务42写的时候,获取了一个类似于写锁的东西,只不过他并不是阻塞其他事务的提交,而是负责通知:通知所有在entry 1234上的事务,告诉他们读的数据已经过时了。因此事务43最后会被丢弃.
3.4 SSI的表现
许多工程细节影响算法在实践中的工作效果。跟踪事务的读写的粒度会产生影响。
如果数据库非常详细地跟踪每一个事务的活动,那么它就可以精确地判断哪些事务需要中止,但是这些开销会变得很大。
而不太详细的跟踪事务会更快速,但可能导致更多的事务被中止。
相比与两阶段锁,可串行化隔离快照是大有好处的:一个事务不需要阻塞等待另一个事务持有的锁。
4.思考
过时假定下的决策
这个部分很重要,结合上一节幻读的例子,理解这个思想
SSI的构成
在快照隔离的基础上,结合乐观锁的思想,检测两种case来判断事务的提交是否有冲突进而被丢弃。
SSI检测冲突的两种方法的区别
结合上面图1,图2理解
图1是事务42 select,update之后,事务43才执行,因此有MVCC出现
图2,是事务42,43都select之后,42再进行update,所以没有MVCC而是有事务42直接的写的结果,影响了事务43之前的select结果
5.名词
2PL
Predicate locks
Index-range locks
SSI
6.refer
https://www.jianshu.com/p/a84e4f41a2aa
存储过程
http://www.cnblogs.com/hoojo/archive/2011/07/19/2110862.html
https://www.w3cschool.cn/sql/sql-storage.html