乐趣区

关于数据库:如何实现数据库读一致性

1 导读

数据的一致性是数据精确的重要指标,那如何实现数据的一致性呢?本文从事务个性和事务级别的角度和大家一起学习如何实现数据的读写一致性。

2 一致性

1. 数据的一致性:通常指关联数据之间的逻辑关系是否正确和残缺。

举个例子:某零碎实现读写拆散,读数据库是写数据库的备份库,小李在零碎中之前录入的学历信息是高中,通过小李努力学习,胜利取得了本科学位。小李及时把信息变成成了本科,可是因为明天零碎备份工夫较长,小李变更信息时,数据曾经开始备份。公司的 HR 通过零碎查问小李信息时,发现还是本科,小李的申请被驳回。这就是数据不统一问题。

2. 数据库的一致性:是指数据库从一个一致性状态变到另一个一致性状态。这是事务的一致性的定义。

举个例子:仓库中商品 A 有 100 件,门店中商品 A 有 10 件。上午 10 点,仓库发送商品 A50 件到门店,最初仓库中有商品 A50 件,门店有商品 A60 件,这样商品的总是是不变的。不能门店收到货后,仓库的商品 A 还是 100 件,这样就呈现数据库不统一问题。仓库和门店商品 A 的总数是 110 才是正确的,这就是数据库的一致性。

3 数据库事务

数据库事务 (transaction) 是拜访并可能操作各种数据项的一个数据库操作序列,这些操作要么全副执行, 要么全副不执行,是一个不可分割的工作单位。事务由事务开始与事务完结之间执行的全副数据库操作组成。

事务的性质:

  • 原子性(Atomicity):事务中的全副操作在数据库中是不可分割的,要么全副实现,要么全副不执行。
  • 一致性(Consistency):几个并行执行的事务,其执行后果必须与按某一程序 串行执行的后果相一致。
  • 隔离性(Isolation):事务的执行不受其余事务的烦扰,事务执行的两头后果对其余事务必须是通明的。
  • 持久性(Durability): 对于任意已提交事务,零碎必须保障该事务对数据库的扭转不被失落,即便数据库呈现故障

4 并发问题

数据库在并发环境下会呈现脏读、反复读和幻读问题。

1. 脏读

事务 A 读取了事务 B 未提交的数据,如果事务 B 回滚了,事务 A 读取的数据就是脏的。举例:订单 A 须要商品 A20 件,订单 B 须要商品 A10 件。仓库中有商品 A 库存是 20 件。订单 B 先查问, 发现库存够,进行扣减。在扣减的过程中,订单 A 进行查问,发现库存只有 10 个不够订单数量,抛出异样。这时候订单 B 提交失败了。库存数量又变成 20 了。这时候,仓库人员去查库存,发现数量是 20,可是订单 A 却说库存有余,这就让人很奇怪。

2. 不可反复读

复读指的是在一个事务内,最开始读到的数据和事务完结前的任意时刻读到的同一批数据呈现不统一的状况。举例:库房管理员查问商品 A 的数量,读取后果是 20 件。这是订单 A 出库,扣减了商品 10 件。这时管理员再去查商品 A 时,发现商品 A 的数量时 10 件和第一此查问的后果不同了。

3. 幻读

事务 A 在执行读取操作,须要两次统计数据的总量,前一次查问数据总量后,此时事务 B 执行了新增数据的操作并提交后,这个时候事务 A 读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。举例:操作员查问可生产单量 10 个,调用接口下发 10 个订单,事务 A 减少 10 个订单。操作员获取 10 个订单落库,查问 发现变成 30 个订单。

5 事务隔离级别

Read Uncommitted(未提交读) 一个事务能够读取到其余事务未提交的数据,会呈现脏读,所以叫做 RU,它没有解决任何的问题。

Read Committed(已提交读) 一个事务只能读取到其余事务已提交的数据,不能读取到其余事务未提交的数据,它解决了脏读的问题,然而会呈现不可反复读的问题。

