关于快照隔离和幻影读的一些深入思考

本文通过一个具体的消费场景案例,详细对比了MySQL中Repeatable Read与Serializable两种隔离级别的区别,特别是针对范围读锁的问题进行了深入探讨。

       mysql的隔离级别有四种:read uncommitted,read committed,repeatable read ,serializable

       前面三种的区别 比较容易理解 。关键是第三种个第四种的区别 。

       前段时间微信公众号上看到的一个例子,大致是这么说的:

    • table credit上面记录了每个用户的消费记录,record1(小明,date1,消费5000元),record2(小明,data2,消费2000元)
    • 有一条隐式约束,即小明的消费记录加载一期不能超过小明的信用额度1万元:5000+2000<10000
    • 事务1,小明想消费了2000元,先去select sum(消费) from credit where user=小明;若sum(消费)< 10000,则允许小明此次消费2000元,向credit表中插入一条新的记录,否则交易失败;
    • 事务2跟事务1相同,也是消费2000元;
  • 如果事务1跟事务2完全串行,那么事务2不会交易成功
  • 如果事务1未提交之前,事务2开始,那么
    • 在read repeatable的隔离级别下,事务2也是可以执行的,因为事务2开始时,计算小明消费总额时,读取到的是credit表的快照,此时仍然是可以消费的;
    • 如果级别是serializable的隔离级别,事务1在计算消费总额度时,会针对查询的范围都加上范围读锁,事务2执行的时候,发现事务1已经自己要读取的记录,加了范围锁,那么事务2会阻塞等待事务1提交或回滚。
        对于范围读不加锁导致的不一致性现象,SQL标准中叫幻影读(   phantom read)。mysql的前三种隔离级别,采用的是基于MVCC的快照隔离技术。而对于顺序读,则按照严格的2PC协议实现。对于顺序读,是否就无法采用快照隔离的技术呢。答案是否定的,可以增加一些额外的技巧做workaround。例如读取一个范围数据时,将这些范围数据映射为一个写操作,这个在第二个事务也做同样的范围读时,就就可以检测到写冲突。
        这个问题另外一个规避手段,应用层规避,用一条单独的记录来存储用户的剩余信用额度,每次只读取一条记录做判断。在做数据库设计时,为了性能,采用Repeatable Read的隔离级别(数据库的全局配置参数);为了一致性,尽量规避范围查询后做的写操作,而是把范围查询的聚合运算结果提前存到一个记录中。补充一点, mysql的RR隔离级别的MVCC实现是有些问题的,会导致write skew。简单的例子就是下面的语句update set count = count+1;如果是RR级别,并发高了,会出现lost update。oracle和pg不存在这个问题,pg的mvcc的rr实现,第二个事务提交前会检查写冲突。当然mysql可以通过自己的一个sql关键字规避这个问题,但是对于不熟悉的人,是个很大的坑,笔者就遇到过。
后记,实际上PG的serializable级别可能会存在write skew,PG的serializable实现了一种很特殊的serializable snapshot isolation,也会有些write skew。比如这么做:
  1. R两条记录A.count和B.count,if A.count+ B.count >1 然后W A=A-1。
  2. R两条记录A.count和B.count,if A.count+ B.count >1 然后W B=B-1。
出事条件是A.count=1且B.count=1;如果这两个事物同时执行,PG的serializable会允许两者同时提交,PG的SSI机制会认为事物1修改A和事务2修改B没有冲突。显然这违背了业务的约束,产生了write skew。看来,只有严格的2PL协议,才能保证不会出现write skew和read skew的现象。

参考
https://en.wikipedia.org/wiki/Snapshot_isolation

https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html

http://www.evanjones.ca/db-isolation-semantics.html

补充,一些容易混淆的技术概念:

  • 对于快照隔离,一般认为是一种隔离的级别,笔者更倾向于理解为隔离的技术,在此基础上可以实现不同的隔离级别,例如PG中对于serializable的实现,就是对SI隔离的升级为serializable snapshot isolation;
  • SQL92事务隔离级别的定义是基于悲观锁的思想,例如教科书上的三级封锁协议的定义;商用数据库的实现过程中,多采用基于乐观锁的MVCC技术;商用DBMS事务隔离级别的定义,跟SQL92的标准是有所差别的。
  • 不同的DBMS对于事务隔离级别的定义和实现相差非常大;例如DB2中最高隔离级别repeatable read,相当于mysql的serializable;DB2的cursor stability(锁住select查询游标指向的当前记录)在mysql并没有定义;

