likes
comments
collection
share

Innodb 并发事务问题

作者站长头像
站长
· 阅读数 7

脏读、幻读、不可重复读

释意

  • 脏读 dirty read

读取到其他事务未提交的数据。

例如:

TransactionADatabaseTransactionB初始状态 a = 0beginbeginset a = 1a = 1read aa = 1rollbacka = 0commitTransactionADatabaseTransactionB

事务 A 读取到了事务 B 未提交的修改。如果 B 事务直接正常提交结束,这只是一个并发问题,但是一旦事务 B 回滚或再次更新,事务 A 就相当于使用一个错误的值进行后序操作,会直接破坏数据的一致性。

  • 不可重复读 non-repeatable read

同一事务内前后读取结果不一致。

例如:

TransactionADatabaseTransactionB初始状态 a = 0 限制 a < 2beginread aa = 0beginset a = 1commita = 1read aa = 1rollbackTransactionADatabaseTransactionB
  • 幻读 phantom read

查询结果集中出现了没有在上一次相同查询中出现的结果。与不可重复读的区别:不能通过锁定结果集规避。

例如

TransactionADatabaseTransactionB初始状态 id = 0beginread allid = 0begininsert id = 1commitid = 0, id = 1read allid = 0, id = 1rollbackTransactionADatabaseTransactionB

为什么需要解决这些问题

最直接的目的还是要满足数据库设计的 ACID 原则。

ACID

  • Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则。
  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
  • Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

ACID 并非平级关系,AID 三个特性存在的目的是为了保证 C 特性。

解决

直觉的枷锁:

  • 脏读 对修改的数据加排他锁。提交或回滚后释放锁。
  • 不可重复读 对读取过的数据加读锁,仅允许读,不允许修改。
  • 幻读 对查询范围加读锁,只允许读取。

缺点:频繁的锁竞争会降低性能,并且容易发生死锁。

事务隔离

  • 读未提交(Read Uncommitted):允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。
  • 读提交(Read Committed):允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。
  • 可重复读取(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻读数据。这可以通过“共享读锁”和“排他写锁”实现。
  • 序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

mvcc 事务隔离

通过为每一行记录添加两个额外的隐藏的值来实现 MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。但是 InnoDB 并不存储这些事件发生时的实际时间,相反它只存储这些事件发生时的系统版本号。这是一个随着事务的创建而不断增长的数字。每个事务在事务开始时会记录它自己的系统版本号。每个查询必须去检查每行数据的版本号与事务的版本号是否相同。实现主要依赖于记录中的三个隐藏字段(DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID)、undolog,read view 来实现的。

使用 mvcc 后可以在无锁情况下解决脏读和不可重复读问题,而解决幻读所付出的锁代价没有增加,可以调高数据库的并发性能。此外使用 mvcc 在事务回滚时,舍弃掉当前事务持有的数据即可,不需要对数据进行修改。

一些思考 欢迎探讨

为什么要在事务中重复读取数据

可能的原因:

  • 复杂查询拆分实现 例如:一次活动,需要对账户 余额[500,1000)积分[0,500]的用户以及余额[1000,∞)的用户发放奖励。实现方案有多种,可以用复杂查询一次性得到需要发放奖励的用户。也可以分别查询两种用户。
  • 隐式查询 例如:对 a 字段添加了大小限制,要求小于 500。党对 a 进行增加时,就会验证 a 的结果是否符合条件,是一种变相的查询。包括主键唯一性,唯一键等都是如此。

为什么需要隔离性,可以重复读取

首先要明确并发写入的冲突是不可避免的必须要使用一些机制取规避。在这种前提下我们只需要考虑并发读取的情况: 可能的原因:

  • 事务操作的顺序性 举例:开放条件下,事务 A 根据当前用户余额和一些其他条件为用户发放奖励,事务 B 对用户进行扣款。事务 A 执行时用户可以获取到奖励,事务 B 执行完成后则不满足获取奖励条件。这时更具事务启动时间来看,事务 A 应该能执行成功。但是考虑到事务 B 的执行,就可能造成用户无法得到奖励的情况。
  • 乐观和悲观模式 隔离性更像时一种乐观模式,假设事务开始后就没有其他事务对数据进行修改。相对的一种悲观模式的处理就是假设,假设之后会有事务需要用到相关数据。乐观模式可以减少竞争,但是相对的存在更大的回滚风险。

参考资料

mysql5.7 官方文档 百度百科

转载自:https://juejin.cn/post/7380163683009593407
评论
请登录