前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅谈 InnoDB 事务

浅谈 InnoDB 事务

作者头像
搬砖俱乐部
发布2019-09-12 12:18:53
6280
发布2019-09-12 12:18:53
举报
文章被收录于专栏:BanzClubBanzClub

傲是对付敌人的武器,特别是对弱小对手的一种气势压制,这种武器也有使用场景,不应常用。尽量看清自己,尽量看清大势,并不是你本人有多牛,只不过你有这个机会站在那个荣耀的位置,这也理应是你更加努力的位置。当然,你内心可以荣耀,但请不要高傲,因为无论你处于什么位置,你都太渺小了。

01

事务的描述

软件设计思想本质就是抽象的思维模式,拿数据库来说,它的本质就是存储系统。如果按照存储系统的数据模型来分类的话,通常包括文件、关系以及使用场景丰富的键值模型等,数据库属于关系模型。数据库作为关系模型有两个比较重要的特性:

一个特性是索引,抽象的说,对于查询某个数据来说,键值系统可以根据Key-Value映射关系,直接定位数据位置;而文件系统不存在快捷定位目标文件位置功能,只能逐级检索;而关系模型系统的索引,使用类似二叉树算法,数据组织和检索的算法的效率介于文件系统和键值系统之间。

另一个特性是事务,具体定义:【摘自百度百科】事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transaction和end transaction语句(或函数调用)来界定。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。

在计算机系统里,通常是多线程并发执行数据库操作,这就要求数据库不仅要满足单个事务内部执行的正确性,也要保证多个并发的事务正确性,以及对整个数据库数据的正确性和持久性的保护。事务的实现是数据库底层的一整套机制,也是数据库区别于文件系统的重要特性。

依据事务的定义,事务需同时满足四大特性:A(原子性),C(一致性),I(隔离性),D(持久性)。

  • 原子性:指的是数据库事务是不可分割的工作单元,整个事务中的所有操作要么全部成功,要么全部失败。
  • 一致性:指的是事务将数据库从一个一致性状态转变为另一个一致性状态,在任何一个事务的开始前和结束后,无论这个事务是成功提交,还是失败回滚,对于整个数据库状态都没有破坏。
  • 隔离性:一个事务的影响在该事务提交前对其他事务都不可见。InnoDB存储引擎提供多种隔离级别来实现隔离性,不同隔离级别特点不同,通常使用锁来实现,后续将展开讨论。
  • 持久性:事务一旦提交,结果就是永久性的,即使发生宕机,数据库也能恢复。InnoDB存储引擎不是在事务执行时,直接将数据写到磁盘的,而是通过后台线程负责刷盘。异步刷盘,可能会出现数据丢失,InnoDB提供重做日志机制,来保证持久性。

以上是为了解决数据库存储系统事务问题,抽象的定义了一套标准,而具体的实现是由不同数据库底层决定的。MySQL 5.1版本之前,默认存储引擎是MyISAM,是不支持事务的;在MySQL 5.1版本之后,默认的存储引擎是InnoDB,InnoDB通过undo、redo、行锁、表锁、MVCC等机制实现的事务。Oracle并不像MySQL提供插件式存储引擎,不过对于事务的实现和InnoDB有些类似,都提供行级锁、提供一致性的非锁定读等。下面,InnoDB存储引擎对于事务的实现是本篇文章的重点讨论内容,主要从锁机制、重做日志、MVCC等方面展开。

02

事务的实现

数据库要实现事务,还是要从事务的四大特性角度来看,当然,不同数据库对于事务的理解是不一样的。

数据库为了实现原子性,通过形如begin transaction和end transaction语句来界定事务开始和事务结束。当然对于结束有两种可能,要么成功提交,要么失败回滚。InnoDB 通过记录事务日志方式,将事务里所有数据的更新操作都存储起来,即重做日志。