Repeatable Read(可反复读) 它解决了不可反复读的问题,也就是在同一个事务外面屡次读取同样的数据后果是一样的,然而在这个级别下,没有定义解决幻读的问题。

Serializable(串行化) 在这个隔离级别外面,所有的事务都是串行执行的,也就是对数据的操作须要排队,曾经不存在事务的并发操作了,所以它解决了所有的问题。

6 解决数据读一致性

有两个计划能够解决读一致性问题:基于锁的并发操作(LBCC)和基于多版本的并发操作(MVCC)

6.1 LBCC

既然要保障前后两次读取数据统一,那么读取数据的时候,锁定我要操作的数据,不容许其余的事务批改就行了。这种计划叫做基于锁的并发管制 Lock Based Concurrency Control(LBCC)。

LBCC 是通过乐观锁来实现并发管制的。

如果事务 A 对数据进行加锁,在锁开释前,其余事务就不能对数据进行读写操作。这样并发调用,改成了顺序调用。对目前的大多数零碎来说,性能齐全不能满足要求。

6.2 MVCC

要让一个事务前后两次读取的数据保持一致,那么咱们能够在批改数据的时候给它建设一个备份或者叫快照,前面再来读取这个快照就行了。不论事务执行多长时间,事务外部看到的数据是不受其它事务影响的,依据事务开始的工夫不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。这种计划咱们叫做多版本的并发管制 Multi Version Concurrency Control(MVCC)。

MVCC 是基于乐观锁的。

在 InnoDB 中,MVCC 是通过 Undo log 中的版本链和 Read-View 一致性视图来实现的。

6.2.1 Undo log

undo log 是 innodb 引擎的一种日志, 在事务的批改记录之前, 会把该记录的原值先保存起来再做批改, 以便批改过程中出错可能复原原值或者其余的事务读取。undo log 是一种用于撤销回退的日志,在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件外面,当事务回滚时或者数据库解体时,能够利用 undo log 来进行回退。

对数据变更的操作不同,undo log 记录的内容也不同:

  • 新增一条记录的时候,在创立对应 undo 日志时,只须要把这条记录的主键值记录下来,如果要回滚插入操作,只须要依据对应的主键值对记录进行删除操作。
  • 删除一条记录的时候,在创立对应 undo 日志时,须要把这条数据的所有内容都记录下来,如果要回滚删除语句,须要把记录的数据内容生产相应的 insert 语句,并插入到数据库中。
  • 更新一条记录的时候,如果没有更新主键,在创立对应 undo 日志时,如果要回滚更新语句,须要把变更前的内容记录下来,如果要回滚更新语句,须要依据主键,把记录的数据更新回去。
  • 更新一条记录的时候,如果有更新主键,在创立对应 undo 日志时,须要把数据的所有内容都记录下来,如果要回滚更新语句,先把变更后的数据删掉,再执行插入语句,把备份的数据插入到数据库中。

undo log 版本链

每条数据有两个暗藏字段,trx_id 和 roll_pointer,trx_id 示意最近一次事务的 id,roll_pointer 示意指向你更新这个事务之前生成的 undo log。事务 ID:MySQL 保护一个全局变量,当须要为某个事务调配事务 ID 时,将该变量的值作为事务 id 调配给事务,而后将变量自增 1。

举例:

  • 事务 A id 是 1 插入一条数据 X,这条数据的 trx_id =1 ,roll_pointer 是空(第一次插入)。
  • 事务 B id 是 2 对这条数据进行了更新,这条数据的 trx_id =2 ,roll_pointer 指向 事务 A 的 undo log.
  • 事务 C id 是 3 又对数据进行了更新操作,这条数据的 trx_id =3,roll_pointer 指向 事务 B 的 undo log.

所以当多个事务串行执行的时候,每个事务批改了一行数据,都会更新暗藏字段 trx_id 和 roll_pointer,同时多个事务的 undo log 会通过 roll_pointer 指针串联起来,造成 undo log 版本链。

6.2.2 Read-View 一致性视图

