《高性能Mysql》-事务
一、什么是事务?
一组SQL查询,只有所有语句都能成功才执行,否则全部不执行。经典银行示例:支票和储蓄是两张数据表,用户从支票账户转200到储蓄账户,分3步:检查支票账户是否有200,支票账户余额减200,储蓄账户余额加200;这三个步骤就需要在一个事务中。
事务由存储引擎实现,如InnoDB就是事务型引擎。不推荐事务中混合使用存储引擎:如果操作A和操作B处于同一个事务T中;其中操作A用于修改数据表A,表A的存储引擎是InnoDB,支持事务;操作B用于修改数据表B,表B的存储引擎是MyIASM,不支持事务;如果事务T需要回滚,则只有表A能够回滚,表B不行。
二、事务需要满足ACID
1)原子性:一组sql语句,要么一起成功,要么都不执行。
2)一致性:多个数据源的数据一致,比如储蓄账户和支票账户。
3)持久性:断电数据不丢失。
4)隔离性:并发情况下,一个事务所做修改在提交之前对其他事务不可见;下面将详解持久性和隔离性。
2.1 持久化与事务日志与双写缓存
满足持久性意味着需要将数据写入(flush)到磁盘。常规的持久化方式是把InnoDB缓冲池的脏块(有数据修改的数据块)直接写入到磁盘,数据通常映射到表空间的随机位置,因此磁盘写入需要多次随机IO,随机IO意味着把磁头移动到正确位置,等待读写后再回到开始位置,这个过程是相对耗时的。那么事务日志就是一种在满足持久化的前提下提高写入速度的方式,把数据文件的随机IO转换为几乎顺序的日志文件IO和数据文件IO:存储引擎在修改表数据时只需修改内存拷贝,并把修改行为采用追加方式持久在硬盘上的事务日志,数据持久化在后台慢慢执行。追加意味着写日志只操作磁盘一小块区域内的顺序IO,不像随机IO在磁盘多个地方移动磁头,速度更快。如果系统崩溃,存储引擎根据日志恢复数据。事务日志也成为预写式日志,采用事务日志时修改数据需要写两次磁盘。
对于后台运行的数据持久化,当脏页比例超过阈值会触发磁盘写入,通过合并写入(也称为延迟写入)使得数据写入更顺序,称为lazy模式。有两种合并方式:1)同一条数据在内存中被多次改写,但最终通过一次写入到磁盘;2)不同数据在内存中被修改,批量写入到磁盘。当事务日志没有足够空间时,InnoDB进入激烈刷写模式。由此可见,事务日志大小决定了持久化数据的间隔时间。
事务日志也有缓冲区,InnoDB执行写操作时,会写一条变更记录到日志缓冲区;当缓冲区满了或事务提交了,InnoDB把日志缓冲区的内容写入到磁盘上的事务日志。写入过程中用互斥锁锁住日志缓冲区,写入到某位置,然后移动剩下的条目到缓冲区前面,之后再解锁。这里的日志缓冲区不是基于页的,存储效率更高。事务日志通常使用一组文件作为循环日志;采用循环方式写入,当记录到日志尾部则重新跳转到头部,但不会覆盖未应用到数据文件的日志记录。
服务器崩溃、bug等可能导致数据页没写完整,进而造成数据损坏,双写缓冲就是应对这种情况,力图保证数据完整性。当数据页尾部的校验和与数据页内容不匹配时,表明页面损坏。双写缓冲是一块1.6M的连续内存,可保存100个数据页;双写缓冲中保存最近写入数据页的备份。也就是说,当innoDB把数据从缓冲池写入到磁盘时,首先写入到双写缓冲,然后再写到磁盘。双写缓冲和事务日志一起保障数据成功持久化,双写缓冲包括完整的数据页,因此事务日志只是页面的二进制变化量。可通过配置innodb_doublewrite来关闭双写缓冲。
2.2 隔离性、并发、锁
隔离性与并发强相关,如果所有请求串行处理,则无需考虑隔离性。并发指多个查询同时修改数据,并发读写;锁用于并发控制。
2.2.1 锁的分类:
1)共享锁和排他锁,对应读锁和写锁;读锁允许多用户可同时读取同一资源,但阻塞写锁;写锁会阻塞其他读锁和写锁;写锁优先级高,锁队列中可插到读锁前面。
2)根据锁粒度不同分为表锁和行锁:行锁锁定的数据量少,系统并发度高,同时,加锁/解锁/检查是否锁定等系统开销也高。多数数据库只提供行锁,mysql不同存储引擎锁策略/粒度不同。存储引擎和服务层都可以管理表锁,如服务层在alter table是给数据表加表锁。行锁只在存储引擎层实现,可见服务器层和存储引擎层有各自的并发控制机制。
锁定方式分为隐式和显式锁定;显式锁定通过lock/unlock tables语句控制锁定与解锁,不常用;隐式锁定在事务过程中根据隔离级别执行锁定,在commit/rollback时候解除锁定。
2.2.2 死锁
多个事务争用同一资源,并试图以不同顺序锁定资源。举例:三个资源是草莓、樱桃和芒果;宝宝的事务是先吃草莓,再吃樱桃,最后吃芒果,此时宝宝已经对草莓和樱桃加写锁,准备给芒果加写锁;妈妈的事务是先吃芒果,再吃草莓,最后吃樱桃,当前已经对芒果加写锁,准备给草莓加写锁;我们知道事务中写锁要提交事务时才能释放,因此妈妈和宝宝都等不到锁,并且无法完成事务并提交,进入死锁状态。
数据库如何处理死锁:回滚持有最少行级写锁的事务重新执行,如上面的例子,宝宝已经持有两个写锁,妈妈持有一个,因此妈妈的事务回滚,把芒果吐出来,这样宝宝就可以对芒果加写锁,完成事务并提交;然后妈妈再执行。其他方式如死锁检测,检测到死锁的循环依赖后立即返回错误;超时机制,查询等待时间到达锁等待超时后放弃锁请求。
2.3 InnoDB的多版本并发控制MVCC
MVCC在每行数据后面添加两个列存储行的创建/删除版本号;每开始一个新事务,版本号加1;事务开始时的版本号作为事务版本号。在可重复读的隔离级别下:1)Insert:当前事务版本号为创建时间,删除时间不存在;2)Select:创建时间早于或等于当前事务版本的数据行,确保读到的数据都是事务开始前已经存在,或者是事务本身插入/修改的。删除时间不存在或晚于当前事务版本的数据行,确保读到的数据行事务开始前未删除;也就是说读取数据时不会加读锁,该数据可以被其他事务修改;3)Delete:当前事务版本号作为删除时间;4)Update:insert+delete;创建新纪录创建时间为当前事务版本;原有行的删除时间为当前事务版本。
可实现可重复读和不可重复读的隔离级别。无标准规范,是行锁的变种;优点是减少加锁开销,读操作不需要加锁,写操作只锁定必要的行;缺点在于额外2个字段的开销。原理是保证事务在执行过程中看到的数据是一致的;根据事务开始时间不同,每个事务看到的数据可能不同。
2.4 ANSI标准定义的四种隔离级别
隔离级别越低则开销越低,支持并发度越高。
1)Read uncommitted:事务的未提交修改对其他事务可见,不推荐;读取未提交数据称为脏读。可解决更新丢失问题,即事务A和事务B同时修改ID为1的用户姓名,事务A希望把姓名修改为John,事务B希望把姓名修改为Jim,没有任何锁的情况下,有一个修改会丢失。read uncommitted隔离级别下,事务A进行写操作时加写锁,读操作不加锁,因此写操作时其他事务不能写,但可以读(因为读不用加锁,读锁和写锁互斥,没有读锁就不存在互斥)。
2)Read committed:多数数据库的默认隔离级别,mysql不是。事务提交后,所做修改才对其他事务可见。事务进行写操作时加写锁,读操作时加读锁,写锁在事务提交后释放,但读锁在读操作结束后释放,而不用等到事务提交。事务提交之前,数据一直被写锁锁定,不能被写或被读,也就不存在脏读。但是一个事务内执行两次同样的读操作,可能得到不同结果,称为不可重复读;这是因为读锁在读操作后即解锁,解锁后被读数据可以被修改,再次读取后可能读到不同的值。
3)Repeatable read:mysql默认隔离级别;解决不可重复读的问题,事务进行写操作时加写锁,进行读操作时加读锁,二者都是在事务提交时释放。读锁在事务提交时释放解决了不可重复读的问题;仍存在幻读:事务A把row1从1更新为2,事务B插入row2,事务B把row2和仍为1的row1提交,事务A再次读取row1时,发现仍然是1仿佛更新没有生效。InnoDB通过间隙锁防止幻读:不仅锁定查询涉及的行,还对索引中的间隙进行锁定,防止幻影行的插入。
4)Serializable:强制事务串行执行;适用于亟须确保数据一致性且可接受无并发。
三、事务提交
1)InnoDB默认采用自动提交模式autocommit,每个查询被当作一个事务执行并提交;2)显式开始事务并提交start transaction,commit/rollback;3)执行alter table强制提交当前所有活动事务。
四、分布式事务XA
存储引擎的事务特性保证在存储引擎级别实现ACID;而分布式事务则在多个数据库之间保证事务,通过两阶段提交实现:第一轮,协调者询问所有参与者准备好了没?第二轮,提交。
Mysql本身的插件式架构导致其内部需要XA事务,例如一个跨存储引擎的事务;或者通过分布式事务协调存储引擎和二进制日志,在存储引擎提交的同时,将提交信息写入二进制日志。
Mysql可以作为多个数据库的分布式事务的参与者;也就是外部XA事务。分布式事务中,通信延迟和参与者本身的失败,导致性能消耗大;因此分布式事务并不是最推荐的一致性方案。除了分布式事务,还可以考虑其他数据同步方式,例如在本地写入数据,并将其放入对列,分发到下游系统,类似三靠谱。