Database Isolation Level

2019-05-23  本文已影响0人  lionel880

参考文档:https://www.cnblogs.com/huanongying/p/7021555.html
几个概念的梳理:

一、常见概念

mysql默认的事务隔离级别为repeatable-read

需要搞清楚的几个概念

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

二、单机数据库是如何实现的

1、READ COMMITED
no dirty read.png

READ COMMITED保证了2点

实现细节

no dirty writes
大部分数据库通过 row-lock,行锁,来保证,你的写,要么得不到锁等到,拿到锁写入的时候,肯定是已提交的
no dirty read
最简单,参考写锁,用同一个锁,即写和读都要去拿同一个行锁。但这会导致一个写比较慢时,大量的读堵塞的情况。严重影响性能
实际的实现:
当持有写锁时,数据库同时有一份老的数据和新的数据,如果未提交或回滚,则此时读就会给老的数据,如果提交了则使用新的数据。此时数据库维护2份数据

2.Snapshot Isolation and Repeatable Read

乍一看 read commited已经很完美,那么还会有其他问题吗?


image.png

你在一个事务中读到一个变化的值, 这被称作 nonrepeatable read or read skew。说实话,这在大部分情况下,没有问题,你读到了一个最新的值。但在有些情况下,这会带来问题

解决方案--SNAPSHOT isolation

*如何实现MVCC?---通过 transaction id,即txid
难道我们真的要将所有数据每个事务,都全部保存一次吗?这显示是完成不了的。
先了解txid是什么?txid是数据库分配给每个事务的唯一id,可以体现出先后顺序


image.png

每一个写入操作,都会触发一个版本号的维护任务
每一行数据都有一系列版本维护信息
create_by field
delete_by field:一开始为空,当一个transaction删除这一行数据时,数据没有被真正的删除,只是在这个字段记录上txid信息,只有随着时间推移,当确认没有transcation会读取这个数据后,才会真正通过垃圾回收删除掉
update操作本质上是一个delete+create操作


这样的优点是什么呢?
原来我想象中的snapshot 直观印象上都是整个库的快照,但它的实现,其实非常精致巧妙,它只需要通过txid,将你的事务开始前的 transaction in progress 维护一个list,这个list数目实际上不会很大,在加上txid的有序递增,所有大于你的txid事务都不可见,就实现了快照的功能

3.可重复读的情况下,还有什么问题?

lost update
2个事务都read-modify-write cycle,假设都把一个值+1,最终可能只会+1,因为第二个操作,将第一个给覆盖了,好像之前的事务没有修改过一样?这就是无所谓的 lost update

UPDATE counters SET value = value + 1 WHERE key = 'foo';

这个原子操作是通过一个特殊的锁实现的,使用这个特殊锁时,读的数据也是加锁的, 其他事务无法在获得锁事务结束前读取数据,这个方法被称为 cursor stability
此外还有一些探测lost update等操作,各个数据库采用不同的策略,有些库支持lost update,有些则不自动支持。

write skew
lost update其实是最简单的一种write skew类型,更广泛的类型是read出来的数据,用于判断,然后去操作另一份数据。
write skew的基础流程
1.query 满足条件的row
2.依赖query的结果,决定之后的操作
3.write(insert,update,delete)

先看例子


write skew

可以看出,这仍然是一个read-modify-write的操作,但和lost update相比的差别在哪?
lost update其实是write skew的一种特殊情况,特殊在那?即modify和update修改的是同一个值,而write skew修改的是不同的值,这更为难搞,因为他们修改的甚至都不是同一个值?
因此解决这个问题也更为苛刻

BEGIN TRANSACTION;
    SELECT * FROM doctors
    WHERE on_call = true
    AND shift_id = 1234 FOR UPDATE;

    UPDATE doctors
    SET on_call = false  
    WHERE name = 'Alice'
    AND shift_id = 1234;
COMMIT;

As before, FOR UPDATE tells the database to lock all rows returned by this
query.

四、seriablizability

这里的串行化,只是一个概念,它保证的是所有的结果都会像串行执行出来一样
具体有以下3种方式

实际理论算法2pl--two-phase locking

过去30年,数据库实现serializability,只有1种算法,即为2pl,之前我们已经提到过,no dirty write的概念,通过加锁实现。后面我么又提过 snapshot 的实现原则,读和写不会互相阻塞
而2pl则是更为强力的方式,写不仅堵塞其他的写,还会阻塞其他的读和版本号修改

2pl的具体实现

以mysql的innodb以此方式实现:
1.每个object都有一个锁,这个锁有2种模式,共享状态和独占状态。
2.当一个事务read时,获得一个 共享锁,共享锁可以被多个read获得,但如果该锁为独占时,就必须等待
3.当一个事务要去写时,必须获得一个独占的锁,当有其他事务持有锁时,必须等待所有其他的持有释放共享或者独占的锁
4.当一个事务一开始读取object,然后改为写入,此时要将共享锁改为独占锁,操作等同于获得一个独占的锁
4.当事务获得锁时,会一直持有直到事务结束

可以看到,所有的object都会有个锁,会有大量的锁同时存在,很容易产生死锁的情形,数据库会探测死锁的情况,然后放弃事务,由应用进行相应处理

可以看到,相比于之前,写锁是独占式的,他是会影响读取的!!!

2pl的问题也是显而易见的,写对读的影响,很容易导致,一个事务必须等待完另一个事务完成,再延迟性能上很难保证。

假设事务A第一步查询为:

SELECT * FROM bookings
WHERE room_id = 123 AND
end_time > '2018-01-01 12:00' AND
start_time < '2018-01-01 13:00';

这时候事务B如果持有独占锁的object也满足这些条件,那么A必须等待,直到B释放锁

当A想修改,删除,新增记录时,他必须去检查,修改前后的新值或者旧值,是满足已存在的predict lock的,如果有,那么也必须等待。
这里能看出predict lock的意义,它不仅仅会对已存在的object进行加锁,它还会对未来要修改或者新增的object进行加锁。

当2pl+predict lock时,就实现了Serializability效果

但predict lock的性能太差了,它需要对所有的条件进行匹配
因此大部分情况使用 index-range locking进行代替,也被称为next-key locking,这是predict lock的简化版

这里的关键概念是什么,减少匹配的难度,即加快匹配的速度
实际的实现:将精确的匹配条件转为更为简单的匹配,放宽匹配的要求
如1p.m-2p.m room123,扩大为所有的room和所有的时间

还是刚刚的例子,让你的room_id有索引的,可以直接将 这个index加锁,这样自然就锁住了room 123,同理时间范围也可以
所有的操作转化到了相应的index上。
index-range 的方式可能不如之前的那么精确,但能提高效率,此时要确保查询字段有index,不然可能会转为表锁

五、SSI Serializable Snapshot Isolation

SSI是2008年提出的一种新算法,在保证Serializable的同时,代价更小。目前已在一些单点数据库如PostgreSQL上使用。

上一篇下一篇

猜你喜欢

热点阅读