Skip to main content

moregeek program

如何实现数据库读一致性_京东云官方的博客-多极客编程

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却说库存不足,这就让人很奇怪。

如何实现数据库读一致性_mvc

2.不可重复读

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

如何实现数据库读一致性_mvc_02

3.幻读

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

如何实现数据库读一致性_数据库_03

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 数据举例

如何实现数据库读一致性_数据_04

如图所示:

事务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和锁也可以结合使用,没有最好只有更好。


作者:陈昌浩

©著作权归作者所有:来自51CTO博客作者京东云官方的原创作品,请联系作者获取转载授权,否则将追究法律责任

数据火器库 - 八卦系列之借老枪谈可靠性_阿里云情报局的博客-多极客编程

​​来源:云数据库技术​​数据库打工仔喃喃自语的八卦1. 老枪:Db2/z和可靠性2. K.I.S.S (Keep it Simple, Stupid!)3. 系统验证和测试:猪肉出厂的质检章数据库的可靠性1、数据库里的老枪 - Db2 for zOS上次聊了瑞士军刀SQLite, 从年纪上SQLite出生于大数据和手机时代之前,对比后来的大数据引擎和云原生数据库,SQLite可谓个头不大,辈分不

lua脚本在redis事务中的应用实践_京东云官方的博客-多极客编程

使用过Redis事务的应该清楚,Redis事务实现是通过打包多条命令,单独的隔离操作,事务中的所有命令都会按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务中的命令要么全部被执行,要么全部都不执行(原子操作)。但其中有命令因业务原因执行失败并不会阻断后续命令的执行,且也无法回滚已经执行过的命令。如果想要实现和MySQL一样的事务处理可以使用Lua脚本来实现,Lua脚本中

mysqldump备份期间做ddl会发生什么_mysql dba攻坚之路的博客-多极客编程

MDL锁当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。MDL 会直到事务提交才释放,在做表结构变更的时候,你一定要小心不要导致锁住线上查询和更新。读写互斥读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始

docker mysql8修改root密码方法_winjayx的博客-多极客编程

Docker MySQL8修改root密码方法 1. 启动脚本信息 [root@mysql]# cat docker_start_Mysql_WinJay.sh docker rm -f MySQL8.0_DB docker container run -d \ --volume /etc/localtime:/etc/localtime:ro \ --volume `pwd`/

sql数据分析—查询银行账户余额明细列表_吴明课堂的博客-多极客编程

从系统导出了公司银行卡的初期信息和收款付款明细数据,接下来要用SQL去对它做查询和分析。一. 已有数据图示:账户期初金额收款明细付款明细​二. 查询目标数据列表图示:想要实现的查询结果​三. 银行余额算法逻辑说明:上期结转金额(查询结果第一行的银行余额) = 查询日前一日的余额,本处业务发生日为2022/07/01,所以上期结转金额等于期初金额。若查询开始日为2022/08/01,则上期结转金额为

mysql面试题(三)_蓦然的博客-多极客编程

27、索引的底层实现原理和优化B+树, 经过优化的B+树主 要是在所有的叶子结点中增加了指向下一个叶子节点的指针,  因此InnoDB 建 议 为大部分表使用默认自增的主键作为主索引。28、什么情况下设置了索引但无法使用1、  以 “ % ” 开 头 的 LIKE 语 句 ,  模 糊 匹 配2 、  OR 语 句 前 后 没 有 同 时 使 用 索 引3 、数 据 类 型 出 现 隐式转化(如

如何实时计算中证1000指数的主买/主卖交易量_dolphindb的博客-多极客编程

主买是指以卖方的报价成交,主卖是指以买方的报价成交。 一般来说,主动买入就是资金流入,主动卖出就是资金流出,所以实时统计主买/主卖交易量能够实时监控资金的流入流出情况。本文基于中证 1000 指数,介绍如何利用 DolphinDB 流数据处理框架,实时高效计算中证1000指数的主买/主卖交易量。本文包含场景概述、实现思路、实时计算主买/主卖交易量、结果展示等部分。1. 场景概述本文介绍如何使用 D

【数据库数据恢复】asm磁盘组掉线导致oracle asm实例无法挂载的数据恢复案例_sun的博客-多极客编程

数据库数据恢复环境: 4个硬盘组成Oracle ASM磁盘组。 数据库故障&分析: ASM磁盘组掉线 ,ASM实例不能mount。管理员联系我们数据恢复中心进行oracle数据库的数据恢复。 对故障ASM磁盘组的磁盘进行分析。北亚数据库数据恢复工程师取出ASM元数据进行分析发现ASM存储元数据损坏,diskgroup无法mount。 可以尝试重组ASM存储空间,然后从ASM磁盘组中导出数据

数据火器库 - 八卦系列之借老枪谈可靠性_阿里云情报局的博客-多极客编程

​​来源:云数据库技术​​数据库打工仔喃喃自语的八卦1. 老枪:Db2/z和可靠性2. K.I.S.S (Keep it Simple, Stupid!)3. 系统验证和测试:猪肉出厂的质检章数据库的可靠性1、数据库里的老枪 - Db2 for zOS上次聊了瑞士军刀SQLite, 从年纪上SQLite出生于大数据和手机时代之前,对比后来的大数据引擎和云原生数据库,SQLite可谓个头不大,辈分不

oh-my-zsh,让你的终端从未这么爽过_雍州无名的博客-多极客编程

1.oh my zshshell的类型有很多种,linux下默认的是bash,虽然bash的功能已经很强大,但对于以懒惰为美德的程序员来说,bash的提示功能不够强大,界面也不够炫,并非理想工具。 而zsh的功能极其强大,只是配置过于复杂,起初只有极客才在用。后来,有个穷极无聊的程序员可能是实在看不下去广大猿友一直只能使用单调的bash, 于是他创建了一个名为oh-my-zsh的开源项目... 自

docker mysql8修改root密码方法_winjayx的博客-多极客编程

Docker MySQL8修改root密码方法 1. 启动脚本信息 [root@mysql]# cat docker_start_Mysql_WinJay.sh docker rm -f MySQL8.0_DB docker container run -d \ --volume /etc/localtime:/etc/localtime:ro \ --volume `pwd`/

在windows10上通过nomachine连接kali的远程桌面_雍州无名的博客-多极客编程

对于需要经常远程到X windows的网友来说,VNC与Xmanager经常被使用,而NoMachine也是不错的选择之一,该工具简单,易用,尤其是在低带宽、慢速率的网络环境下工作性能良好。本文描述了NoMachine的快速安装与使用。1.NoMachine介绍NoMachien能使用户从另一台计算机访问和控制远程的Windows,Linux PC或Mac电脑,实现工作或娱乐等操作。NoMachi