mysql 锁

锁是一种很珍贵的资源。锁一定是并发场景下才会出现的。

保证数据的隔离性,一致性。

1.latch

这个锁存在于内存中,用来控制并发访问的,保护的是内存数据结构。他锁住的是并发资源。就是临界区。

就是java和golang中的mutex。

他加锁的对象是线程。持续时间是临界时间。只有读写锁和互斥量。并且没有死锁检测机制,这个是纯粹靠应用程序加锁的顺序保证无死锁。

查看:show engine innodb mutex;

 

2.lock

加锁的对象是事务。

持续的时间是整个事务周期。死锁通过waits-for graph , time out机制控制

存在于lock manager的hashtable中。

分类:

S行级共享锁(lock in share mode)

X行级排它锁(写和当前读)

IS

IX

AI自增锁

相关的信息你可以show engine innodb status\G,找到transaction部分。

innodb_lock_wait_timeout可以调少一点,调到秒级。

s和x就不说了,老生常谈了。主要聊一下下面的三种。

 

对于is和ix。表示下一层级请求的锁的类型。

is表示事务想获得一个表中某几行的共享锁。

ix表示事务想获得一个表中某几行的排它锁。

意向锁都是表锁。意向锁都是为了实现多粒度级别的锁。

举一个例子。

数据库的层级应该是怎么划分的?

 

database     table     page     row

4个层级。

我要对row=1这行加一个x锁。那么从上至下,锁是怎么加的?

database(IX)  table(IX)  page(IX)  row(X)

这时候,我又要对row=2这行加一个X锁。那么database层能不能拿到IX锁?

答案是能,因为IX表示的是下一层级的锁,也就是IX是table的锁,database本身没有锁。而到了table层级,他持有的也是IX锁,表示我table层级没有锁,我持有的是下一层page的锁。到了page层同理。只有到了row级,发现,如果我们两个需要对同一行进行dml,才会发生争用。

而对于mysql来说,上面的四个层级,意向锁只会在table层发生。database和page是没有的。所以说意向锁是表锁。

有个参数innodb_status_output_locks这个参数打开,你看锁信息会多一点。

还有几张系统表:

sys.innodb_lock_waits

ps.innodb_trx

ps.innodb_locks

ps.innodb_lock_waits

 

show engine innodb status中的heap no表示的是page中的插入顺序。也表示的是具体的记录,也是行锁的表现。每页的heap no都是从2开始的。0和1被系统给了min和max。

 

innodb中的锁管理:

每个页会有个锁的对象

 

 

 

 

也就是说,锁其实是要消耗资源的。

通过位图的方式来管理锁的。一个锁可能是在30字节左右。

 

自增锁:mysql的自增值是没有持久化的。也就是说,如果mysql重启了,他会先执行select max(auto_increment_col) from table for update.取的当前最大的自增值。8.0的话就没有这个问题。8.0会对自增值持久化。

同样也就是这个原因。自增列必须有索引,没有索引你建表会失败。

这个锁与其他锁最大的不同是他在事务提交前就已经释放了。你insert执行完毕就已经释放了。

这就带来了一个问题,如果我有一个需求,一个大事务,批量插入100w数据。那在这100w数据插入完成之前,你的线上业务是插入不了的。

解决方式有两种,一个就是拆事务,拆小一点,5000个数据一批,慢慢跑

还有种方式就是修改innodb_autoinc_lock_mode为2.这个值的意思是,我自增列每自增一次就释放一次锁。这样就不需要等100w数据插入完成了。

 

锁的算法:

recode lock:记录锁。对单个记录上锁。

gap lock:范围锁。对某个范围加锁,但是不包含记录本身。

next-key lock:上面两个锁的结合,既锁住记录本身,也锁住范围。

innodb锁住的都是索引。

所谓的rr隔离级别,标识的就是他对所有记录的加锁算法都是next-key lock,但是如果你锁住的索引是唯一的,并且只返回一条记录,那么会退化为recode lock。

而rc,都是recode lock。

 

在rr的情况下,会出现一条sql语句,同时加3个锁。

举例:a列主键,b列索引。现有如下记录:(2,4)(4,6)(6,8)(8,10)

查询:where b=6 for update.

会加如下锁:在b列索引上加,next-key lock 范围:(4,6],还会加gay lock 范围:(6,8).

同时,还会在主键索引上对a=4加 recode lock。

这时候如果你要插入一条(3,4)那么是会等待锁的。(明明4是开区间,为什么插入失败。因为插入是插入到当前记录的后面,所以是有锁的。)

但是如果你要插入(1,4).那么会成功。因为二级索引是包含主键值的。上面的锁范围其实是包括主键的,也就是说实际的锁范围如下:((4,2),(6,4)],((6,4),(8,6))。

 

如果你在这是换一个查询:where b = 12 for update。

这时候锁住的是什么?

我们上面说了,heap no从2开始才给到用户数据。这时候,你会发现锁在heap no 1上。对max进行的加锁。

也就是说,这是锁在(10,max)上。

 

这里补充说一点。如何判断,我的当前查询是阻塞还是正常运行。

在MySQL中,有个全局的active_trx_list表,里面保存着当前活跃的事务的trx_id(即没有提交)(trx_id在之前讲过,就一个记录的隐藏列,trx_id,rooback_ptr)

这个trx_id是全局自增的(持久化在共享表空间)。当你开启了一个查询,他会有个read_view,同时拷贝一份当前的active_trx_list。然后把当前查询的trx_id拿去一个一个比较。如果查询出来,有重复的trx_id,那么这个row对当前查询就是不可见的。所以这个时候就要去查undo,查看他的上一个版本。

而rr和rc的区别就在这。rr的read_view,每个事务创建一次。而rc每个查询重新创建一次。这就为rc带来了性能开销,如果一个事务中有大量查询,那么rc的性能会比rr差。(测dml语句不行,因为有next-lock锁,rr性能一定比rc差)

 

另外,这个read_view是在sql执行的时候创建的,不是在begin的时候,rr,rc都一样。如果要在事务开始的时候创建,那么要用start transaction with consisitent snapshot;

 

 

插入意向锁(insertion intention lock):

这是一个隐式锁。你插入的时候是看不见这把锁的。只有其他事务阻塞了,这把锁才会显式出现。

你如果想显式地观察这个锁。你把隔离级别设置为rr,然后对主键进行一个范围查询,然后开第二个事务,在你查询范围内insert一条记录,你就能观察到这把锁。实际上是类似于gap锁。

为什么说是类似于,因为这个gap锁只阻塞当前线程。

比如说我:

事务0:select xxxx  where a < 20 for update; a是主键,假设a的值只有10,20.

事务1: insert into xxxx where a = 14;

事务2: insert into xxxx where a = 15;
当事务0commit后,事务1持有的是(14,20)的插入意向锁。如果是单纯的gap锁,事务2其实是要阻塞的。但是实际上,事务2是能正常提交的。

所以插入意向锁的本质是为了提高插入效率。

 

posted @ 2022-06-29 17:30  拿什么救赎  阅读(199)  评论(0编辑  收藏  举报