InnoDB 为每个事务保护了一个数组,这个数组用来保留这个事务启动的霎时,以后沉闷的事务 ID。这个数组里有两个水位值:低水位 (事务 ID 最小值) 和 高水位(事务 ID 最大值 + 1); 这两个水位值就形成了以后事务的一致性视图(Read-View)

ReadView 中次要蕴含 4 个比拟重要的内容:

  • m_ids:示意在生成 ReadView 时以后零碎中沉闷的读写事务的事务 id 列表。
  • min_trx_id:示意在生成 ReadView 时以后零碎中沉闷的读写事务中最小的事务 id,也就是 m_ids 中的最小值。
  • max_trx_id:示意生成 ReadView 时零碎中应该调配给下一个事务的 id 值。
  • creator_trx_id:示意生成该 ReadView 的事务的事务 id。

有了这些信息,这样在拜访某条记录时,只须要依照下边的步骤判断记录的某个版本是否可见:

  • 如果被拜访版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值雷同,意味着以后事务在拜访它本人批改过的记录,所以该版本能够被以后事务拜访。
  • 如果被拜访版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在以后事务生成 ReadView 前曾经提交,所以该版本能够被以后事务拜访。
  • 如果被拜访版本的 trx_id 属性值大于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在以后事务生成 ReadView 后才开启,所以该版本不能够被以后事务拜访。
  • 如果被拜访版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就须要判断一下 trx_id 属性值是不是在 m_ids 列表中,如果在,阐明创立 ReadView 时生成该版本的事务还是沉闷的,该版本不能够被拜访;如不在,阐明创立 ReadView 时生成该版本的事务曾经被提交,该版本能够被拜访。
  • 如果某个版本的数据对以后事务不可见的话,那就顺着版本链找到下一个版本的数据,持续依照上边的步骤判断可见性,依此类推,直到版本链中的最初一个版本。如果最初一个版本也不可见的话,那么就意味着该条记录对该事务齐全不可见,查问后果就不蕴含该记录。

6.2.3 数据的查找形式

1. 快照读

快照读又叫一致性读,读取的是历史版本的数据。不加锁的简略的 SELECT 都属于快照读,即不加锁的非阻塞读,只能查找创立工夫小于等于以后事务 ID 的数据或者删除工夫大于以后事务 ID 的行(或未删除)。

2. 以后读

以后读查找的是记录的最新数据。加锁的 SELECT、对数据进行增删改都会进行以后读。

6.2.4 数据举例

如图所示:

事务 A id =1 初始化了数据事务 B id=2 进行了查问操作(MVCC 只读取创立工夫小于以后事务 ID 的数据或者删除工夫大于以后事务 ID 的行)事务 B 的后果是 (商品 A:10, 商品 B:5)

事务 C id =3 插入了商品 C 事务 B id=2 进行了查问操作(MVCC 只读取创立工夫小于以后事务 ID 的数据或者删除工夫大于以后事务 ID 的行)事务 B 的后果是 (商品 A:10, 商品 B:5)

事务 D id =4 删除商品 B 事务 B id=2 进行了查问操作(MVCC 只读取创立工夫小于以后事务 ID 的数据或者删除工夫大于以后事务 ID 的行)事务 B 的后果是 (商品 A:10, 商品 B:5)

事务 E id =4 批改商品 A 的数量 事务 B id=2 进行了查问操作(MVCC 只读取创立工夫小于以后事务 ID 的数据或者删除工夫大于以后事务 ID 的行)事务 B 的后果是 (商品 A:10, 商品 B:5)

所以当事务 E 提交后,以后读获取的数据和事务 B 读取的快照数据显著不同。

6.2.5 可解决问题

MVCC 能够很好的解决读统一问题,只能看到这个工夫点之前事务提交更新的后果,而不能看到这个工夫点之后事务提交的更新后果。而且升高了死锁的概率和解决读写之间梗塞问题。

7 小结

LBCC 和 MVCC 都能够解决读统一问题,具体应用哪种形式,要联合业务场景抉择最合适的形式,MVCC 和锁也能够联合应用,没有最好只有更好。


作者:陈昌浩

退出移动版