mysql innodb事务&锁的理解

事务

事务的四大特性

原子性: 构成事务的所有操作必须是一个逻辑单元, 要么全部执行, 要么全部不执行.

一致性: 数据库在事务执行前后状态都必须是稳定的或者是一致的,从数据库层面理解,操作完的数据必须符合你设置的约束(负数),从业务层面理解,操作映射的业务必须满足真实的规则

隔离性: 事物之间不会互相影响. 有锁机制和MVCC机制来实现.

持久性: 不管是数据库重启或崩溃,事务执行成功后必须全部写入磁盘。

对于一致性的理解

一致性是指,事务必须是使数据库从一个一致性状态变到另一个一致性状态。(从一个正常状态转换为另一个正常状态)。
一致性体现在两个层面:

  1. 数据库机制层面
    数据库层面的一致性是,在一个事务执行之前和之后,数据会符合你设置的约束(唯一约束,外键约束,Check约束等)和触发器设置。比如:A有90,向B转了100,这时A的账户余额就是负数,而金额的字段设置了unsigned(即非负数),这时数据库就会报错,违反了非负约束的一致性。
  2. 业务层面
    于业务层面来说,一致性是保持业务的一致性。比如:A有100,B有0,A向B转了100,这时AB的账户之和还得是100。如果不是就违反了业务逻辑的一致性。再比如扣了2块手续费,B只收到了98,那么业务上就必须处理考虑手续费的这2块,不然也就违反了业务的一致性。

mysql事务启动方式

  1. 显示启动事务语句,begin或者start transcation。配套的提交语句是commit,回滚语句为rollback
START TRANSACTION;
事务代码
commit;
  1. set autocommit=0, 这个命令会将自动提交关闭。这意味着如果你只执行一个select语句,这个事务就启动了,并且不会自动提交。这个事务持续存在知道你主动执行commit或rollback语句,或者链接断开。

    mysql中查看当前自动提交状态的命令为:show VARIABLES like 'autocommit'; 如下valueon代表是自动提交已经打开。

事务并发遇到的问题

  1. 脏读在这里插入图片描述

  2. 不可重复读
    在这里插入图片描述

  3. 幻读
    在这里插入图片描述

对比

脏读&不可重复读

  • 脏读没有提交,不可重复读提交了

不可重复读&幻读

  • 都提交了,但是不可重复读发生在修改和删除,幻读发生在新增

解决事务并发

标准的隔离级别
在这里插入图片描述

未提交读:脏读,不可重复复,幻读都没解决

已提交读:解决了脏读问题

可重复读:解决了脏读,不可重复读,没有解决幻读

串行化:解决所有问题,但是慢

mysql的innodn隔离级别

在这里插入图片描述

和标准的隔离级别对比,innodb的可重复读解决了幻读问题。也是innodb的模式隔离级别。

事务隔离级别的解决方案

  1. 基于锁的并发控制(LBCC)----在读取数据前,对其加锁,阻止其他事务对数据进行修改
  2. 基于多版本的并发控制(MVCC)----快照

以上两种方案在不同的场景协同使用

按锁的粒度分:行锁,表锁

按锁的用法分:乐观锁,悲观锁

按锁的类型分:排他锁,意向锁,共享锁,自增锁

按锁的算法分:间隙锁,记录锁,临键锁,插入意向锁

在这里插入图片描述

共享锁(S锁)–用于读取数据的

  • 又称读锁,顾名思义,共享锁允许多个事务对于同一数据可以共享一把锁,都只能读,不能修改。
  • 行锁
  • 加锁方式select * from student where id = 1 LOCK IN SHARE MODE;
  • 释放锁commit/rollback;

排它锁(X锁)

  • 又称写锁,排它锁不能与其他锁并存,如一个事务获取了一个数据行的排它锁,其他的事务就不能够再获取该行的锁,只有获取了排它锁的事务才能够对数据行进行读取和修改
  • 行锁
  • 加锁方式
    • 自动delete/update/insert默认加上排它锁
    • 手动select * from student where id=1 FOR UPDATE;
  • 释放锁commit/rollback;

