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日志分析改行记录之前的数据,从而提供改行版本信息,让用户实现一致性非锁定读。
innodb存储引擎中对undo的管理采用段的方式,rollback segment称为回滚段,每个回滚段中有1024个undo log segment。老版本中只支持一个回滚段,mysql5.5以后支持128个回滚段,即支持128*1024个回滚操作。还可以通过参数innodb_undo_logs控制自定义多少个回滚段,默认是128。
#参数控制平均分配到多少个文件中,默认是0,取值范围值0-128
innodb_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的内部机制
-
delete操作实际上不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。 update分为两种情况:
如果不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。
如果是主键列,update分两部执行:先删除该行,再插入一行目标行。
二进制日志是在存储引擎上层产生的,归属于mysql,不管是什么引擎,对数据库修改就会记录在二进制日志中,对而redo日志是产生于Innodb存储引擎,记录的是innodb存储引擎中对数据的修改,而且二进制日志先于redo日志产生
二进制日志是按行记录的,记录的修改事件,记录sql语句、执行时间、环境变量、执行状态、修改值等,而redo日志是按页记录的,记录的是每页数据的修改。
二进制日志是最终提交时一次性刷到磁盘里的,而redo日志是每次提交操作都会刷到磁盘,最后一次事务提交记录覆盖之前的,而且redo日志是记录页的修改,一个页可能有多个事务修改,所以可能同时记录多个事务的提交记录。
最终保存到磁盘上的文件为ib_logfileN,如下所示:
0:日志提交时先写到用户态内存中的Log Buffer中,然后每秒写入OS Buffer内核态文件系统缓存中,同时调用fsync()刷到磁盘中
1:日志每次提交到OS Buffer内核文件缓存中,同时调用fsync()刷到磁盘,这种是最可靠的方式,不会丢失数据,但是性能是最低的,默认值也是1
2:日志每次提交时写入OS Buffer内核文件缓存中,然后每秒调用fsync()刷到磁盘
-
如果启用了二进制日志,则设置sync_binlog=1,即每提交一次事务同步写到磁盘中。 总是设置innodb_flush_log_at_trx_commit=1,即每提交一次事务都写到磁盘中。
日志块头包含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文件数量,默认值为2
innodb_log_files_group
#自动以ib_logfile的存储目录,默认为mysql数据目录
innodb_log_group_home_dir
日志存储,两个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为一行加上写锁后,阻止其他事务为该行数据添加读锁和写锁,不加锁可以正常读这条记录,不加锁就没有锁机制,其他事务不能对这条数据做修改、删除,直到锁释放。
表锁:基于数据库表的锁,开销小,获锁快,锁粒度最大,锁冲突概率大,并发小,不会出现死锁
页锁:基于数据存储页的锁,开销及粒度介于表锁和行锁之间,并发一般,会出现死锁
行锁:基于数据行的锁,锁粒度最小,获锁慢,锁冲突概率小,并发最大,会出现死锁
3.1 行共享锁(读锁S)
允许事务读一行数据,事务获取到读锁后,其他事务不能对该行做修改、删除操作,即不能获取改行的写锁X,但是可以读改行,也可以获取改行的读锁,所以说读锁时共享的。只有表中有索引,才能上行锁,如果一个表中没有索引,行锁会自动升级为表锁,在SQL语句中添加lock in share mode即可添加读锁。案例分析如下:
3.2 行排他锁(写锁X)
允许事务修改记录,事务A获得一行记录的写锁后,其他事务不能对修改、删除该行记录,可以读改行记录,但是不能对其加读共享锁,也不能对该行加行写锁。同样如果检索条件中没有索引,行锁会升级为表锁。在SQL语句中添加for update可获锁。案例分析如下:
3.3 Next Key
case2:查询条件中有索引,但是无主键索引
对于主键索引,其仅对id等于5的索引加上行写锁。而对于辅助索引,其加上Next-Key Lock,锁定了范围(1,3),同时InnoDB存储引擎还会对辅助索引下一个键值加上Gap Lock,即范围(3.6)的锁。
通过上面分析可知,A事务给f_id取值范围为[1,6]的记录加了写锁,下面我们来验证一下:
四、事务一致性保证
一执行是事务的根本追求,前面的原子性、隔离性、持久性的最终目的就是为了达到事务的最终一致性。并发控制技术锁保证了事务的隔离性,是数据库一致性状态不会因为并发事务而破坏,undo日志保证了事务的原子性,使一致性状态不会因为事务或系统故障而破坏,同时redo日志保证了事务的持久性,使事务已提交的数据不会因为系统崩溃或意外中断而丢失。保证事务在并发时、事务或系统故障、系统崩溃、断电等特殊情况下,数据库的状态保持一致,这就是一致性。
五、死锁
行锁可能会产生死锁,针对死锁MYSQL中以等待图的方式检测死锁,就是采用深度遍历优先的方式检测各个事务之间是否连接成为一个闭环,跟图一样,这样就产生了死锁,检测到后,MYSQL会挑选undo比较少的事务进行回滚,抛出异常。死锁示例如下图所示:
在业务中可通过如下三种方式来避免死锁:
如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率。
对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率。