先考虑事务成功提交的情况:当系统正常运行时,从事务的开始到结束,redo log记录了完整的事务日志。脏页数据和事务日志都通过刷盘的方式,存储到磁盘,事务日志可以通过同步或异步方式,刷到redo log file中;当系统发生宕机、断电等异常情况时,可能会存在事务提交,但 InnoDB 还未来得及刷盘的场景。这时在数据库重启时,将通过redo log的重做页内数据,这里redo log只恢复已完成事务提交的数据,保证了原子性。当然,如果重做日志也是异步写入文件的话,则存在redo log丢失的极端情况,这时将会丢失日志未刷盘,数据页未刷盘的部分数据。

现在考虑事务失败回滚的情况,InnoDB 通过undo log来记录,事务中的每个更新操作,undo log记录的是更新操作的相反语句,当需要数据回滚时,将通过undo log来完成回滚更新,保证事务的原子性。

对于数据库来说,无论怎样也不能出现错误数据,也就是说相比于丢失某个事务,出现错误数据更加不能容忍。如果事务不能满足一致性的话,那对于数据库来说,将不能保证数据库数据的正确性,那事务特性就没有存在的意义了,所以,个人觉得一致性应该是数据库事务最基本的特性。

隔离性就是为了实现一致性来设定的,通过了解不同隔离级别的特性,让开发人员在使用事务的开发过程中,避免因错误的理解,导致数据不一致情况的产生。下面,我们介绍下数据库的四种隔离级别及特性:

READ UNCOMMITTED(读未提交):在‘读未提交’的隔离级别下,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读到未提交的数据,也被叫做脏读。这个级别会导致很多问题,想想看,如果某个数据依赖未提交读做业务处理,后来这个未提交的事务回滚了,将会产生错误数据,即破坏了数据的一致性,所以,一般很少使用此级别的隔离级别。

READ COMMITED(读已提交):Oracle默认的隔离级别就是RC,也就是一个事务开始时,只能“看见”已提交事务所做的修改,但会导致同一事务内,两次读到不一致的数据,即不可重复读。这也破坏了事务的一致性。

REPEATABLE READ(可重复读):MySQL默认的隔离级别,在RR下解决了脏读和不可重复读,多次重复读同样的记录结果是一致的。理论上,可重复读还是无法解决幻读问题,即当某个事务在读取某个范围内的记录时,另外一个事务在该范围插入了一条新的记录,当之前的事务再次读取该范围时,会产生幻行(读取时多出来一行数据),这也属于破坏了一致性,InnoDB通过多版本并发控制(MVCC Multiversion Concurency Control)解决了幻读问题,稍后将进一步讨论MVCC。

SERIALIZABLE(串行化):‘串行化’是最高的隔离级别,它通过强制事务的串行执行,避免了前面说的幻读。通常使用锁来实现每一行数据上的串行,在实际应用中,可能会导致大量的锁争用和超时,实际应用中很少用到这个隔离级别。

具体 InnoDB 是如何实现一致性和隔离性的呢?通常在解决并发时对统一资源的访问控制使用的技术是锁,InnoDB除了锁机制,还提供了MVCC机制。

不同数据库实现的锁机制是不一样的,如:MyISAM只支持表锁。InnoDB 提供多粒度的锁用以提高数据库的并发性,支持行级锁,也支持表级锁,默认情况下是采用行级锁。另外,按照不同的操作模式,还分读锁和写锁,读锁互相之间不冲突,写锁之间冲突,下面我们来介绍一下InnoDB的锁机制。

InnoDB存储引擎实现了两个标准的行级锁:

  • 共享锁(S Lock):获得某行共享锁的事务,可以读该行数据;
  • 排它锁(X Lock):获得某行排它锁的事务,可以删除或者更新该行数据;

当一个事务T1获得了行r的共享锁,那么事务T2可以立即获得行r的共享锁,这种情况称为锁兼容;若当其他事务想获得行r的排它锁,则必须等待事务T1、T2释放行r上的共享锁,这种情况称为锁不兼容。

此外,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁。意向锁为表级别的锁,设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型,其支持两种意向锁:

  • 意向共享锁(IS Lock):事务想要获得一张表中某几行的共享锁;也就是说:一个事务要获得某行的共享锁之前,必须先对表加IS锁;
  • 意向排它锁(IX Lock):事务想要获得一张表中某几行的排它锁;也就是说:一个事务要获得某行的排它锁之前,必须先对表加IX锁。

