DDIA读书笔记 7.2 弱隔离级别

Design for data intensive application 读书笔记 第七章 第二节

第二节: 弱隔离级别(Weak isolation levels)

当两个transaction不触及相同数据的时候, 可以彼此并行处理.
但当触及相同数据的时候就会有并发问题(Concurrency issues, race conditions)

长期以来, 数据库开发人员试图去通去提供 transaction isolation 来解决这个问题使api开发者的工作更容易, 比如通过可序列化隔离(serializable isolation).
但实现可序列化隔离要付出相应的代价. 很多数据库不愿承受这么高的代价, 所以有些数据库调低了他们的隔离级别, 实现了不同水平的较弱的隔离级别, 统称弱隔离级别.(Weak isolation levels)
讨论了四种弱隔离级别.
当然, 调低隔离级别也是要付出代价的. 有时是严重的财务损失.
我们需要了解这些概念以便根据实际应用场景选择合适的数据库.

1. Read Committed( 只去读已经提交的数据 )

Read commited 提供了两个保证

  1. 读的时候只能读到已经提交(commited)的数据. No dirty read
  2. 写的时候只能覆盖掉已经提交的数据. No dirty write.
    不能有脏读
    以发送邮件为例. Sender发邮件有两个操作, a. 把邮件发到receiver的邮箱, b.更新receiver的未读计数器. 只有两个操作都完成commit之后, receiver才能看到. commit完之前是看不到的.
    不能有脏写
    两个人同时买一辆车的场景.
    有两个操作,
    更新销售记录
    开具销售发票 .

Read committed 的实现
实现 no dirty write相对比较简单, 行锁(row-level lock)
但是如果同样使用row-level lock实现no dirty read可能就会带来比较大的overhead. 一个时间较长的写请求会block住很多读请求, 极大的影响用户体验( 假设 read QPS >> write QPS)
所以经常是通过让数据库记住旧的数据来实现.

  1. Snapshot isolation and repeatable read (快照隔离, 可重复读)
    Read committed看上去还不错, 但不是全部, 还有不少场景cover不到. 书中举了爱丽丝查询银行帐户的例子.
    如果Alice有两个银行户头, 在她查询(只读操作) 的过程中如果这两个户头发生了一笔转帐, 但有可能她一个帐户读的是转帐前的值,一个帐户读的是转帐后的值.
    这样并没有违反read committed. 如果她刷新一次也可以解决, 然而这会造成用户极大的困扰.
    这种情况被称为不可重复读 或者读取偏差.
    一个更极端的例子是数据库备份的时候. 由于数据库备份可能花费更久的时候, 此时仍然需要接受写请求. 备份数据会包含一部分旧的数据和一部分新的数据. 还有分析查询(analytic queries) 和 integrity check的场景.

快照隔离(Snapshot isolation) 可以解决这些问题.
一个读的transaction 只能读到在他开始之前已经committed的数据. 即使以后写入了新的数据, 也不能读到新的.
快照隔离的实现:
防止dirty write依然可以通过锁来实现, 但读的时候加锁会极大影响性能. 关键是读不能阻塞写, 写不能阻塞读.
之前在read committed的时候, 是用记住committed之前的数据来实现read committed. 这种办法可以推广用来解决snapshot isolation的问题. 保存一个对象的几个committed versions. Multi version concurrency control (MVCC).
既然有多个version, data model必然要改变, 加一个created by , 和deleted by ( transaction ID)
既然有多个version, 也要定义哪些可见哪些不可见.
Visibility rule
transaction开始时, 那些尚在执行的transaction ID统统不可见
abort的也不可见
之后开始的也不可见.

引入了多版本之后 又带了新的问题, 索引.
简单的处理办法是指向所有版本, 然后过滤, 以后再删.(Garbage collection的时候)
另外一种办法是创建新的树.

命名混淆
Snapshot isolation有一些很容易混淆的名字. 在Oracal中被称为Serializable, 在PostgreSQL, MySQL中叫repeatable read.

3. Preventing Lost Updates (防止丢失更新)

前两个都主要是关于读的.
在 读取, 修改, 写入 (read-modify-write) cycle时, 如果两个类似操作同时发生, 仍然会发生并发性问题. 这种叫Lost Update.
这是一个很常见的问题, 所以已经有了一些解决方案.

解决方案

  1. 原子写

UPDATE counters SET value = value + 1 WHERE key = 'foo';
MongoDB, Reddis也提供了类似的操作.
但ORM框架很容易意外执行不安全的read-modify-write.

  1. 显性锁定
    有效, 但容易出错.
  2. 自动检测 Lost updates
    允许这些操作并行执行, 同时在后台进行检查. 所幸, 数据库可以高效的进行这些检查
    优点: 用户方便.
  3. Compare and set
    要小心一点, 如果read是从old snapshot读的就不对了.

4. Write Skew and Phantoms (写入偏差 和 幻读)

前面讨论的是操作相同对象的情况.
但不止于此. 以医院医生oncall作为应用场景
医院可能同时有两名或以上的医生oncall, 会最少要求有一名医生oncall.
如果现在有两人oncall, 两人同时想请病假. 则有可能两个人的请求都被批准.
这种被称为写入偏差(Write skew).

如果两个transaction读取相同的一批对象, 然后各自更新其中不同的对象, 则可能发生写write skew. (如果更新相同的对象则是dirty write或者lost update)

处理对策:

原子操作没用. 不同对像
有些 自动检测 lost update也没有用., PostgreSQL repeatable read, MYSQL/InnoDB repeatable read. 需要真正的Serializable isolation.
有些数据库允许配置constraint. 但这涉及对多个对像的constraints. 可以使用tigger, materialized view.
实在不行, 就把相关的row都锁上也不失为一种办法.

更多例子
会议预订, 多人游戏, 抢注用户名, 防止双重开支

导致写入偏差的幻读
上面的例子都遵循下面的模式
1. 一个SELECT查询符合条件的row, 做一些检查.
2. 按第一个查询的结果, application level决定是否要进行下一步的操作
3. 如果决定继续, 就写入, commit
如果两个transaction同时发生, 一个transaction执行到第三步的时候, 可能会改变另外一个transaction在第一步读的结果. 这时称为幻读. 这和 snapshot isolation不一样. snapshot isolation避免了只读查询时的幻读.

Materializing Conflict(物化冲突)

会议室的例子可以通过加锁来解决, 但是锁加在何处...
可以引入一个time slot + room的表格来加锁. 这个表格只是用来加锁的, 不是用存预定的.
这种办法叫物化冲实. 这种情况最好还是用 serializable 级别的isolation.

你可能感兴趣的:(DDIA读书笔记 7.2 弱隔离级别)