一、事务
1)什么是ACID
Atomicity 原子性:某个操作,要么全部执行完毕,要么全部执行回滚
Consistency 一致性:数据库中的数据全部符合现实世界的约束,则满足一致性。比如性别没有不男不女,转账过程不影响已有货币总量(不凭空多出货币或少)
Isolation 隔离性(作用体现在我们开启和关闭事务及设置隔离级别时):多个事务访问相同数据时。对该数据状态的转换对应数据库操作顺序有一定规律,彼此互不干涉
Durability 持久性:现实中数据状态的转换映射到数据库中,即对数据的修改要保存在磁盘中
2)并发执行时数据的一致性问题
脏写:两个事务开启并发修改同一数据(不重要,因为正常使用的所有隔离级别都不会发生脏写)
脏读:一个事务(A)读取到了另一个事务(B)修改过的数据
不可重复读:另一个事务(B)修改一个事务(A)读取过的数据
幻读:一个事务(A)根据(select ... where vip='是')查询了一些记录。A未提交时,另一个事务(B)写入了(insert、update、delete都行,以insert为例)一些符合上述搜索条件的记录(insert into ... values(3,700,'是') )
3)SQL事务隔离级别(不单指MySQL隔离级别,而是指所有SQL)
隔离级别从上到下安全度递增,效率递减。
MySQL默认隔离级别为REPEATABLE READ(可重复读),而且MySQL的REPEATABLE READ可大概率防止幻读
二、MVCC(开启事务后操作才会分配新的trx_id)
1)MVCC & ReadView
MVCC(Multi-Version Concurrency Control):多版本并发控制,利用记录的版本链和ReadView,来控制并发事务访问相同记录的行为
ReadView:一致性视图,用来判断版本链中的哪个版本是当前事务可见的(主要用于 Read Committed 和 Repeatable Read)
2)版本链
每次更新该记录,都会将旧值放在一条undo日志中。随着更新次数的增加被roll_point属性连接成一条链表,这条链表称之为版本链
3)ReadView()包含的内容
m_ids:在生成ReadView时,当前系统中的事务id列表
min_trx_id:在生成ReadView时,当前系统活跃的读写事务中,也就是m_ids中的最小值
max_trx_id:在生成ReadView时,系统应该分配给
creator_trx_id:的事务的事务id
4)如何通过ReadView莱判断记录的某个版本可见
如果,则表明当前事务在访问自己修改过的记录,所以此版本可以被当前事务访问
如果,则表明生成该版本的事务在当前事务生成ReadView之前已经提交了,所以此版本可以被当前事务访问
如果,则表明生成该版本的事务在当前事务生成ReadView之后才开启,所以此版本不可以被当前事务访问
如果包含,说明生成此版本的事务还在活跃中,此版本不可以被当前事务访问
如果不包含,说明生成此版本的事务已经提交了,此版本可以被当前事务访问
Ps:如果某个版本的记录对当前事务不可见,那么上述判断指向版本链的下一条记录,直至找到可见记录或者到最后一个版本为止(属于记录,其他的事务Id都属于此刻生成的版本快照)
5)ReadView()生成的时机
Read Committed和Repeatable Read隔离级别之间最大的区别——生成ReadView的时机不同!!!
Read Committed:在一个事务中,每次读取数据前都生成一个ReadView
Repeatable Read:在一个事务中,只有第一次读取数据时生成一个ReadView
三、锁
1)并发事务问题——写-写
2)并发事务问题——读-写/写-读
方案1:读操作——
写操作——对记录进行
方案2:读、写操作都进行加锁
如果采用MVCC的方式,读-写批次并不冲突,性能更高(日常默认方案1)
如果采用加锁方式,读-写操作彼此需要排队执行,从而影响性能
Ps:一般来说,会选择MVCC方式来解决读-写操作并发执行的问题,但是在某些特殊的业务场景(如:银行业务,需要读取最新数据。严格要求数据准确性,对执行时间却并无苛刻要求),才会选择读、写操作都进行加锁
3)行级锁
共享锁/S锁(Shared Lock):在事务要读取一条记录时,需要先获取该记录的S锁
独占锁/排它锁/X锁(Exclusive Lock):在事务要修改一条记录时,需要先获取该记录的X锁
针对操作:
1、加写锁:select * from tb for update
2、加读锁:select * from tb lock in share mode
针对操作:先在B+树中定位到这条记录的位置,然后获取这条记录的,最后执行操作
针对操作:一般情况,新插入的一条记录受保护,不需要在内存中为其生成锁结构。不过事务插入一条记录之前,需要先定位该记录在B+树中的位置,如果该位置的下一条记录已经加了或。那当前事务就会为该记录加并进入等待状态
针对操作,分为三种情况:
1、并且记录更新前后所:先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,最后在原位置修改
2、并且记录更新前后所:先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,之后将原纪录彻底删除(即:把记录移入垃圾链表),最后插入一条新记录
3、:原纪录之行DELETE操作之后,再INSERT一条新数据。加锁操作按照DELETE和INSERT的规则进行
4)表级锁 & 表级意向锁
表级共享锁(S锁):其他事务可以继续获得/的锁,其他事务不可以继续获得/的锁
表级独占锁(X锁):其他事务不可以继续获得/的锁或锁
意向共享锁(IS锁):当事务准备在上加锁时,首先需要在表级别加一个锁
意向独占锁(IX锁):当事务准备在上加锁时,首先需要在表级别加一个锁
IS锁和IX锁仅仅用来在之后需要加表级锁的时候(起标识作用),表中的记录有无加行级锁,而不是一条一条遍历查看记录有无加行级锁
5)InnoDB表级锁
InnoDB存储引擎提供的表级锁比较“鸡肋”,特殊情况才用(比如系统崩溃恢复数据)
Ps:当使用了auto_increment()修饰列的时候,就会涉及到AUTO_INC锁和轻量级锁。其中innodb_autoinc_lock_mode系统变量,用来控制到底使用上述两种方式中的哪一种
AUTO_INC锁和轻量级锁区别:多线程情况下,AUTO_INC一次只分配一个主键id,待其插入成功之后,其他要插入的线程才能获得AUTO_INC锁(AUTO_INC锁保证数据插入成功);轻量级锁可以一次分配多个id,不需要插入成功,其他要插入的线程也能获得轻量级锁(轻量级锁不保证数据插入成功)
6)InnoDB行级锁
LOCK_REC_NOT_GAP:记录锁,也就是仅仅负责把一条记录锁上的锁
LOCK_GAP:gap锁,锁住了指定记录,防止其间插入新纪录(gap锁仅仅是为了防止幻读现象)
LOCK_ORDINARY:next-key锁,本质就是+的合体。既能锁住该条记录,也能防止别的事务将新纪录插入到被保护记录前面的间隙
LOCK_INSERT_INTENTION:插入意向锁,事务在等待时也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新纪录,但是现在处于等待状态
隐式锁:
场景一:对于有一个trx_id隐藏列,该隐藏列记录着最后改动该记录的事务id,在当前事务新插入一条聚簇索引记录后,该记录的trx_id隐藏列代表的就是当前事务的事务id。如果其他事务想对该记录添加或,首先会看一下该记录的trx_id是否为当前活跃事务。如果不是正常读取,如果是就由想对该记录加锁的其他事务帮该事务创建一个X锁的锁结构,该锁结构的is_waiting属性为false,同时想对该记录加锁的其他事务自己也创建一个锁结构,该锁结构的is_waiting属性为true然后进入等待状态
场景二:对于本身没有trx_id隐藏列,但是在二级索引页的Page Header部分有一个属性,该属性代表对该页面做改动的最大事务id。如果此属性,说明对该页面改动的事务都已提交,否则就需要在页面中定位到对应的二级索引记录,再回表找到对应的聚簇索引,然后再重复场景一操作
7)InnoDB锁的内存结构
8)加锁操作解析
8.1)普通的select语句
8.2)锁定读语句
归类为以下四种语句:
语句一:SELECT ... LOCK IN SHARE MODE;
语句二:SELECT ... FOR UPDATE;
语句三:UPDATE ...
语句四:DELETE ...
不管是,还是,首要条件是先查到对应的语句再进行操作
SELECT ... LOCK IN SHARE MODE示例一:
针对number为搜索条件,隔离级别为或,执行
SELECT ... LOCK IN SHARE MODE示例二:
针对number为搜索条件,隔离级别为或,执行
SELECT ... LOCK IN SHARE MODE示例三:
针对name作为搜索条件,隔离级别为或,执行
'' ''
SELECT ... LOCK IN SHARE MODE示例四:
针对name作为搜索条件,隔离级别为或,执行
‘‘ ‘‘
UPDATE ... 示例一:
针对name进行修改,隔离级别为或,执行
''
PS::当Update语句读取到已经被其他事务加了的记录时,InooDB将此记录的读出来,再判断最新版本记录是否和Update语句的,不匹配则忽视该记录跳到下一条记录;匹配则再次读取该记录并加锁(仅当隔离级别为Read Uncommitted或Read Committed且执行Update操作时,采用半一执行性读)
UPDATE ... 示例二:
针对name进行修改,隔离级别为
或,执行
‘‘
SELECT ... FOR UPDATE & DELETE ... :
SELECT ... FOR UPDATE语句的加锁过程与SELECT ... LOCK IN SHARE MODE语句类似,区别点在于SELECT ... FOR UPDATE为记录加。
DELETE ... 语句加锁过程与UPDATE ...语句处理方式相同,当表中含有,或二级索引记录之前实际加的是,效果等同于加记录锁
8.3)加锁操作补充内容
二级索引精准匹配(非范围查找)示例一:
隔离级别为或时,不会为扫描记录的后一条。比如执行
select * from tb_user where name = ‘muse’ for update,情况如下图
二级索引精准匹配(非范围查找)示例二:
隔离级别为或时,会为扫描记录的后一条。比如执行
select * from tb_user where name = ‘muse’ for update,情况如下图
二级索引查找不到记录,示例一:
隔离级别为或并且是,则为扫描区间。比如执行
select * from tb_user where name = ‘moon’ for update,情况如下图
二级索引查找不到记录,示例二:
隔离级别为或并且是,则为扫描区间。比如执行
select * from tb_user where name > ‘m’ and name < 't' for update,情况如下图
左闭区间加锁示例:
隔离级别为或,使用,如果定位到第一个聚簇索引记录的number值刚好与扫描区间中最小的值。那么会为该聚簇索引记录加。比如执行
select * from tb_user where number >= 3 for update,情况如下图
自右向左扫描加锁(DESC)示例:
隔离级别为或,的顺序扫描,会讲匹配到的第一条记录。比如执行
select * from tb_user where name >= 'john' and name <= 'tom' and age = 20 order by name FOR UPDATE,情况如下图