意向锁(意向共享锁(IS锁)/意向排它锁(IX锁))

  • 表锁
  • 由数据引擎自己维护,用户无法手动操作意向锁
  • 意向共享锁表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁
  • 意向排它锁表示事务准备给数据行加入排它锁,也就是说一个数据行加排它锁前必须先取得该表的IX锁

加表锁的前提:没有其他任何事务已经锁定了该表的任意一行数据

由于这个前提,在加表锁的是时候需要对表的每一行进行扫描来判断是否加了锁

如果有了意向锁,就不需要全表扫描判断了,意向锁相当于一个标志用于提高数据库加表锁的效率

mysql的innodb是聚蔟索引

在没有指定主键的时候,会去看有没指定唯一索引,如果没有指定唯一索引,会按照行号_rowid来构建聚蔟索引(可以查到,select _rowid from table;)

锁的本质是锁住了索引

一. 未指定任何主键,索引,内部会按照_rowid存放

  • 因为没有索引,查询操作都是全表扫描
  • 给任意一行加排它锁的时候会锁住所有的_rowid,导致锁表,不能再给其他行加锁

二. 指定主键

  • 行排它锁只会锁住一行,按索引锁的
  • 其他行可正常读写和加锁

三. 指定主键索引(id)和辅助索引(name)

  • 使用where name = 4加锁的时候,会先通过name索引找到对应的主键,按照主键对行进行加锁

innodb之所以在可重复读的隔离级别就可以避免脏读,不可重复复,幻读是因为引入了间隙锁,临键锁,MVCC

在这里插入图片描述

记录锁是按照唯一索引等值查询,精准匹配加锁的

在这里插入图片描述

间隙锁和临建锁是按照区间进行加锁的

在这里插入图片描述
在这里插入图片描述

按查询的区间进行区间加锁可防止该区间的插入操作

mysql事务日志

事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。 事务日志持久以后,内存中被修改的数据在后台可以慢慢地刷回到磁盘。 目前大多数存储引擎都是这样实现的,我们通常称之为预写式日志(Write-Ahead Logging),修改数据需要写两次磁盘。
如果数据的修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。

MySQL Innodb中跟数据持久性、一致性有关的日志,有以下几种:

  • Bin Log:是mysql服务层产生的日志,常用来进行数据恢复、数据库复制,常见的mysql主从架构,就是采用slave同步master的binlog实现的
  • Redo Log:记录了数据操作在物理层面的修改,mysql中使用了大量缓存,修改操作时会直接修改内存,而不是立刻修改磁盘,事务进行中时会不断的产生redo log,在事务提交时进行一次flush操作,保存到磁盘中。当数据库或主机失效重启时,会根据redo log进行数据的恢复,如果redo log中有事务提交,则进行事务提交修改数据。
  • Undo Log: 除了记录redo log外,当进行数据修改时还会记录undo log,undo log用于数据的撤回操作,它记录了修改的反向操作,比如,插入对应删除,修改对应修改为原来的数据,通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC

MVCC(理解)

MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。

当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
下面看一下在REPEATABLE READ隔离级别下,MVCC具体是如何操作的。

  • SELECT

InnoDB会根据以下两个条件检查每行记录:

  1. InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
  2. 行的过期时间要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。

只有符合上述两个条件的记录,才能返回作为查询结果

  • INSERT

InnoDB为新插入的每一行保存当前系统版本号作为行版本号。

  • DELETE

InnoDB为删除的每一行保存当前系统版本号作为行删除标识。

  • UPDATE

InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识(过期时间)。
保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作

举例

create table mvcctest( 
id int primary key auto_increment, 
name varchar(20));

transaction 1:

start transaction;
insert into mvcctest values(NULL,'mi');
insert into mvcctest values(NULL,'kong');
commit;

假设系统初始事务ID为1;

IDNAME创建时间过期时间
1mi1undefined
2kong1undefined

transaction 2:

start transaction;
select * from mvcctest;  //(1)
select * from mvcctest;  //(2)
commit
SELECT

假设当执行事务2的过程中,准备执行语句(2)时,开始执行事务3 ----可能发生“幻读”

transaction 3:

start transaction;
insert into mvcctest values(NULL,'qu');
commit;
IDNAME创建时间过期时间
1mi1undefined
2kong1undefined
3qu3undefined

