vlambda博客
学习文章列表

MYSQL中事务特性及日志分析

事务的4大特性分别有:

  • 原子性:事务中的所有操作作为一个整体像原子一样不能分开,要么全部成功,要么全部失败,当其中一条执行失败或抛出异常,整个事务就会回滚到原始状态。

  • 一致性:事务的执行结果必须从一个一致性状态到另一个一致性状态。如转账前后两个账户的金额总和应该保持不变。

  • 隔离性:并行执行的事务应该互不影响,其对数据库的影响应该和正常串行是一样。如多个用于同时往一个账户转账,最后账户的结果应该和按先后顺序转账的结果一样。

  • 持久性:事务一旦提交,对数据库的更改应该持久化到磁盘,任何事务或系统故障都不会导致数据丢失

mysql中是如何实现事务的这4中特性,保证数据的一致性和可靠性呢。

一、原子性的保证

mysql中的通过undo日志来实现事务的原子性,使一致性状态不会因为事务或系统故障而破坏。

1.1 undo log介绍

undo log是逻辑日志,它是按行记录的,在操作任何数据之前先将数据备份到undo log中,然后进行数据修改,如果出现错误或者用户执行了rollback操作,系统可以根据undo log中备份将数据恢复到事务开始之前的状态。

undo log是逻辑日志,可以理解为:

  • 当delete 一条记录时,undo log中会记录一条对应的insert记录

  • 当insert一条记录时,undo log中会记录一条对应的delete记录

  • 当update一条记录时,他记录一条对应相反的update记录

undo日志也可实现mvcc(多版本并发控制),当读取的某一行被其他事务锁定时,可以通过undo日志分析改行记录之前的数据,从而提供改行版本信息,让用户实现一致性非锁定读。

undo日志是采用段(segment)的方式来存储的,每个undo 操作在记录时占用一个undo log segement段。
1.2 undo log日志存储

innodb存储引擎中对undo的管理采用段的方式,rollback segment称为回滚段,每个回滚段中有1024个undo log segment。老版本中只支持一个回滚段,mysql5.5以后支持128个回滚段,即支持128*1024个回滚操作。还可以通过参数innodb_undo_logs控制自定义多少个回滚段,默认是128。

undo日志默认是存储在mysql数据目录中的idbdata文件中,

如果开启了innodb_file_per_table 配置,则会存储在每个表的idb文件中。
默认undo日志是都写在一个文件中,可通过 innodb_undo_namespace 参数控制平均分配到多少个文件中,默认是0,取值范围值0-128,该参数只能在最开始初始化MySQL实例的时候指定,如果实例已创建,这个参数是不能变动的,如果在数据库配置文.cnf 中指定innodb_undo_tablespaces 的个数大于实例创建时的指定个数,则会启动失败,提示该参数设置有误。
下面是undo相关的几个参数:
#参数控制平均分配到多少个文件中,默认是0,取值范围值0-128innodb_undo_tablespaces#控制最大undo tablespace文件的大小,当启动了innodb_undo_log_truncate时,#undo tablespace文件超过该值时才会去尝试truncate。#该值默认大小为1G,truncate后的大小默认为10M。innodb_max_undo_log_size#InnoDB的purge线程,根据该值设置开启或关闭、innodb_max_undo_log_size的#参数值,以及truncate的频率来进行空间回收和undo file的重新初始化。#该参数生效的前提是,已设置独立表空间且独立表空间个数大于等于2个。innodb_undo_log_truncate#自定义undo日志的存放目录innodb_undo_directory

1.3 delete/update的内部机制

当A、B两个事务同时操作一行记录时,如何A事务提交了,此时innodb不会立即删除该回滚段,当事务隔离级别为REPAETABLE READ时,B事务还没提交,此时B事务查到的还是它开启事务时的提交行版本,只要B事务不提交,undo日志就不会被删除,保证了可重复读。当B事务提交时,会将undo加入删除列表中,通过purge来定时清理。并且提交事务时,还会判断undo log分配的页是否可以重用,如果可以重用,则会分配给后面来的事务,避免为每个独立的事务分配独立的undo log页而浪费存储空间和性能。
通过undo log记录delete和update操作的结果发现:(insert操作无需分析,就是插入行而已)
  • delete操作实际上不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。
  • update分为两种情况:

    如果不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。

    如果是主键列,update分两部执行:先删除该行,再插入一行目标行。

