最近疯狂的查看MySQL相关书籍,学习MySQL的东西。主要咱之前也没对MySQL理解的这么到位,不知道的东西咱也不敢瞎写啊,写错了说不定又被怼,哈哈。磨磨唧唧的看了几天书,才写了这么点。= =
什么是锁?
锁是协调多个进程或线程并发访问某一资源的一种机制。
解决并发事务带来的问题
并发事务访问相同记录的情况大致可以分为3种:
-
读 - 读
读操作不会对数据有任何影响,也不会引发什么问题。该情况可以忽略。
-
写 - 写
多个未提交的事务对同一条数据对修改操作,需要让他们排队执行。这个排队本质就是通过为该记录加锁来实现。
-
读 - 写 或 写 - 读
在该情况下,会出现脏读、不可重复读、幻读。
SQL 92 标准规定,不同的隔离级别有如下特点:
- RU隔离级别下,脏读、不可重复读、幻读都可能发生;
- RC隔离级别下,脏读不可能发生,不可重复读、幻读可能发生;
- RR隔离级别下,脏读、不可重复读不可能发生,幻读可能发生 ;
- SERIALIZABLE隔离级别下,上述现象都不可能发生。
不同的数据库厂商对SQL标准支持可能不一样,MySQL与SQL标准不同的是,在RR隔离级别下,很大程度的避免了幻读(在某些情况下可能会出现幻读)。
那么如何避免脏读、不可重复读、幻读这些现象?有两种可选方案:
-
读操作使用MVCC(多版本并发控制)、写操作进行加锁
MVCC通过生成ReadView,找到符合条件的记录版本(历史版本由undo日志构建),读操作只能读到生成ReadView之前已提交事务所做的更改,而在生成ReadView之前没有提交的事务或生成ReadView之后开启的事务对记录做的更改是看不到的,写操作针对的是最新版本的记录,与读记录的历史版本不发生冲突。
普通的select语句,在RC、RR隔离级别下,会使用MVCC读取记录:
在RC隔离级别下,一个事务在执行读操作时,每次select,都会生成一个ReadView。ReadView的存在,保证了事务不能读到未提交事务所做的更改,也就避免了脏读;
在RR隔离级别下,一个事务在执行过程中,只有第一个读操作才会生成一个ReadView,之后的select都会复用同一个ReadView,这样也就避免了不可重复读和幻读。
-
读、写操作都进行加锁
当然,有一些场景是不允许读取记录的旧版本,比如银行的存款。这样在读操作的时候,也要对其进行加锁操作。
总结
- 显而易见,这两种方法从性能上讲,MVCC的方式,读写不冲突,性能更高;而读写都加锁的方式,读写彼此都需要排队执行,肯定是会影响性能的,但是在特殊的场景业务下,还是需要加锁的
- 事务利用MVCC进行的读操作,称为一致性读,一致性无锁读,也称快照读(读取的快照)。普通的select语句在RC、RR下都算是一致性读。一致性读对表中的记录不会加锁,其他事务可以自由的对表中的记录进行改动。
- 在读取记录前就将该记录加锁的读取方式称为锁定读(Locking Read)。
行级锁
上面讲到事务并发造成的问题,使用MVCC或加锁来解决。
使用加锁来解决,既要保持读-读不受影响,又要保证读-写或写-读、写-写的操作互相阻塞。就有了如下的各种锁:
-
共享锁
共享锁(Shared Lock),又称读锁,S锁。事务在读取一条记录时,需要先获取该记录的S锁。
-
独占锁
独占锁(Exclusive Lock),又称写锁,X锁。事务在修改一条记录时,需要先获取该记录的X锁。
这样一来:
-
要保证读-读不受影响,那么S锁与S锁具有兼容性;
-
要保证读-写或写-读操作互相阻塞,那么S锁与X锁不兼容;
-
要保证写-写操作互相阻塞,那么X锁与X锁也不兼容。
对兼容性汇总:
S锁 | X锁 | |
---|---|---|
S锁 | √ | × |
X锁 | × | × |
MySQL中提供两种特殊的select语句格式来支持锁定读。
-
对读取的记录加S锁
SELECT ... LOCK IN SHARE MODE;
省略号省略的是常规的查询语句。比如:
SELECT name,age,address FROM t_user WHERE id=1 LOCK IN SHARE MODE;
事务A执行了该语句,会对id=1的这条记录加S锁,别的事务仍然可以获取到id=1这条记录的S锁,但是不能获取到id=1记录的X锁。想要获取到id=1记录的X锁,需要等待至事务A提交事务,释放了S锁。
-
对读取的记录加X锁
SELECT ... FOR UPDATE;
比如:
SELECT name,age,address FROM t_user WHERE id=2 FOR UPDATE;
事务A执行了该语句,对id=2这条记录加X锁,别的事务即获取不到S锁,也获取不到X锁,直到事务A提交完事务,将id=2这条记录的X锁释放。
表级锁
上面的S锁,X锁都只是针对表中的记录加锁,也叫行锁。那么按照锁的粒度来分,除了行锁,还有表锁,全局锁。
同样的,给表加锁也可以分为S锁、X锁。此外还包含IS锁、IX锁。
-
S锁
事务A给表t_user加了S锁,虽然事务B可以继续获得表t_user的S锁,也可以获得表t_user中记录的S锁;但是事务B不能继续获得表t_user的X锁,也不可以继续获得表t_user中记录的X锁。
-
X锁
事务A给表t_user加了X锁,事务B获取不到表t_user的S锁、X锁,也获取不到表t_user中记录的S锁、X锁。
-
IS锁
意向共享锁(Intention Shared Lock),简称IS锁。当事务在某条记录上加S锁时,需要在记录对应的表上加一个IS锁。
-
IX锁
意向独占锁(Intention Exclusive Lock),简称IX锁。当事务在某条记录上加X锁时,需要在记录对应的表上加一个IX锁。
为什么在记录上加S或X锁时,还要在表上加IS或IX锁?
表级锁IS、IX锁的提出,在之后对表加S锁或X锁时,可以快速判断表中的记录是否加了S锁或X锁,无需遍历表中的所有记录查看有没有上锁的记录。这样的话,IS锁与IX锁兼容,IX锁与IX锁兼容。
那么以表级别的锁兼容性做汇总:
S锁 | X锁 | IS锁 | IX锁 | |
---|---|---|---|---|
S锁 | √ | × | √ | × |
X锁 | × | × | × | × |
IS锁 | √ | × | √ | √ |
IX锁 | × | × | √ | √ |
MySQL中的行锁和表锁
不同的存储引擎支持的锁也不同。对于MyISAM、MEMORY、MERGE这些存储引擎来说,不支持事务,它们只支持表级锁,所以当我们为使用这些存储引擎的表加锁时,一般都是针对当前会话。
因为使用MyISAM、MEMORY、MERGE这些存储引擎的表在同一时刻,只允许一个session会话对表进行写操作,所以这些存储引擎实际上最好用在只读的场景下,或者用在大部分都是读操作或单用户的情境下。
重点讲解InnoDB中的锁。 InnoDB存储引擎既支持表级锁,也支持行级锁。
表级锁粒度粗,资源占用少,性能比较差;
行级锁粒度细,更加精准的并发控制,占用资源较多。
InnoDB中的表级锁
-
表级S锁、X锁
在对表执行select,insert,update,delete语句时,InnoDB存储引擎是不会对该表加S锁或X锁。
InnoDB存储引擎提供的表级S锁或X锁仅仅在一些特殊情况下用到(比如系统崩溃恢复时)。
手动获取InnoDB存储引擎表的S锁或X锁(在
autocommit=0
,innodb_table_locks=1
前提下):获取S锁:
LOCK TABLES t_user READ;
获取X锁:
LOCK TABLES t_user WRITE;
扩展:
当对表执行DDL语句时,其他事务的对该表的增删改查SQL语句会发生阻塞。反之,当事务在进行增删改查的SQL操作时,其他会话执行同表的DDL语句时也会阻塞。本质是在server层使用元数据锁(Metadata Lock,MDL)实现。
DDL语句执行一般会在若干个特殊事务中完成,在开启这些特殊事务前,需要将当前会话中的事务提交。
尽量避免在InnoDB表中手动锁表,一来降低了并发,二来也不会提供额外的保护。
-
表级IS锁、IX锁
在上面已经介绍,对行记录加S锁前,先对该记录对应的表加IS锁;对行记录加X锁前,先对该记录对应的表加IX锁;
-
AUTO-INC锁
我们知道,InnoDB独有的特点,自增键(AUTO_INCREMENT)。
对修饰AUTO_INCREMENT如何自增?MySQL实现的方式主要有:
-
使用AUTO-INC锁。在执行插入语句时,加一个表级别的AUTO-INC锁,为该列分配递增值。插入语句完成后,再把锁释放掉。这样一来,一个事务在持有AUTO-INC锁时,其他事务的插入语句均被阻塞。从而保证一个语句中分配的递增值连续。
如果插入语句在执行前不明确有多少条记录(比如:insert … select),一般使用AUTO-INC锁为AUTO_INCREMENT列生成对应的值
-
采用一个轻量级锁
所谓轻量级锁,在插入语句获取到AUTO_INCREMENT列生成对应的值后就释放掉该锁,无需等到整个插入语句执行完毕才释放锁。
如果插入语句在执行前明确有多少条记录,使用轻量级锁为AUTO_INCREMENT列生成对应的值。避免了锁表,提升性能。
扩展:
-
AUTO-INC锁与其他锁不同,在插入语句执行完毕就会释放。上面介绍的锁,通常是在事务提交以后才会释放。
-
MySQL提供
innodb_autoinc_lock_mode
系统变量控制使用上面的两种锁来为AUTO_INCREMENT列赋值。当
innodb_autoinc_lock_mode=0
,使用AUTO-INC锁;当
innodb_autoinc_lock_mode=1
,两种锁混用,插入记录固定,使用轻量级,不固定使用AUTO-INC锁;当
innodb_autoinc_lock_mode=2
,使用轻量级锁。注意:
innodb_autoinc_lock_mode=2
时,存在不同事务插入语句,其AUTO_INCREMENT列的值是交叉的,在主从复制场景中不安全。
-
InnoDB中的行级锁
InnoDB中的行锁有多种类型,对同一条记录加行锁,类型不同,作用也不同。
-
Record Lock
LOCK_REC_NOT_GAP。
记录锁?仅仅把确定的一条记录上锁。
-
Gap Lock
LOCK_GAP
锁住该记录前的间隙,防止其他事务向该间隙插入新记录。
因为MySQL在RR隔离级别下很大程度的解决了幻读,除去使用MVCC方案,使用加锁的方案解决,gap锁的作用是为了防止插入幻影记录。
-
Next-key Lock
LOCK_ORDINARY
既可以锁住某条记录,又防止其他事务向该记录前的间隙插入新记录。本质是Record Lock和Gap Lock的结合体。
-
Insert Intention Lock
LOCK_INSERT_INTENTION
插入意向锁。一个事务A在插入记录时,发现插入的位置被另一个事务B加了gap锁,那么事务A需要等待至事务B提交事务,释放了gap锁以后才能插入。事务A在等待时也需要在内存中生成一个锁结构,表明自己在某个间隙中插入记录,但处于等待状态。
-
隐式锁
依赖于trx_id(事务id)来保护不被别的事务来改动该记录。
InnoDB存储引擎属于聚簇索引,存在一个trx_id隐藏列。记录着最后改动该记录的事务的事务id。如果其他事务对该记录加S锁或X锁,会先查询trx_id是否是当前的活跃事务。如果是,可以正常读取,如果不是,就帮组当前事务创建一个X锁的锁结构,再为自己创建一个锁结构,进入等待状态。对于二级索引,通过回表操作找到聚簇索引的记录,仍然同上。
参考文献:《MySQL是怎样运行的》