Mysql事务级别,锁的级别分类,redo log和undo log,事务实现机制

一般我们都熟知,事务有四大属性:ACID,即:

  • A:Atomic。原子性,任何一个事务都是一个原子性的,事务要么成功要么失败,不存在事务的一部分执行成功,一部分执行失败

  • C:Consitence,一致性,事务的一致性可以理解为数据的完成性的约束,这些包括主键约束、唯一键约束、

  • I:Isonlation,隔离性,事务之间不能相互影响。

  • D:Durability 事务一旦被提交,则在数据库中的改变将永久保存下来,不会改变

Mysql中InnoDB支持标准的事务ACID。

Mysql中事务很多特性都和锁有关。

表锁

MySQL中最大粒度的锁是表锁:

LOCK TABLE 表名称 READ; --读锁表,不允许对表的任何修改
LOCK TABLE 表名称 WRITE; --写锁表,阻塞其他事务对表的读和写

行锁

  • 排它锁(X lock):写锁,同一份数据加了写锁,其他事务不能加任何锁,可以通过如下加排它锁:
SELECT ....... FOR UPDATE
  • 共享锁(S lock):读锁,同一份数据,其他事务只能获取共享锁,无法加排它锁,可以通过如下加共享锁
SELECT ..... LOCK IN SHARE MODE

释放表锁:

UNLOCK TABLES;

当出现如下场景时:
事务A获取行锁,锁住了一行数据,只能读不能写,
事务B申请获取表锁,这时候怎么判断事务B是否能够获取到表锁呢,常规判断如下:

  1. 判断表是否被其他事务获取表锁
  2. 判断表中每行数据是否有行锁

这种判断比较繁琐,主要在第二步的时候需要判断表中每行数据是否有行锁,为此MySQL设计了意向锁,来简化操作,上面的场景优化为:

  1. 事务A先获取表的一项共享锁,然后获取行锁
  2. 事务B的判断则改为:
    (1)判断表锁这步不变
    (2)判断表上有意向共享锁的时候,这时表中有行数据被锁住,事务B等待

意向锁是表级别的锁

由此意向锁和排它锁以及共享锁组合,之间的兼容关系如下:

排它锁意向排它锁共享锁意向共享锁
排它锁ConflictConflictConflictConflict
意向排它锁ConflictCompatibleConflictCompatible
共享锁ConflictConflictCompatibleCompatible
意向共享锁ConflictCompatibleCompatibleCompatible

MySQL官方锁的解释: https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-intention-locks

The intention locking protocol is as follows:

  • Before a transaction can acquire a shared lock on a row in a table, it must first acquire an IS lock or stronger on the table.
  • Before a transaction can acquire an exclusive lock on a row in a table, it must first acquire an IX lock on the table.

可以发现:

  • 如果想要获取行的共享锁,必须先获取表级的意向共享锁
  • 如果想要获取行的排它锁,必须先获取表级的意向排它锁

MySQL InnoDB存储引擎主要有三种行锁算法:

  • Record Lock 记录锁,单个记录上的行锁,总是去锁住索引上的记录,即使数据没有索引,MySQL会隐式的创建索引
  • Gap Lock 间隙所,锁住的是一个范围区间,不包含记录本身,即开区间:(x,y)
  • Next-Key Lock 临键锁,Record Lock和Gap Lock的组合,锁定范围区间并且包含记录本身,闭区间:[x,y]

一般间隙锁条件为:

  • 使用普通索引锁定;
  • 使用多列唯一索引;
  • 使用唯一索引锁定多行记录。

接下来我们用实例来说明,表结构如下

create table test(
a int primary key,
b int,
c int
-- key(b)先不含普通索引测试
);
insert into test values(1,1,1);
insert into test values(2,2,2);
insert into test values(3,3,3);
insert into test values(5,5,5);
insert into test values(10,10,10);
insert into test values(12,12,12);
insert into test values(18,18,18);
insert into test values(23,23,23);

当执行如下语句的时候:

select * from test where a=1 for update;

这个时候间隙锁会退还为记录锁,锁住的只有a=1的这行记录,

当执行如下语句:

begin;
select * from test where a between 5 and 10 for update;

这时候锁住的区间是(5,10],(10,12]

这时候如果执行如下操作:

begin;
insert into test values(4,4,4); --执行成功,不会阻塞等待

insert into test values(8,8,8,8); --阻塞等待
insert into test values(12,1212,12); --阻塞等待

insert into test values(13,1313,13); --执行成功,不会阻塞等待

如果我们锁住的是一条不存在的记录时呢?

select * from test where a = 8 for update;

这时候执行如下操作:

insert into test values(4,4,4); --执行成功,不会阻塞等待
insert into test values(5,5,5); -- 执行成功,不会阻塞等待
insert into test values(10,10,10); -- 执行成功,不会阻塞等待