意向锁是 InnoDB 自动加的,不需要用户干预,事务结束后会自行解除。意向锁的存在,使每当事务来检查数据行的锁持有情况之前,先检查意向锁是否存在,将可能存在阻塞的情况提升到表级别,不需要每次都遍历细粒度的行锁,提升了InnoDB存储引擎的性能。

InnoDB 行锁的实现是通过给索引上的索引项加锁来实现的,这个特性决定了,只有通过索引条件检索数据时,才能使用行锁,否则使用表级锁,这是SQL优化时需要重点考虑的情况,具体锁的算法如下:

Record Locks 锁定的对象是主键、非空的唯一索引对应的索引记录。如果表没有定义索引,则InnoDB存储引擎会创建一个隐藏的聚簇索引,并在此索引上进行记录锁定。

Gap Locks 锁定的是索引记录之间的间隙,或者是第一个索引之前、最后一个索引之后的间隙。在同一个间隙上,允许不同事务同时持有该间隙上的间隙锁,间隙锁锁定间隙的目的是,防止其他事务对此间隙进行插入记录。间隙锁也分为共享锁和排他锁,不过它们之间没有区别。

Next-Key Locks 是 Record Locks 和 Gap Locks的组合,当查找或扫描一个表中的索引时,行级锁实际上是索引记录锁,而Next-Key Locks锁定的是索引记录和该索引记录到上一个索引之间的间隙,也就是说,如果一个事务在记录R上,具有共享锁和排它锁,则其他事务不能在记录R到上一个索引记录的间隙中插入记录。在InnoDB存储引擎里,Next-Key Locks只用于RR隔离级别,用以解决幻读问题。

当查询的索引是唯一索引时,Next-key lock会进行优化,降级为Record Lock,此时Next-key lock仅仅作用在索引本身,而不会作用于gap和下一个索引上。

插入意向锁是由Insert语句进行行插入之前设置的一种间隙锁,是意向排它锁的一种。插入意向锁表示多个事务向同一间隙插入时,只要插入的行互相不冲突,就不会发生锁等待。

自增锁是事务插入时自增列上特殊的表级别的锁。如果一个事务正在向表中插入值,则其他事务必须等待。不过,InnoDB实现的自增锁,仅仅持有到当前SQL语句的末尾,而不是整个事务。

在 InnoDB 事务中,行锁是在需要时才加上的,但并不是不需要了立即释放,而是要等待事务结束时才释放,这就是两阶段锁协议。所以,我们在实际开发过程中,遇到可能发生锁冲突的操作语句,尽量放到事务的后面进行,以减少锁的持有时间。尽管我们在事务中,非常注意锁的竞争关系,但在真实的数据库并发环境下,仍然可能会出现多个线程之间的资源的循环依赖,涉及到的线程都在等待别的线程释放资源,就会导致几个线程都进入了无限等待的状态,这就是死锁。InnoDB提供了两种方式,解决死锁问题:

  • 设置锁等待超时时间:当出现锁等待时,进入等待,直到超时,然后线程返回失败并回滚。超时时间通过 innodb_lock_wait_timeout 配置参数设置,默认为50s。这个超时时间不太好界定,设置太短的话,可能会误伤不是死锁的锁等待;设置太长,也会使其他线程等待时间过长,才失败。
  • 死锁检测:InnoDB可以自动检测事务死锁并回滚事务,以打破死锁。通过设置 innodb_deadlock_detect 配置是否开启死锁检测,默认是ON。每当事务因请求锁而进入等待时,将触发wait-for graph算法进行主动死锁检测,死锁检测也需要耗费系统资源。当多个线程竞争同一个锁时,死锁检测会变慢,而影响数据库性能,这时可以禁用死锁检测。

当然,无论哪种方式都不是最优的解决方案,需要我们根据实际业务场景结合着InnoDB锁的特点,来制定具体的实现方案,这就体现了整体设计和优化的重要性了。

  • MVCC

