MySQL中的锁

最近疯狂的查看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,这样也就避免了不可重复读和幻读。


  • 读、写操作都进行加锁

    当然,有一些场景是不允许读取记录的旧版本,比如银行的存款。这样在读操作的时候,也要对其进行加锁操作。


总结

  1. 显而易见,这两种方法从性能上讲,MVCC的方式,读写不冲突,性能更高;而读写都加锁的方式,读写彼此都需要排队执行,肯定是会影响性能的,但是在特殊的场景业务下,还是需要加锁的
  2. 事务利用MVCC进行的读操作,称为一致性读,一致性无锁读,也称快照读(读取的快照)。普通的select语句在RC、RR下都算是一致性读。一致性读对表中的记录不会加锁,其他事务可以自由的对表中的记录进行改动。
  3. 在读取记录前就将该记录加锁的读取方式称为锁定读(Locking Read)。

行级锁

上面讲到事务并发造成的问题,使用MVCC或加锁来解决。

使用加锁来解决,既要保持读-读不受影响,又要保证读-写或写-读、写-写的操作互相阻塞。就有了如下的各种锁:

  • 共享锁

    共享锁(Shared Lock),又称读锁,S锁。事务在读取一条记录时,需要先获取该记录的S锁。

  • 独占锁

    独占锁(Exclusive Lock),又称写锁,X锁。事务在修改一条记录时,需要先获取该记录的X锁。

这样一来:

  1. 要保证读-读不受影响,那么S锁与S锁具有兼容性;

  2. 要保证读-写或写-读操作互相阻塞,那么S锁与X锁不兼容;

  3. 要保证写-写操作互相阻塞,那么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=0innodb_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列生成对应的值。避免了锁表,提升性能。


    扩展:

    1. AUTO-INC锁与其他锁不同,在插入语句执行完毕就会释放。上面介绍的锁,通常是在事务提交以后才会释放。

    2. 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是怎样运行的》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值