https://www.ibm.com/support/knowledgecenter/SSEPGG_9.7.0/com.ibm.db2.luw.admin.perf.doc/doc/c0004121.html

https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html

http://it.dataguru.cn/article-8406-1.html?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

### **两阶段提交(2PC)与快照隔离(Snapshot Isolation)机制详解** --- ## **1. 两阶段提交(2PC)** ### **(1)核心目标** 确保 **分布式事务的原子性**,即多个参与节点(如 MongoDB 的分片)要么全部提交,要么全部回滚。 ### **(2)执行流程** | 阶段 | 说明 | MongoDB 分片集群中的行为 | |------|------|--------------------------| | **阶段一:准备(Prepare)** | 协调者(mongos)向所有参与者(分片)发送 `prepare` 请求,询问是否可以提交。 | 各分片锁定相关数据,记录事务日志(oplog),返回 `YES` 或 `NO`。 | | **阶段二:提交/回滚(Commit/Rollback)** | 若所有参与者返回 `YES`,协调者发送 `commit`;否则发送 `rollback`。 | 分片释放锁并持久化数据(提交)或丢弃日志(回滚)。 | ### **(3)关键特点** - **阻塞性**:参与者需等待协调者指令,可能因网络问题导致长时间阻塞。 - **单点故障风险**:协调者宕机可能导致事务挂起(需超时机制解决)。 - **MongoDB 的实现**: - 分片事务通过 **全局逻辑时钟** 同步状态。 - 默认超时时间为 **60秒**,超时后自动回滚。 ### **(4)示例场景** ```plaintext 用户跨分片转账: 1. mongos 向分片A(账户1)分片B(账户2)发送 prepare。 2. 分片A/B 锁定账户,记录“扣款100元”到日志,返回 YES。 3. mongos 收到所有 YES 后,发送 commit。 4. 分片A/B 提交日志,完成转账。 ``` --- ## **2. 快照隔离(Snapshot Isolation)** ### **(1)核心目标** 提供 **事务内取一致性**,确保事务看到的数据是 **某个时间点的快照**,避免脏、不可重复。 ### **(2)实现原理** | 机制 | 说明 | MongoDB 中的实现 | |------|------|------------------| | **多版本并发控制(MVCC)** | 数据修改时保留旧版本,事务快照版本。 | 通过 oplog 存储引擎(如 WiredTiger)的多版本支持。 | | **事务快照时间戳** | 事务开始时获取全局一致性快照时间戳。 | 使用 **逻辑时钟(logical clock)** 标记事务开始时间。 | | **冲突检测** | 若事务提交时数据被其他事务修改,则回滚(写-写冲突)。 | 基于时间戳检查数据版本是否变化。 | ### **(3)关键特点** - **不阻塞写**:取旧版本快照,不影响并发写入。 - **避免幻**:快照范围内数据保持一致(需配合索引)。 - **MongoDB 的配置**: - 事务需显式指定 `readConcern: { level: "snapshot" }`。 - 默认隔离级别为 **已提交(read committed)**。 ### **(4)示例场景** ```plaintext 事务T1 取用户余额(快照时间戳=100): 1. T1 开始,获取快照时间戳 100。 2. 事务T2 在时间戳 110 修改余额并提交。 3. T1 仍取时间戳 100 的余额(不受T2影响)。 4. 若T1 提交时余额版本≠100,则回滚(写冲突)。 ``` --- ## **3. 两阶段提交 vs. 快照隔离** | 特性 | 两阶段提交(2PC) | 快照隔离 | |------|------------------|----------| | **解决的核心问题** | 分布式原子性 | 取一致性 | | **依赖的技术** | 协调者+参与者日志 | MVCC+时间戳 | | **性能开销** | 高(网络往返+锁) | 中(版本存储) | | **典型应用场景** | 跨分片数据修改 | 跨文档查询+修改 | --- ## **4. MongoDB 中的协同工作** - **事务流程**: 1. 事务开始时,获取快照时间戳(快照隔离)。 2. 跨分片操作通过 2PC 提交(原子性)。 3. 提交时检查数据版本冲突(快照隔离)。 - **配置建议**: ```javascript session.startTransaction({ readConcern: { level: "snapshot" }, // 快照隔离 writeConcern: { w: "majority" } // 2PC 持久化 }); ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值