InnoDB存储引擎不单单实现了行级锁和表级锁,还实现了多版本并发控制(MVCC Multiversion Concurency Control),来提升数据库的并发度,可以认为MVCC是行级锁的一个变种。MVCC-多版本指的是行记录版本,也就是同一时刻可能存在某一记录的多个版本,记录的版本号通过隐藏的事务id列来表示,由于事务id是递增的,因此行记录版本也是自增的。同一记录的多个版本不是通过存放多条记录的版本来实现的,这样会使用很大一部分磁盘空间。InnoDB存储引擎通过回滚段内,保存前一个版本的undo log,通过当前记录加上undo log可以构造出记录的前一个版本,从而实现同一记录的多版本。undo log不是永远保存在公共表空间的回滚段中的,当没有事务引用版本记录时,就可以回收这部分回滚段,以供其他事务使用。

MVCC使用场景:

a、实现回滚

  • 事务通过排他锁方式,修改行记录;
  • InnoDB 存储引擎把修改记录SQL语句的反向SQL记录在undo log中,通过回滚指针与主数据关联;
  • 事务成功则不做其他操作,如果事务失败,则根据undo log恢复到之前状态(rollback).

b、一致性非锁定读

InnoDB存储引擎的‘一致性非锁定读’是通过MVCC机制实现的,主要用来优化行数据读取的并发性能。对于一致性非锁定读,当读取当前执行时间的行数据时,即使当前行数据正在执行DELETE和UPDATE操作,读取操作不会等待行上锁的释放,而是会读取当前行的一个快照数据,这里的快照数据指的是当前行的历史版本。具体操作如下图所示:

通常情况下,每一行可能存在多个版本,这也是为啥叫多版本并发控制(MVCC)的原因。MVCC只在 RC 和 RR 两个隔离级别下工作。其他两个隔离级别和MVCC不兼容, 因为 RU 总是读取最新的数据行,而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。MVCC机制对于 RC 与 RR 的主要区别在于建立Read View的时机不同,我们先回顾一下RC和RR的特性,RC解决了脏读,仍存在不可重复读和幻读;RR则不存在脏读、不可重复读和幻读。其中RR的幻读是通过间隙锁实现的,而RC下是没有间隙锁的。RC是可以看到其他事务提交后的数据的,也就是每次读取时,都会读取最新的行版本数据,而RR只能读取到事务开始时的行版本数据。这是因为,在 RC 隔离级别下,每次语句执行都关闭ReadView,然后重新创建一份ReadView。在RR下,事务开始后第一个读操作创建ReadView,一直到事务结束关闭

InnoDB存储引擎对于持久性的保证是通过日志实现的,包括redo log,undo log,binlog等。通过redo log的工作原理我们知道,当事务提交之后,所有事务的日志通过log buffer持久化到了redo log file上,这时即使发生数据库宕机,也可以根据redo log对数据进行恢复,再继续持久化。另外,binlog也是对数据库的数据的持久化,当数据库文件被损坏时,可以使用binlog对数据库进行恢复,备份。持久性是所有数据库的一个重要特性,只有数据落到磁盘上,才能保证数据不丢失,实现持久化存储。

03

相关MySQL命令

1、MySQL提供的一个用于查看InnoDB存储引擎信息的工具:

