mysql——事务以及锁

一、事务的特性(ACID),原子性,一致性,隔离性,持久性;

二、Mysql的事务隔离级别

1、读未已提交(READ UNCOMMITTED):

      一个事务可以读取到未提交的数据(比如只进行了更新操作),产生脏读,幻度。

2、读已提交(READ COMMITTED):

     事务读取到已经提交的数据,多次select,查询结果不一致,产生幻度。

3、可重复读(REPEATABLE READ) :

    一个事务中多次读取数据相同,可能产生幻读,但是mysql的可重复读解决了幻读的问题,事务读取可以分为:快照读使用与select 语句,比如:select id from user where id = 1 (MVCC:READVIEW 和 版本链解决)、当前读,比如:select id from user where id = 1 for update (一个事务内读取数据,间隙锁),可以参考一下链接加深理解:https://blog.csdn.net/qq_40918324/article/details/104617714

4、串行化(SERIALIZABLE):

三、1、 什么是版本链:

 在innoDB存储引擎的表,有三个隐藏列分别是:row_id,trx_id,roll_pointer,其中row_id不是必要的,如果有主键索引,则列号不一定存在。

trx_id:记录当前事务的事务号。

roll_pointer:指向前一个事务号,(通过指针找到修改前的记录)。

2、ReadView:

①  对于读已提交和可重复读,需要使用上述的版本链,核心问题:找到哪个事务是正确的,可见的。那么怎么判断哪个事务版本是可用的呢,就需要ReadView了,我发现其实很多人都在说mvcc但是却不知道readView,真的是很奇怪。

②  ReadView 主要由四部分组成:

    1、m_ids:记录当前活跃的所有事务,相当于一个数组,[1,2,3,4]

     2、min_trx_id: 记录当前活跃事务中最小的事务号。

     3、max_trx_id:记录下一个应该开启的事务的事务号,事务号是递增的,其实就是时间戳。

     4、creator_trx_id:表示生成该ReadView的事务号。

注意max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比方说现在有id为1,2,3这三个事务,之

后id为3的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,

max_trx_id的值就是4。

③ 有了readView之后,再查询语句的时候就可以判断哪些事务的数据是可以被查到的了:

    1、比如查询的数据的事务id  <  min_trx_id  表示数据在查询前没有操作过,可以操作。

     2、如果数据的事务id == creator_trx_id 则表示当前事务在查询数据,可以操作。

     3、如果数据的事务id > max_trx_id 则表示查询时,数据又被其他的事务操作过,不能查询数据,需要根据版本链查询。

     4、如果min_trx_id < 数据的事务id <max_trx_id, 需要判断是否在m_id,并且不在m_id 列表中则代表,查询时已经被提交过,可以查询。

④ 对于读已提交和可重复读,在生成ReadView的规则不同,导致了他们读取数据时候的区别:

   1、读已提交:每次执行select的时候都会生成一个readView,记录当前活跃的事务id。所以他可以读取到最新的别提交过的数据。

   2、可重读度:他只会在第一次执行select 语句的时候生成ReadView,所以多次执行ReadView相同所以查询数据相同。

四、mysql innoDB的行锁与表锁

 一、行锁:

      1、行锁分为: ① LOCK_REC_NOT_GAP:单个行记录上的锁。   

                ② LOCK_GAP:间隙锁,只会锁住查询数据之间的间隙。

                ③ NEXT_KEY_LOCK:结合单行锁和间隙锁的锁。

       2、对于读已提交,不存在间隙锁,只会对表中查询出来的数据加锁。

       3、对于可重读度,对于主键索引和唯一索引不会加间隙锁。对于普通索引和没有索引的列,会对查询出来的数据的间隙加锁(可能会造成死锁)。

       4、这里的锁对于二级索引,首先会对索引字段加锁,再下来同时会对,索引页中的主键id进行加锁,为什么会加两次锁?

           ① 当查询数据有二级索引列时,直接判断加锁快速返回。

           ② 当前查询没有二级索引列,但是时主键索引对应的数据时,保证数据被加锁。