insert into test values(7,7,7); --阻塞等待
insert into test values(8,8,8,8); --阻塞等待

可以发现,这时候,实际锁住的数据是 8 所在的区间,即:(5,10)

通过上面的例子我们可以发现:如果表中只有唯一主键,则:

  • 当我们查询主键一条指定的记录,如果存在,则加锁的是记录锁,如果记录不存在,则加锁的是间隙锁
  • 如果查询的的是一个范围的话,则加锁的是间隙锁,且还会额外锁住相邻的下一个范围

如果是普通索引呢?表结构如下:

create table test(
a int primary key,
b int,
c int,
key(b)
);
insert into test values(1,1,1);
insert into test values(6,4,2);
insert into test values(9,8,3);
insert into test values(10,11,5);
insert into test values(11,15,10);
insert into test values(12,17,12);
insert into test values(18,22,18);
insert into test values(23,23,23);

进行如下操作:

begin;
select * from test where b=8 for upate;

在另外一个事务中进行如下操作:

insert into test values(3,3,1); --操作成功
insert into test values(4,12,1); --操作成功
insert into test values(13,11,1); -- 操作成功

insert into test values(6,6,1); --阻塞等待
insert inot test values(2,11,1) ; --阻塞等待
insert into test values(7,11,1); --阻塞等待

这里,我们理解,在普通索引上,锁住的应该是(4,11)这个区间,但是:

insert into test values(13,11,1); -- 操作成功
insert into test values(7,11,1); --阻塞等待

一个成功,一个却失败了,这是因为:
在普通索引和唯一索引同时存在时,数据间隙的分析是优先根据普通索引排序,然后根据唯一索引排序,锁住的实际上根据普通索引和唯一索引构成的这段数据区间。
通过分析发现,
select * from test where a = 8 for update;
实际上锁住的是 (b=4,a=6) ~ (b=11,a=10)的这个区间里面,
因此对于普通索引:

  • 在普通索引上,如果加锁,不管任何查询,都会产生间隙锁
  • 在普通索引和唯一索引并存的条件下,数据的间隙是优先根据普通索引排序,在根据唯一索引进行排序

MySQL中使用间隙锁解决了幻读的问题。

数据库事务中比较注意的几个问题:

  • 脏读 脏读是读取了别的事务尚未提交的数据,如果事务回滚了,则这部分已经读取的数据就是脏数据了。
  • 不可重复读 指的是在一个事务内,多次读取同样的条件下的数据,返回的结果不一样,一般不可重复读主要指的是其他事务更新update导致当前事务前后读取同样条件的数据不一致
  • 幻读 指的是当前事务多次读取同样条件下的数据,读取到了新的数据,一般幻读主要是其他事务进行了insert或者delete,当前事务多次读取同样条件的数据,数据条数会增加

MySQL中在RR事务级别下,通过 临键锁 解决了幻读问题

MySQL在不同事务模式下,提供了两种读模式:一致性非锁定读(非加锁,快照读)、当前读(加锁,也称锁定读)。

MySQL中在RC和RR的事务级别下都提供了MVCC(Multiversion Concurrency Control , 多版本控制)。引入了MVCC之后,MySQL中除了写写,读读、读写、写读都能够并发运行。MVCC只在 RC和RR事务隔离级别下工作(这里的读指的是快照读)

RR隔离事务级别依靠 MVCC+临键锁 实现,MVCC则依靠 undo log、Read view 、隐藏字段。

MySQL中每行记录还包含了如下隐藏字段:

  • DATA_TRX_ID 标记最后更新这条行记录的transaction id
  • DATA_ROLL_PTR 指向当前记录项的rollback segment的undo log记录,找之前版本的数据就是通过这个指针
  • DB_ROW_ID,当由innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,否则聚集索引中不包括这个值,这个用于索引当中
  • DELETE BIT位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除,真正意义的删除是在commit的时候

所谓的Read View指的是Consitent Read View一致读视图

在RC和RR隔离级别下,每次更新都会更新MVCC快照的数据,RR和RC的MVCC不同之处在于Readview:

  • RC在每次select的时候都是读取最新的ReadView快照,如果这期间其他事务更新了数据,前后读取数据不一致,即: 不可重复读
  • RR第一次select读取ReadView快照之后,后续读取的都是该快照,不改变,即实现了可重复读