show engine innodb status;
| InnoDB |      |=====================================2019-09-10 09:37:26 0x7f1ed01a9700 INNODB MONITOR OUTPUT=====================================Per second averages calculated from the last 4 seconds-----------------BACKGROUND THREAD-----------------srv_master_thread loops: 25 srv_active, 0 srv_shutdown, 1642377 srv_idlesrv_master_thread log flush and writes: 1642402----------SEMAPHORES----------OS WAIT ARRAY INFO: reservation count 75OS WAIT ARRAY INFO: signal count 73RW-shared spins 0, rounds 73, OS waits 37RW-excl spins 0, rounds 1, OS waits 1RW-sx spins 0, rounds 0, OS waits 0Spin rounds per wait: 73.00 RW-shared, 1.00 RW-excl, 0.00 RW-sx------------TRANSACTIONS------------Trx id counter 4413Purge done for trx's n:o < 4412 undo n:o < 0 state: running but idleHistory list length 14LIST OF TRANSACTIONS FOR EACH SESSION:---TRANSACTION 421245700659040, not started0 lock struct(s), heap size 1136, 0 row lock(s)---TRANSACTION 421245700659960, not started0 lock struct(s), heap size 1136, 0 row lock(s)--------FILE I/O--------I/O thread 0 state: waiting for completed aio requests (insert buffer thread)I/O thread 1 state: waiting for completed aio requests (log thread)I/O thread 2 state: waiting for completed aio requests (read thread)I/O thread 3 state: waiting for completed aio requests (read thread)I/O thread 4 state: waiting for completed aio requests (read thread)I/O thread 5 state: waiting for completed aio requests (read thread)I/O thread 6 state: waiting for completed aio requests (write thread)I/O thread 7 state: waiting for completed aio requests (write thread)I/O thread 8 state: waiting for completed aio requests (write thread)I/O thread 9 state: waiting for completed aio requests (write thread)Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,ibuf aio reads:, log i/o's:, sync i/o's:Pending flushes (fsync) log: 0; buffer pool: 0250 OS file reads, 348 OS file writes, 173 OS fsyncs0.00 reads/s, 0 avg bytes/read, 0.00 writes/s, 0.00 fsyncs/s-------------------------------------INSERT BUFFER AND ADAPTIVE HASH INDEX-------------------------------------Ibuf: size 1, free list len 0, seg size 2, 0 mergesmerged operations:insert 0, delete mark 0, delete 0discarded operations:insert 0, delete mark 0, delete 0Hash table size 34673, node heap has 0 buffer(s)Hash table size 34673, node heap has 0 buffer(s)Hash table size 34673, node heap has 0 buffer(s)Hash table size 34673, node heap has 0 buffer(s)Hash table size 34673, node heap has 0 buffer(s)Hash table size 34673, node heap has 0 buffer(s)Hash table size 34673, node heap has 0 buffer(s)Hash table size 34673, node heap has 0 buffer(s)0.00 hash searches/s, 0.00 non-hash searches/s---LOG---Log sequence number 2676627Log flushed up to   2676627Pages flushed up to 2676627Last checkpoint at  26766180 pending log flushes, 0 pending chkp writes104 log i/o's done, 0.00 log i/o's/second----------------------BUFFER POOL AND MEMORY----------------------Total large memory allocated 137428992Dictionary memory allocated 138609Buffer pool size   8191Free buffers       7901Database pages     290Old database pages 0Modified db pages  0Pending reads      0Pending writes: LRU 0, flush list 0, single page 0Pages made young 0, not young 00.00 youngs/s, 0.00 non-youngs/sPages read 219, created 71, written 2060.00 reads/s, 0.00 creates/s, 0.00 writes/sNo buffer pool page gets since the last printoutPages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/sLRU len: 290, unzip_LRU len: 0I/O sum[0]:cur[0], unzip sum[0]:cur[0]--------------ROW OPERATIONS--------------0 queries inside InnoDB, 0 queries in queue0 read views open inside InnoDBProcess ID=10466, Main thread ID=139770325542656, state: sleepingNumber of rows inserted 32, updated 2, deleted 0, read 650.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s----------------------------END OF INNODB MONITOR OUTPUT============================

包含死锁信息,事务信息,缓冲池和内存信息,I/O信息,日志及LSN信息等。

2、通过InnoDB Plugin可以监控数据库当前事务及锁的情况:INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS。

select * from information_schema.innodb_locks;select * from information_schema.innodb_trx;   select * from information_schema.innodb_lock_waits;

  1. 《MySQL技术内幕:InnoDB存储引擎》
  2. 《大规模分布式存储系统:原理解析与架构实现》
  3. 《MYSQL内核:InnoDB存储引擎》
  4. 极客时间-《MySQL实战45讲》林晓彬
  5. https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html
  6. https://dev.mysql.com/doc/refman/5.7/en/innodb-locks-set.html
  7. https://www.ywnds.com/?p=4949
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-09-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 BanzClub 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档