五、mysql的redolog和undolog

      数据是通过什么方式保存到数据库中呢?数据存储过程中是如何保证脏页落盘呢?如何实现数据回滚?如何实现数据恢复呢?

 其实也就是mysql如何保证持久性,原子性和一致性的。

   ① mysql更新操作会先把数据从磁盘加载到内存,进行数据修改,然后再从内存(用户态)到内存空间(内核态)最后再到mysql的磁盘空间保证了数据持久化。

   1>buffer pool,数据会从磁盘加载到buffer pool进行数据修改。

   2> redolog 分成两部分: 1、redolog buffer 2、redolog file

       1、数据在写入redolog file之前会先写入redolog buffer,因为写入缓存的效率更快,当发生commit或者到达固定时间后(主要由innodb_flush_log_at_trx_commit 和 innodb_flush_log_at_timeout 两个值决定才会写入redolog file,每次mysql重启是都会判断根据 redolog恢复数据(保证持久化),这里还有一个lns的概念后面说,具体的写入规则 由 innodb_flush_log_at_trx_commit 0|1|2 控制,如下图:

0: 事务提交时,每次都会写入logbuffer ,但是只会定时(每秒)写入(调用fsync)osbuffer ,在写入到磁盘文件,可能会丢失1 秒内的数据。

1: mysql默认规则,事务提交时每次都写入 logbuffer、osbuffer、刷新到磁盘。数据完整但是性能低下。

2: 事务提交时每次都写入osbuffer,但是每秒执行一次写入磁盘操作。

      3>日志刷盘规则:主要由innodb_flush_log_at_trx_commit 和 innodb_flush_log_at_timeout 两个值决定。

           1.发出commit动作时。已经说明过,commit发出后是否刷日志由变量 innodb_flush_log_at_trx_commit 0|1|2 控制。

            2.每秒刷一次。这个刷日志的频率由变量 innodb_flush_log_at_timeout 值决定,默认是1秒。要注意,这个刷日志频率和commit动作无关。

            3.当log buffer中已经使用的内存超过一半时。

            4.当有checkpoint时,checkpoint在一定程度上代表了刷到磁盘时日志所处的LSN位置。

   ② 日志存储格式

     innodb存储引擎中,redo log以块为单位进行存储的,每个块占512字节,这称为redo log block。所以不管是log buffer中还是os buffer中以及redo log file on disk中,都是这样以512字节的块存储的。

     每个redo log block由3部分组成:日志块头、日志块尾和日志主体。其中日志块头占用12字节,日志块尾占用8字节,所以每个redo log block的日志主体部分只有512-12-8=492字节。

因为redo log记录的是数据页的变化,当一个数据页产生的变化需要使用超过492字节的redo log来记录,那么就会使用多个redo log block来记录该数据页的变化。

日志块头包含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块头的第三部分 log_block_first_rec_group ,因为有时候一个数据页产生的日志量超出了一个日志块,这是需要用多个日志块来记录该页的相关日志。例如,某一数据页产生了552字节的日志量,那么需要占用两个日志块,第一个日志块占用492字节,第二个日志块需要占用60个字节,那么对于第二个日志块来说,它的第一个log的开始位置就是73字节(60+12)。如果该部分的值和 log_block_hdr_data_len 相等,则说明该log block中没有新开始的日志块,即表示该日志块用来延续前一个日志块。

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

上面所说的是一个日志块的内容,在redo log buffer或者redo log file on disk中,由很多log block组成。如下图:

1.4 log group和redo log file

log group表示的是redo log group,一个组内由多个大小完全相同的redo log file组成。组内redo log file的数量由变量 innodb_log_files_group 决定,默认值为2,即两个redo log file。这个组是一个逻辑的概念,并没有真正的文件来表示这是一个组,但是可以通过变量 innodb_log_group_home_dir 来定义组的目录,redo log file都放在这个目录下,默认是在datadir下。

mysql> show global variables like "innodb_log%";
+-----------------------------+----------+
| Variable_name               | Value    |
+-----------------------------+----------+
| innodb_log_buffer_size      | 8388608  |
| innodb_log_compressed_pages | ON       |
| innodb_log_file_size        | 50331648 |
| innodb_log_files_in_group   | 2        |
| innodb_log_group_home_dir   | ./       |
+-----------------------------+----------+

[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

可以看到在默认的数据目录下,有两个ib_logfile开头的文件,它们就是log group中的redo log file,而且它们的大小完全一致且等于变量 innodb_log_file_size 定义的值。第一个文件ibdata1是在没有开启 innodb_file_per_table 时的共享表空间文件,对应于开启 innodb_file_per_table 时的.ibd文件。

在innodb将log buffer中的redo log block刷到这些log file中时,会以追加写入的方式循环轮训写入。即先在第一个log file(即ib_logfile0)的尾部追加写,直到满了之后向第二个log file(即ib_logfile1)写。当第二个log file满了会清空一部分第一个log file继续写入。

由于是将log buffer中的日志刷到log file,所以在log file中记录日志的方式也是log block的方式。

在每个组的第一个redo log file中,前2KB记录4个特定的部分,从2KB之后才开始记录log block。除了第一个redo log file中会记录,log group中的其他log file不会记录这2KB,但是却会腾出这2KB的空间。如下:

redo log file的大小对innodb的性能影响非常大,设置的太大,恢复的时候就会时间较长,设置的太小,就会导致在写redo log的时候循环切换redo log file。

1.5 redo log的格式

因为innodb存储引擎存储数据的单元是页(和SQL Server中一样),所以redo log也是基于页的格式来记录的。默认情况下,innodb的页大小是16KB(由 innodb_page_size 变量控制),一个页内可以存放非常多的log block(每个512字节),而log block中记录的又是数据页的变化。

其中log block中492字节的部分是log body,该log body的格式分为4部分:

  • redo_log_type:占用1个字节,表示redo log的日志类型。
  • space:表示表空间的ID,采用压缩的方式后,占用的空间可能小于4字节。
  • page_no:表示页的偏移量,同样是压缩过的。
  • redo_log_body表示每个重做日志的数据部分,恢复时会调用相应的函数进行解析。例如insert语句和delete语句写入redo log的内容是不一样的。

如下图,分别是insert和delete大致的记录方式。

          

   ③ mysql的脏页落盘(持久化):

          1> 脏页: buffer pool中的数据与磁盘中数据不一致的数据(mysql加载数据以页为单位的)成为脏页。

          2> 检查点:到达一个检查点是才会触发脏页落盘。在innodb中,数据刷盘的规则只有一个:checkpoint, Innodb存储引擎中checkpoint分为两种:

  • sharp checkpoint:在重用redo log文件(例如切换日志文件)的时候,将所有已记录到redo log中对应的脏数据刷到磁盘。
  • fuzzy checkpoint:一次只刷一小部分的日志到磁盘,而非将所有脏日志刷盘。有以下几种情况会触发该检查点:
    • master thread checkpoint:由master线程控制,每秒或每10秒刷入一定比例的脏页到磁盘。
    • flush_lru_list checkpoint:从MySQL5.6开始可通过 innodb_page_cleaners 变量指定专门负责脏页刷盘的page cleaner线程的个数,该线程的目的是为了保证lru列表有可用的空闲页。
    • async/sync flush checkpoint:同步刷盘还是异步刷盘。例如还有非常多的脏页没刷到磁盘(非常多是多少,有比例控制),这时候会选择同步刷到磁盘,但这很少出现;如果脏页不是很多,可以选择异步刷到磁盘,如果脏页很少,可以暂时不刷脏页到磁盘
    • dirty page too much checkpoint:脏页太多时强制触发检查点,目的是为了保证缓存有足够的空闲空间。too much的比例由变量 innodb_max_dirty_pages_pct 控制,MySQL 5.6默认的值为75,即当脏页占缓冲池的百分之75后,就强制刷一部分脏页到磁盘。

由于刷脏页需要一定的时间来完成,所以记录检查点的位置是在每次刷盘结束之后才在redo log中标记的。

③ double write: 因为mysql一页数据为16k,而磁盘最小为4k,所以一页数据需要写多次,期间如果出现失败就需要doblewrite来保证持久化成功,大概也是先写入缓存再写入磁盘。

 ④ lsn分析

       1>LSN称为日志的逻辑序列号(log sequence number),在innodb存储引擎中,lsn占用8个字节。LSN的值会随着日志的写入而逐渐增大。

       2>lsn存在于 数据页中,可以通过比较redolog 中lsn的值来决定是否需要redolog执行日志数据恢复,(数据页中的lsn小于redolog中的lsn,则刷新他们之间的差值建的数据)。

innodb从执行修改语句开始:

(1).首先修改内存中的数据页,并在数据页中记录LSN,暂且称之为data_in_buffer_lsn;

(2).并且在修改数据页的同时(几乎是同时)向redo log in buffer中写入redo log,并记录下对应的LSN,暂且称之为redo_log_in_buffer_lsn;

(3).写完buffer中的日志后,当触发了日志刷盘的几种规则时,会向redo log file on disk刷入重做日志,并在该文件中记下对应的LSN,暂且称之为redo_log_on_disk_lsn;

(4).数据页不可能永远只停留在内存中,在某些情况下,会触发checkpoint来将内存中的脏页(数据脏页和日志脏页)刷到磁盘,所以会在本次checkpoint脏页刷盘结束时,在redo log中记录checkpoint的LSN位置,暂且称之为checkpoint_lsn。

(5).要记录checkpoint所在位置很快,只需简单的设置一个标志即可,但是刷数据页并不一定很快,例如这一次checkpoint要刷入的数据页非常多。也就是说要刷入所有的数据页需要一定的时间来完成,中途刷入的每个数据页都会记下当前页所在的LSN,暂且称之为data_page_on_disk_lsn。

详细说明如下图:

参考链接:https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章