文章目录
- [1. MySQL并发事务访问相同记录](#1. MySQL并发事务访问相同记录)
- [2. 锁的分类](#2. 锁的分类)
-
- [2.1 共享锁和排他锁](#2.1 共享锁和排他锁)
- [2.2 表锁、行锁、页锁](#2.2 表锁、行锁、页锁)
-
- [2.2.1 表锁(Table Lock)](#2.2.1 表锁(Table Lock))
- [2.2.2 行锁](#2.2.2 行锁)
- [2.3 乐观锁、悲观锁](#2.3 乐观锁、悲观锁)
- [2.4 按加锁的方式分:显示锁、隐式锁](#2.4 按加锁的方式分:显示锁、隐式锁)
- 2.5全局锁和死锁
- [3. 锁的内存结构](#3. 锁的内存结构)
事务的隔离性由锁来实现
1. MySQL并发事务访问相同记录
读-读: ok
写-写: 脏写问题,一个事务已经commit了但是发现没写进去。--->排队执行is_waiting
读-写、写-读问题:这种情况下可能发生 脏读
、 不可重复读
、 幻读
的问题。
并发问题的解决方案:
-
方案一:
读
操作利用多版本并发控制(MVCC
),写
操作进行加锁
。普通的SELECT语句在
READ COMMITTED
和REPEATABLE READ
隔离级别下会使用到MVCC读取记录。在 READ COMMITTED 隔离级别下,一个事务在执行过程中
每次执行
SELECT操作时都会生成
一个ReadView
,ReadView的存在本身就保证了 事务不可以读取到未提交的事务所做的更改 ,也就 是避免了脏读
现象;在
REPEATABLE READ
隔离级别下,一个事务在执行过程中只有 第一次
执行SELECT操作 才会生成一个ReadView,之后的SELECT操作都 复用 这个ReadView,这样也就避免了不可重复读 和幻读
的问题 -
方案二:读、写操作都采用加锁的方式
问题:可以解决脏读、不可重复读,幻读的话---不知道
2. 锁的分类
2.1 共享锁和排他锁
共享锁:S锁。读操作可以同时进行而不会互相影响,相互不阻塞的。SELECT ... FOR share
排他锁:X锁。当前写操作没有完成前,它会阻断其他写锁和读锁。SELECT ... FOR UPDATE
对于InnoDB引擎
来说,读锁和写锁可以加在表上,也可以加在行上。
写操作:
- DELETE:
对一条记录做DELETE操作的过程其实是先在B+树中定位
到这条记录
的位置,然后获取这条记录的X锁
,再执行delete mark.
操作(标记一下)。也可以把这个定位待删除记录在B+树中位置的过程看成是一个获取X锁的锁定读。 - UPDATE︰在对一条记录做UPDATE操作时分为三种情况:
情况1: 未修改该记录的键值,并且被更新的列占用的存储空间在修改前后未发生变化。 则先在B+树中定位到这条记录的位置,然后再获取一下记录的X锁,最后在原记录的位置进行修改操作。也可以把这个定位待修改记录在B+树中位置的过程看成是一个获取X锁的锁定读。
情况2∶未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发生变化。 则先在B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在B+树中位置的过程看成是一个获取X锁的锁定读,新插入的记录由INSERT
操作提供的隐式锁进行保护。
情况3∶ 修改了该记录的键值,则相当于在原记录上做DELETE操作之后再来一次INSERT操作,加锁操作就需要按照DELETE和INSERT的规则进行了。 - INSERT :
一般情况下,新插入一条记录的操作并不加锁,通过一种称之为隐式锁
的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。
2.2 表锁、行锁、页锁
2.2.1 表锁(Table Lock)
会锁定整张表,是开销最小的策略(因为粒度比较大)。可以很好的避免死锁问题。并发性能差。
MyIsam默认是表锁,InnoDB默认是行锁
-
表级 S、X锁
-
意向锁 :它允许 行级锁 与 表级锁 共存,即意向锁和行级锁兼容(我认为是一种
行级锁
的标志
)- 意向锁是一种不与行级锁冲突表级锁,这一点非常重要。
- 表明"某个事务正在某些行持有了锁或该事务准备去持有锁"
如果我们给某一行加上了排它锁
,InnoDB会向表级别加上意向锁,告诉其他人这个表已经有人上过排它锁了,这样当其他人想要获取表级别锁
的时候,只需要了解是否有人已经获取了这个表的意向排他锁即可。原来需要一条记录一条查,看看行有没有X锁,现在不用了,在表的意向排他锁看一下就ok
口述:意向锁其实就像一种行级锁的标志,当我们要加表锁时,如果表中有行的X锁的话,那其实是不能让这个事务获得表锁的,这样的话如果没有意向锁的话就要去一条一条记录去查看看有没有X锁,这样的话就性能低,意向锁就是在表级别上的这样一个锁,它描述的就是表中记录的X锁啊S锁啊这种,其实更类似于一种行锁的标记吧,也是为了快速判断能不能加表锁。
- 自增锁(AUTO-INC锁):id字段声明了AUTO_INCREMENT,意味着在书写插入语句时不需要为其赋值
- 元数据锁(MDL锁):为了保证DDL与DML语句的正确性的,比如在DML时候,会加MDL读锁,在DDL时,会加MDL写锁,读写是互斥的,保证了数据正确性。比如:先查出数据,其次对表结构更改了,然后再查出数据,这肯定是不可以的,表结构都改了查出来也肯定不一样。
2.2.2 行锁
-
记录锁:S锁和X锁
-
间隙锁:以锁的方式解决了幻读,事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些 幻影记录 加上 记录锁 ,间隙锁可以。
-
临键锁:innodb默认的锁就是临键锁,它其实是记录锁和间隙锁的一种结合】
在3-8之间加间隙锁,id为8的记录加记录X锁。
-
插入意向锁:插入意向锁是一种 特殊的Gap锁 ,在insert操作时产生。
插入意向锁其实是在插入时,被间隙锁阻塞,这时候事务会持有这个插入意向锁,锁结构里面is_waiting为true,等待持有间隙锁的事务释放了锁才能insert。
2.3 乐观锁、悲观锁
- 乐观锁:update version字段,条件是version
- 悲观锁:会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。Java中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
select ... for update语句执行过程中所有扫描的行都会被锁上,因此在MySQL中用悲观锁必须确定使用了索引,而不是全表扫描,否则将会把整个表锁住
。
2.4 按加锁的方式分:显示锁、隐式锁
口述:隐式锁:其实这种锁结构的话我们查是查不到的,所以叫它为隐式锁,比如说我们执行insert时,它其实是不会加锁的,当然只是针对隐式锁,这种写操作的话是会加X锁的,那针对隐式锁,当insert时候别的事务没有访问这条记录,那自然不会产生冲突,也就没有这种隐士锁,那当insert时别的事务也要访问这条记录,这时候其实这些访问的事务会为原来的事务加一个隐式锁,吧insert的事务的is_waiting设置为false,自己的设计为true,然后等待,其实它是判断了insert这条记录的隐藏列中的事务id,一看别的事务加了就为原来事务生成个隐式锁,自己的锁结构设置为等待状态,从而避免了脏写和脏读。
显示锁:前面的----------略
2.5全局锁和死锁
全局锁:在库的层面加锁
死锁:行锁
可能导致死锁。
死锁:
如何处理死锁:
- 方式1:等待,直到超时(innodb_lock_wait_timeout=50s)这种方式不太好,我们设计锁的目的就是为了避免同时进入,让一些事务等待,设置超时时间的话粒度不好控制
- 方式2:使用死锁检测处理死锁程序(检测有没有环)
死锁检测的原理是构建一个以事务为顶点,锁为边的有向图,判断有向图是否存在环,存在既有死锁。
3. 锁的内存结构
略