二、持久性保证
mysql中事务持久性保证是通过redo日志和binlog日志来实现的,其中binlog用于分布式mysql的主从数据复制,默认是不开启的。二进制日志和redo日志都可实现重做的功能,二者的区别如下:
  • 二进制日志是在存储引擎上层产生的,归属于mysql,不管是什么引擎,对数据库修改就会记录在二进制日志中,对而redo日志是产生于Innodb存储引擎,记录的是innodb存储引擎中对数据的修改,而且二进制日志先于redo日志产生

  • 二进制日志是按行记录的,记录的修改事件,记录sql语句、执行时间、环境变量、执行状态、修改值等,而redo日志是按页记录的,记录的是每页数据的修改。

  • 二进制日志是最终提交时一次性刷到磁盘里的,而redo日志是每次提交操作都会刷到磁盘,最后一次事务提交记录覆盖之前的,而且redo日志是记录页的修改,一个页可能有多个事务修改,所以可能同时记录多个事务的提交记录。

和undo日志不同,它记录的是新数据的备份,是WAL预写日志,在事务提交前就会写入磁盘,redo日志是物理日志,它是按页记录的。redo日志可分为两部分:一部分是在内存缓存中的log Buffer,一部分是持久化到磁盘上的redo日志文件。在事务提交前,会先将所有事务操作日志写入到磁盘中,最后才提交事务,更改数据。redo日志持久化的流程图如下所示:
MYSQL中事务特性及日志分析

最终保存到磁盘上的文件为ib_logfileN,如下所示:

MYSQL中事务特性及日志分析

mysql支持自定义将redo日志写入磁盘的方式,通过innodb_flush_log_at_trx_commit参数来控制,有如下3个值:
  • 0:日志提交时先写到用户态内存中的Log Buffer中,然后每秒写入OS Buffer内核态文件系统缓存中,同时调用fsync()刷到磁盘中

  • 1:日志每次提交到OS Buffer内核文件缓存中,同时调用fsync()刷到磁盘,这种是最可靠的方式,不会丢失数据,但是性能是最低的,默认值也是1

  • 2:日志每次提交时写入OS Buffer内核文件缓存中,然后每秒调用fsync()刷到磁盘

MYSQL中事务特性及日志分析
在主从复制结构中,要保证事务的持久性和一致性,需要对日志相关变量设置为如下:
  • 如果启用了二进制日志,则设置sync_binlog=1,即每提交一次事务同步写到磁盘中。
  • 总是设置innodb_flush_log_at_trx_commit=1,即每提交一次事务都写到磁盘中。

上述两项变量的设置保证了:每次提交事务都写入二进制日志和事务日志,并在提交时将它们刷新到磁盘中。
2.1 redo日志的结构
innodb中redo日志是按块进行存储的,称为redo log block每块大小是512字节,无论在log Buffer、OS Bufffer中都是以块存储的,最终写入磁盘也是按块写入的。
MYSQL中事务特性及日志分析
如上图所示,日志块中包含块头、块体、块尾组成,可以看到一个日志块最大存储492字节的数据也修改记录,当一个数据页的修改记录超过492时,会记录在多个日志块中。

日志块头包含4个部分:

  • log_block_hdr_no: (4字节)该日志块在redo log buffer中的位置ID。
  • log_block_hdr_data_len: (2字节)该log block中已记录的log大小。写满该log block时为0x200,表示512字节。
  • log_block_first_rec_group: (2字节)该log block中第一个log的开始偏移位置。
  • lock_block_checkpoint_no: (4字节)写入检查点信息的位置。

日志块尾只有一个部分:log_block_trl_no ,该值和块头的 log_block_hdr_no 相等。

2.2 log group

log group表示的是redo log group,一个组内有多个大小相同的ib_logfile组成。

#ib_logfile文件数量,默认值为2innodb_log_files_group#自动以ib_logfile的存储目录,默认为mysql数据目录innodb_log_group_home_dir
相关参数如下:

MYSQL中事务特性及日志分析日志存储,两个ib_logfile文件

[root@xuexi data]# ll /mydata/data/ib*-rw-rw---- 1 mysql mysql 79691776 Mar 30 23:12 /mydata/data/ibdata1-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile0-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile1

内存中的redo日志刷到磁盘时,会以追加的方式写入ib_logfile,当第一文件写满以后,会写入第二个文件,当第二个文件写满以后又会回头清理第一个文件,清理后写入第一个文件。

三 、事务隔离性保证:锁

事务的隔离性是通过锁来实现的,从MYSQL层面来讲,从锁的用途可以分为共享锁和排他锁。

  • 共享锁:又称读锁,允许一个事务去读一行,阻止其他事务对该行数据加上排它锁,允许其他事务对该行加上读锁,不允许其他事务修改、删除改行数据,知道锁释放。可在查询语句后加上lock in share mode添加读锁。

  • 排它锁:又称写锁,允许一个事务修改一行记录,事务T为一行加上写锁后,阻止其他事务为该行数据添加读锁和写锁,不加锁可以正常读这条记录,不加锁就没有锁机制,其他事务不能对这条数据做修改、删除,直到锁释放。