事务3执行完毕,开始执行事务2 语句2,由于事务2只能查询创建时间小于等于2的,所以事务3新增的记录在事务2中是查不出来的,这就通过乐观锁的方式避免了幻读的产生。

UPDATE

假设当执行事务2的过程中,准备执行语句(2)时,开始执行事务4----可能发生“不可重复读”

transaction session 4:

start transaction;
update mvcctest set name = 'fan' where id = 2;
commit;

InnoDB执行UPDATE,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID,同时保存当前事务ID到要UPDATE的行的删除时间

IDNAME创建时间过期时间
1mi1undefined
2kong14
2fan4undefined

事务4执行完毕,开始执行事务2 语句2,由于事务2只能查询创建时间小于等于2的,所以事务修改的记录在事务2中是查不出来的,这样就保证了事务在两次读取时读取到的数据的状态是一致的

DELETE

假设当执行事务2的过程中,准备执行语句(2)时,开始执行事务5----可能发生“不可重复读”

transaction session 5:

start transaction;
delete from mvcctest where id = 2;
commit;
IDNAME创建时间过期时间
1mi1undefined
2kong15

事务5执行完毕,开始执行事务2 语句2,由于事务2只能查询创建时间小于等于2、并且过期时间大于等于2,所以id=2的记录在事务2 语句2中,也是可以查出来的,这样就保证了事务在两次读取时读取到的数据的状态是一致的

MVCC(底层)

版本链+事务id+回滚指针+read-view

事务第一次执行sql就会创建一个read-view,对全库记录了事务提交情况,事务id

通过按照一定的规则判断read-view,就能取出对应正确的数据

当执行查询sql时会生成一致性视图read-view,它由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(包括提交和未提交的事务) (max_id)组成,查询的数据结果需要跟read-view做比对从而得到快照结果

在这里插入图片描述

对比规则

  • 事务id<min_ id ,表示这个版本是已提交的事务生成的,这个数据是可见的;(min_id是未提交中最小的事务id,小于他的数据版本都是已提交的–>可见
  • 事务id>max_id ,表示这个版本是由将来启动的事务生成的,是肯定不可见的;(max_id是所有事务中id最大的,大于他的数据版本都没发生–>不可见
  • min_id<=事务id<=max_id, 分情况
    • 如果该数据版本的事务id在未提交事务的数组中,表示该版本的数据未提交,–>不可见
    • 如果该数据版本的事务id不在未提交事务的数组中,表示该版本的数据已提交,–>可见

对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将事务id修改成删除操作的事务id,同时在该条记录的头信息. (record header)里的(deleted flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag为True,意味着记录已经被删除。

如有以下各个事务的时序操作,对应的状态为:
在这里插入图片描述

在这里插入图片描述

  • 对于查询1
    • read-view为[1,3], 4–>min_id=1,max_id=4,查看状态3
    • 第一个数据版本的事务id为4,在min_id和max_id之间,又因为不在未提交事务的数组里,所以第一个数据版本就是了–>A2
  • 对于查询2
    • read-view为[1,3], 4–>min_id=1,max_id=4,查看状态5
    • 第一个数据版本的事务id为1,在min_id和max_id之间,又因为在未提交事务的数组里,所以通过回滚指针回到第二个版本,第二版本的事务id为1,同理不可见,通过回滚指针回滚到第三个版本,第三个版本的事务id为4,在min_id和max_id之间,又因为不在未提交事务的数组里,所以第三个数据版本就是了–>A2
  • 对于查询3
    • read-view为[1,3], 4–>min_id=1,max_id=4,查看状态7, 对于可重复读的隔离级别,该处的read-view沿用的第一次查询的read-view
    • 第一个数据版本的事务id为3,在min_id和max_id之间,又因为在未提交事务的数组里,所以通过回滚指针回到第二个版本,第二个版本的事务id为3,回滚,第三个版本的事务id为1,,,最终找到第五个版本–>A2
    • 对于已提交读隔离级别,该处的read-view为[3],4查询结果为A4
  • 对于查询4
    • read-view为[3], 4–>min_id=3,max_id=4,查看状态7
    • 按规则找到第三个数据版本即为结果–>A4
    • 得出read-view是事务第一次执行sql时生成的

参考文献:https://www.jianshu.com/p/f692d4f8a53e

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值