ReadView实际上是有多个版本,成一个类似链式结构,包含了当前活跃的事务,那么怎么读取哪一个版本的Readview呢,即所谓的可见性,在InnoDB引擎中,Readview中有一个tx_ids的集合,按从大到小排序,记录了当前Readview中活跃的、未提交的事务ID,当前事务读取一条记录时,首先将当前行记录的DATA_TRX_ID设置为tx_id,将tx_id与tx_ids集合进行比较,获取可见读取的记录:

  1. 如果tx_id < tx_ids中最小的事务id,表明最后修改当前行记录的事务当前事务创建快照之前就已经提交了,该行记录对当前事务是可见,直接返回该行记录
  2. 如果tx_id > tx_ids最大的事务id,表明最后修改当前行记录的事务当前事务创建快照之后才更新当前行记录,提交了事务,当前行记录对当前事务不可见,这时候需要进行步骤4
  3. 如果 tx_ids最小的事务id< tx_id <tx_ids最大的事务id,表明当前事务在创建快照的时候,最后修改当前行记录的事务可能处于活跃状态或者已经提交状态,这时候需要在tx_ids中进行查找,如果tx_id在tx_ids中,则表明当前事务创建快照之后tx_id的事务修改了记录,当前行记录对当前事务不可见,这时候需要进行步骤4,如果tx_id不在tx_ids中,则表明当前事务创建快照之前 tx_id的事务已经提交了事务,当前行记录对当前事务可见
  4. 根据当前行记录上的DATA_ROLL_PTR查找undo log回滚段中最新的旧事务号id,赋值给tx_id,然后继续执行上述步骤判断

MySQL中undo log记录数据更新前的记录,是逻辑记录,如果事务回滚,则根据undo log回滚到之前的记录。
MySQL中redo log则是记录更新后的记录,用来数据库异常恢复.

undo log分为两种,分别为:insert undo logupdate undo log.
insert undo log是insert操作产生的undo log,对当前事务可见,对其他事务不可见,在当前事务提交insert之后直接删除。
updae undo log是delete和update操作产生的undo log,可能会在mvcc中提供记录读取,不能再事务提交后立马进行删除。

MySQL中redo logundo log来实现事务的持久性。redo log保证事务的持久性,undlo log来实现事务回滚和MVCC。InnoDB引擎中,每次事务提交,都必须将事务相关数据强制写到redo log中。
MySQL中数据是写入到内存缓冲池中的,然后在以一定的频率刷新到磁盘中,这样在宕机或其他故障其他情况下可能造成数据的丢失,持久性无法保证,因此在MySQL InnoDB中,数据写缓冲池中会先写入到redo log的缓冲中,redo log在以一定频率刷新到磁盘中。
通过参数innodb_flush_log_at_trx_commit来控制redo log缓冲写入到redo log file的频率:

  • 1 (默认)每次提交事务都会立即调用系统指令 fsync将缓冲 立马写入磁盘
  • 0 每次提交事务写入redo log缓冲,不立即写入磁盘,每 1s 或者 redo log 缓冲使用大于一半时,写入磁盘
  • 2 每次提交事务写入redo log 缓冲,不写入磁盘,但是会将redo log穿冲写系统缓冲

在0的情况下,可能会数据丢失,最坏情况丢失大概1s左右的数据
在2的情况下,如果只是mysq服务实例挂了,重启数据能够恢复,不会丢失数据,但是如果服务器宕机,数据依然丢失
在1的情况下,数据不会丢失,但是性能没有上面两个好,如果对数据要求性比较高,建议设置为1.

MySQL中更新一条数据,是先将数据从数据文件读取到buffer pool 中,然后将更新前的记录写入到redo log中,然后直接在buffer pool中更新记录,注意这时候是没有写入磁盘的,然后当提交事务的时候,会将当前更改的记录写入到redo log中 ,redo log根据上面的配置,然后将redo log写入到磁盘中。

MySQL中数据什么时候更新到磁盘的数据文件中,MySQL中checkpoint机制,来进行数的写入,对于redo log而言,由于redo log是循环写入的,当redo log快满无法写入的时候,将触发checkpoint,将buffer pool中的脏页和redo log中的数据写入到数据文件中。

在没有进行checkpoint的时候,这时候更新的数据实际上是在buffer pool和redo log中的,由于buffer pool是在内存的,当宕机后数据就没了,而redo log是写入磁盘的,能够保证数据不丢失,而在进行了checkpoint之后,这部分数据也就永久的保存下来了。

二阶段提交

MySQLz中事务提交是采用两阶段提交机制的。主要分为三个步骤:

  1. 写入redolog,并设置redolog状态为prepare状态
  2. 写入binlog
  3. 提交事务,并设置redolog状态为commit状态

相当于是将写redolog拆成了prepare和commit两个阶段。这么做更多的是为了保持主从一致性
如果不使用二阶段提交,那么当写入redolog成功后崩溃,这时候binglog就没有这条记录,重启后,master会比slave多出已提交记录,如果先写binglog,后写redolog,写完binlog之后崩溃,这时候崩溃,重启后,主从还是不一致。

上面了解到,每个事务实际上都会有一个事务ID,XID。如果在redolog prepare阶段之后崩溃,那么重启后,这时候redolog是prepare状态不是commit状态,通过redolog的XID去binlog中找XID对应的记录,找不到,这时候回滚该事务。如果在写入binglog之后崩溃,这时候能够在binlog找找到对应的事务,提交事务即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值