mysql中的Innodb引擎默认会给insert、update、delete语句涉及到的数据加排它锁,Select语句默认不加锁,可通过lock in share mode加读锁,for update 加写锁。
不同存储引擎对应的锁不一样,Innodb中的锁可分为读共享锁和写排它锁,Innodb只有表锁和行锁,没有页锁。 从锁的粒度上来分可分为 表锁、页锁、行锁。 不同锁粒度特点如下:
  • 表锁:基于数据库表的锁,开销小,获锁快,锁粒度最大,锁冲突概率大,并发小,不会出现死锁

  • 页锁:基于数据存储页的锁,开销及粒度介于表锁和行锁之间,并发一般,会出现死锁

  • 行锁:基于数据行的锁,锁粒度最小,获锁慢,锁冲突概率小,并发最大,会出现死锁

3.1 行共享锁(读锁S)

允许事务读一行数据,事务获取到读锁后,其他事务不能对该行做修改、删除操作,即不能获取改行的写锁X,但是可以读改行,也可以获取改行的读锁,所以说读锁时共享的。只有表中有索引,才能上行锁,如果一个表中没有索引,行锁会自动升级为表锁,在SQL语句中添加lock in share mode即可添加读锁。案例分析如下:

case1:查询条件中为主键索引
事务A加行共享读锁:

MYSQL中事务特性及日志分析

事务B相对id=1的记录做修改,上读锁,看结果:

MYSQL中事务特性及日志分析

case2:查询条件中无索引

MYSQL中事务特性及日志分析

B事务修改表中的其他记录,都会阻塞超时,说明事务A中对该表上了读表锁,看结果:

MYSQL中事务特性及日志分析

3.2 行排他锁(写锁X)

允许事务修改记录,事务A获得一行记录的写锁后,其他事务不能对修改、删除该行记录,可以读改行记录,但是不能对其加读共享锁,也不能对该行加行写锁。同样如果检索条件中没有索引,行锁会升级为表锁。在SQL语句中添加for update可获锁。案例分析如下:

case1:查询条件有索引加写锁

MYSQL中事务特性及日志分析

B事务测试结果如下:

MYSQL中事务特性及日志分析

case2:查询条件无索引加写锁

MYSQL中事务特性及日志分析

B事务测试结果:

MYSQL中事务特性及日志分析

3.3 Next Key

Gap Lock 锁定一个范围,而非记录本身。Next Key=行记录锁+间隙锁,查询条件中得包含索引,并且该索引不能是主键索引或者唯一索引,不然间隙锁会退化为行锁。Next Key用来解决幻读的问题。
case1:查询条件中有主键索引上间隙锁
表T中f_id也建了索引,但不是唯一索,事务A通过主键索引查询并加锁。

MYSQL中事务特性及日志分析

此时会给id范围为(3,正无穷大)的记录添加写锁,这里不包含3,看B事务的操作结果:

MYSQL中事务特性及日志分析

case2:查询条件中有索引,但是无主键索引

A事务中查询条件f_id=3,此时会加Next Key锁,并且有2个索引,其需要分别进行锁定。
对于主键索引,其仅对id等于5的索引加上行写锁。而对于辅助索引,其加上Next-Key Lock,锁定了范围(1,3),同时InnoDB存储引擎还会对辅助索引下一个键值加上Gap Lock,即范围(3.6)的锁。

MYSQL中事务特性及日志分析

通过上面分析可知,A事务给f_id取值范围为[1,6]的记录加了写锁,下面我们来验证一下:

四、事务一致性保证

一执行是事务的根本追求,前面的原子性、隔离性、持久性的最终目的就是为了达到事务的最终一致性。并发控制技术锁保证了事务的隔离性,是数据库一致性状态不会因为并发事务而破坏,undo日志保证了事务的原子性,使一致性状态不会因为事务或系统故障而破坏,同时redo日志保证了事务的持久性,使事务已提交的数据不会因为系统崩溃或意外中断而丢失。保证事务在并发时、事务或系统故障、系统崩溃、断电等特殊情况下,数据库的状态保持一致,这就是一致性。

五、死锁

行锁可能会产生死锁,针对死锁MYSQL中以等待图的方式检测死锁,就是采用深度遍历优先的方式检测各个事务之间是否连接成为一个闭环,跟图一样,这样就产生了死锁,检测到后,MYSQL会挑选undo比较少的事务进行回滚,抛出异常。死锁示例如下图所示:

在业务中可通过如下三种方式来避免死锁:

  • 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。

  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率。

  • 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率。