小黑黑碎碎念,哎,最近有点忙,准备考试好辛苦,明天要搬家了,好辛苦!!想了这周,还是想不开,起来更新,周耕停不下来。偷懒,修改一下之前的一篇历史文章,重新发布。ps:发这篇文章的时候,我正在打季后赛,JD,加油!!P0意外:余额扣多了!这是一个真实的生产事件,事件起因如下:存在一个现有的交易系统,以及对应的账户余额,余额从账户中扣除,余额从账户中增加。为了保证资金安全,在扣除余额时,需要将现有余额与扣除金额进行比较。如果扣款金额大于当前余额,则扣款余额不足,扣款失败。account表(其他字段省略)结构如下:=utf8mb4_bin;减少balance时,sql词序如下:updatethebalancesqlwordorderps:看到上面的词序,是不是有个小问号?为什么相同的查询出现这么多次?其实这些SQL的词序不在同一个方法中,有的方法是提取出来重用的,所以有些相同的查询结果是传不下去的,只好重新从数据库中查询。为了防止并发更新余额,在t3时刻,使用写锁锁定行记录。如果加锁成功,如果其他线程也执行到t3,就会阻塞,直到上一个线程的事务提交。在t5时刻,进入next方法,再次获取账户余额,然后将余额与Java方法中扣除的金额进行比较。如果余额充足,则在t7时刻进行更新操作。上面的SQL词序看似没有问题,其实是一样的。账户系统已经生产运行了很长时间,没有出现任何问题。但是这里需要说一个前提,系统数据库是Oracle。但是从上面的表结构我们可以知道,这次数据库切换到了MySQL,系统的其他任何代码和配置都不会修改(sql有细微的变化)。这种情况下并发执行多扣余额,即实际余额明显小于扣款金额,但进行了余额更新操作,最终余额变为负数。接下来,让我们重现并发情况。假设有两个事务执行单词序列,执行顺序如图所示。注:数据库使用MySQL,默认事务隔离级别为RR。数据库记录是id=1balance=1000,假设当时只执行了这两个事务。读者可以先想想t2、t3、t4、t5、t6、t11的余额。把答案贴在下面事务隔离级别RR下。事务1的查询结果为:t2(1,1000)t4(1,1000)t6(1,1000)事务2的查询结果为:t3(1,1000)t5(1,900)t11(1,1000)结果和你想的一样吗?然后把事务隔离级别改成RC,想想t2、t3、t4、t5、t6、t11时刻的余额。再次将答案粘贴到事务隔离级别RC下。事务1的查询结果为:t2(1,1000)t4(1,1000)t6(1,1000)事务2的查询结果为:t3(1,1000)t5(1,900)t11(1,900)查询事务1的结果大家应该没有问题。主要问题应该是事务2,为什么改变事务隔离级别后结果不一样?先有问题,了解下MySQL的相关原理。看完之后,你就会明白这一切。MVCCConsistencyViewSnapshotReadandCurrentReadMVCC先看一个简单的例子,事务隔离级别为RR,id=1balance=1000updatesequence事务1会更新id=1的记录余额为900,然后事务2在查询t5时刻这一行的记录结果,显然这一行的记录应该是id=1balance=1000。如果t5查询到最新的结果id=1balance=900,这读取的是事务1的未提交数据,显然不符合当前事务隔离级别。从上面的例子我们可以看出id=1的记录有两个版本,交易1版本记录为balance=1000,交易2版本记录为balance=900。对于以上功能,MySQL采用了MVCC机制来实现功能。MVCC:Multiversionconcurrencycontrol,多版本并发控制。摘自淘宝数据库月报的一段解释:多版本控制:指提高并发性的技术。在最早的数据库系统中,只有读和读可以并发,读和写、写和读、写和写必须是阻塞的。引入多个版本后,只有写入和写入块互为阻塞,其他三个操作可以并行化,大大提高了InnoDB的并发性。在内部实现上,与Postgres在数据行上实现多个版本不同,InnoDB是在undolog中实现的,通过undolog可以获取数据的历史版本。可以将检索到的数据的历史版本提供给用户读取(根据隔离级别的定义,部分读取请求只能看到较旧的数据版本),也可以在回滚时覆盖数据页上的数据。InnoDB内部记录了一个全局的活跃读写事务数组,主要用于判断事务的可见性。可以看出,MVCC主要是用来提高并发性的,也可以用来读取旧版本数据。在学习MVCC原理之前,我们首先需要了解MySQL的记录结构。行记录如上图所示,account表有一条行记录,除了真正的数据外,还会有三个隐藏字段记录额外的信息。DB_TRX_ID:交易ID。DB_ROLL_PTR:回滚指针,指向undolog。ROW_ID:rowid,与本次无关。MySQLInnoDB中的每一个事务都会有一个唯一的事务ID,事务ID会在事务开始时应用到InnoDB事务系统中,并严格按照顺序递增。每次事务更新数据时,都会产生一个新的数据版本,然后将当前事务id赋给当前记录的DB_TRX_ID。而数据更新记录(1,1000---->1,900)会记录在undolog(回滚日志)中,然后使用当前记录的DB_ROLL_PTR指向undolog。这样MySQL就可以通过DB_ROLL_PTR找到undolog来推导出上一个版本记录的内容。查找过程如下:如果查找过程需要知道V1版本记录,先根据当前版本V3的DB_ROLL_PTR找到undolog,再根据undolog内容计算出上一版本V2。以此类推,终于找到了V1的版本记录。V1和V2不是物理记录,不存在,只有逻辑意义。一行数据记录的多个版本可能同时存在,但并不是所有记录对当前事务都是可见的。否则,上面的t5可能会查询到最新的数据。所以在查找数据版本的时候,MySQL必须判断数据版本是否对当前事务可见。一致性视图MySQL会在事务开始后(不是立即)创建一个一致性视图,在这个视图中,所有活跃的事务(还没有提交的事务)都会被保存。假设当前事务保存活跃事务数组如下图所示。view数组在判断版本对当前事务是否可见时,根据以下规则进行判断:如果版本事务id小于当前活跃事务id数组的最小值,例如版本id为40,小于活动事务数组的最小值45。这意味着事务的当前版本已经提交,当前版本对当前事务可见。如果版本事务id大于当前活跃事务数组的最大值,例如版本事务id为100,大于数组中最大事务id90。说明这个版本是在当前事务创建之后生成的,所以这个版本对当前事务是不可见的。如果版本事务id是当前活跃的数组事务之一,比如版本事务id为56,表示记录版本所属的事务还没有提交,所以这个版本对当前事务不可见.如果版本事务id不是当前active数组事务之一,而是事务id在active数组的最小值和最大值之一,比如事务id57。表示当前记录事务已经提交,所以这个版本对当前交易是可见的。如果versiontransactionid是当前的transactionid,说明该行数据被当前的transaction改变了,当然必须是可见的。4这个规则可能比较复杂,用上图更容易理解。以上判断规则可能比较抽象,看不懂也没关系。通俗一点解释:未提交事务产生的记录版本是不可见的。提交的事务生成记录版本在视图生成之前是可见的。视图生成后,新交易生成的记录版本不可见。自交易更新始终可见。一致性视图只会在RR和RC下生成。对于RR,一致性视图会在第一条查询语句的时候生成。使用RC,将为每个查询语句重新生成视图。Currentread和snapshotreadMySQL使用MVCC机制读取以前版本的数据。这些旧版本记录不会也不能再修改,就像快照一样。所以我们称这个查询为快照读取。当然,并非所有查询都是快照读取。select....forupdate/insharemode等锁定查询只会查询当前记录的最新版本。我们将此查询称为当前读取。说完问题分析的原理,我们回过头来分析一下出现上述查询结果的原因。这里我们把上面的答案再贴一遍。事务隔离级别为RR,在t2和t3时刻,两个事务由于查询语句分别建立了一致视图。在t4时刻,由于事务1使用select..forupdate锁定了id=1的行,所以得到了最新的结果。在时间t5,由于该行已经被锁定,事务2必须等待事务1释放锁才能继续。根据t6时刻一致的看法,其他事务提交的版本无法读取,所以数据没有变化。t8时刻余额减去100,t9时刻提交交易。此时最新版本记录为id=1balance=900。由于事务1已经提交,行锁被释放,t5成功获取到锁。由于当前读取的是t5,所以查询的结果是最新版本的数据(1,900)。重要的一点来了。目前这条记录的最新版本是(1,900),但是最新版本的事务id是事务2创建后未提交的事务,位于active事务数组中。所以最新的记录版本对事务2是不可见的,没办法只能根据undolog读取之前的版本记录(1,1000)。这条版本记录只对事务2可见,所以t11的记录是(1,1000)。而当我们将事务隔离级别改为RC时,每次都会重新生成一致的视图。因此,一致性视图在时间t11重新生成。此时事务1已经提交,当前最新版本的记录对事务2可见,所以t11的结果会变成(1,900)。总结MySQL默认的事务隔离级别是RR,每行数据(InnoDB)可以有多个版本,每个版本都有一个唯一的事务id。MySQL通过一致的视图确保数据版本的可见性。相关规则总结如下:对于RR事务隔离级别,普通查询只能查到事务开始前已经提交的版本数据。对于RC事务隔离级别,普通查询可以找到查询语句开始前已经提交的版本数据。当前读取始终读取最新版本的数据。参考资料[1]https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html[2]http://mysql.taobao.org/monthly/2017/12/01/[3]http://mysql.taobao.org/monthly/2018/11/04/[4]https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html这个文章转载自微信公众号“程序通识”,可通过以下二维码关注。转载请联系项目总监公众号。
