按照锁粒度进行划分

  • 根据加锁的范围
    • 全局锁,表级锁,行锁
  • 全局锁
    • MySQL提供了一个加全局读锁的方法
    • Flush tables with read lock(当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。)
    • 全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都 select 出来存成文本。
    • 使用全局锁
      • 如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆;
      • 如果你在从库上备份,那么备份期间从库不能执行主库同步过来的 binlog,会导致主从延迟。官方自带的逻辑备份工具mysqldump,当mysqldump使用参数—single-transaction的时候,会启动一个事务,确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
    • 既然要全库只读,为什么不使用 set global readonly=true ?
      • 在有些系统中,readonly 的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。
      • 在异常处理机制上有差异。如果执行 FTWRL 命令之后由于客户端发生异常断开,那么 MySQL 会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为 readonly 之后,如果客户端发生异常,则数据库就会一直保持 readonly 状态,这样会导致整个库长时间处于不可写状态,风险较高。
  • 表级锁
    • 表锁
    • 元数据锁MDL
      • MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是,保证读写的正确性。在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
      • 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
      • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
  • 行锁
    • 在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
    • 如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁的申请时机尽量往后放。(持有的时间最少)
    • MySQL 的行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如 MyISAM 引擎就不支持行锁。不支持行锁意味着并发控制只能使用表锁

从数据库管理的角度对锁进行划分

共享锁

  • 允许事务读一行数据

也叫读锁或 S 锁,共享锁锁定的资源可以被其他用户读取,但不能修改。在进行 SELECT 的时候,会将对象进行共享锁锁定,当数据读取完毕之后,就会释放共享锁,这样就可以保证数据在读取时不被修改。

给 product_comment 在表上加共享锁,可以使用下面这行命令:

  1. LOCK TABLE product_comment READ;

给某一行加上共享锁

  1. SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id = 912178 LOCK IN SHARE MODE

排它锁

(select * from table for update)

也叫独占锁、写锁或 X 锁。排它锁锁定的数据只允许进行锁定操作的事务使用,其他事务无法对已锁定的数据进行查询或修改。

常用角度对进行划分

乐观锁

乐观锁(Optimistic Locking)认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者时间戳机制实现。

在表中设计一个版本字段 version,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行 UPDATE … SET version=version+1 WHERE version=version。此时如果已经有事务对这条数据进行了更改,修改就不会成功。

它会先尝试对资源进行修改,在写回时判断资源是否进行了改变,如果没有发生改变就会写回,否则就会进行重试,在整个的执行过程中其实都没有对数据库进行加锁;

悲观锁

悲观锁(Pessimistic Locking)也是一种思想,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。

它会在获取资源前对资源进行加锁,确保同一时刻只有有限的线程能够访问该资源,其他想要尝试获取资源的操作都会进入等待状态,直到该线程完成了对资源的操作并且释放了锁后,其他线程才能重新操作资源;

image.png

乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。

悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止读 - 写和写 - 写的冲突。

乐观锁不会存在死锁的问题,但是由于更新后验证,所以当冲突频率和重试成本较高时更推荐使用悲观锁,而需要非常高的响应速度并且并发量非常大的时候使用乐观锁就能较好的解决问题,在这时使用悲观锁就可能出现严重的性能问题;在选择并发控制机制时,需要综合考虑上面的四个方面(冲突频率、重试成本、响应速度和并发量)进行选择。

  • 死锁
    • 当出现死锁以后,有两种策略
      • 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout (默认50s)来设置。
      • 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。
  • 在开发的时候如何安排正确的事务语句
    • 如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁的申请时机尽量往后放。
  • 两阶段锁
    • 在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放, 而是要等到事务结束时才释放。

锁的种类一般分为乐观锁和悲观锁两种,InnoDB 存储引擎中使用的就是悲观锁,而按照锁的粒度划分,也可以分成行锁和表锁。

InnoDB 实现了标准的行级锁,也就是共享锁(Shared Lock)和互斥锁(Exclusive Lock);共享锁和互斥锁的作用其实非常好理解:

  • 共享锁(读锁):允许事务对一条行数据进行读取;
  • 互斥锁(写锁):允许事务对一条行数据进行删除或更新;

共享锁之间是兼容的,而互斥锁与其他任意锁都不兼容:

锁的粒度

排他锁(写锁)
**
select … for update 其他 session 可以查询该记录,但是不能对该记录加共享锁或排他锁,而是等待获得锁
其他事务不能读取,也不能写。

共享锁(读锁)
**
其他事务可以读,但不能写。

无论是共享锁还是互斥锁其实都只是对某一个数据行进行加锁,InnoDB 支持多种粒度的锁,也就是行锁和表锁;为了支持多粒度锁定,InnoDB 存储引擎引入了意向锁(Intention Lock),意向锁就是一种表级锁。

意向锁也分为两种:

  • 意向共享锁:事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁;
  • 意向互斥锁:事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁;

锁的算法

三种锁的算法:Record Lock、Gap Lock 和 Next-Key Lock。

Record Lock

记录锁(Record Lock)是加到索引记录上的锁,假设我们存在下面的一张表 users:

  1. CREATE TABLE users(
  2. id INT NOT NULL AUTO_INCREMENT,
  3. last_name VARCHAR(255) NOT NULL,
  4. first_name VARCHAR(255),
  5. age INT,
  6. PRIMARY KEY(id),
  7. KEY(last_name),
  8. KEY(age)
  9. );

如果我们使用 id 或者 last_name 作为 SQL 中 WHERE 语句的过滤条件,那么 InnoDB 就可以通过索引建立的 B+ 树找到行记录并添加锁,但是如果使用 first_name 作为过滤条件时,由于 InnoDB 不知道待修改的记录具体存放的位置,也无法对将要修改哪条记录提前做出判断就会锁定整个表。

Gap Lock

记录锁是在存储引擎中最为常见的锁,除了记录锁之外,InnoDB 中还存在间隙锁(Gap Lock),间隙锁是对索引记录中的一段连续区域的锁;当使用类似 SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE; 的 SQL 语句时,就会阻止其他事务向表中插入 id = 15 的记录,因为整个范围都被间隙锁锁定了。

虽然间隙锁中也分为共享锁和互斥锁,不过它们之间并不是互斥的,也就是不同的事务可以同时持有一段相同范围的共享锁和互斥锁,它唯一阻止的就是其他事务向这个范围中添加新的记录。

一致性锁定读

在默认配置下,事务的隔离级别为可重复读,InnoDB存储引擎的select操作使用一致性非锁定读,在某些情况下,用户需要显式的对数据库的读取操作进行加锁以保证数据逻辑的一致性

InnoDB支持2种一致性的操作

  • select … for update (加了一个x锁,排它锁,其他事务不能对已锁定的行加任何锁)
  • select … lock in share mode (加了一个s锁,共享锁,其他事务可以向被锁定的行加s锁,但是如果加x锁,会被阻塞)

锁的3个算法

行锁的3个算法

InnoDB存储引擎的3种行锁的算法

  • Record Lock 单个行记录上的锁
    • 总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,这时会创建一个隐式主键来锁定
  • Gap Lock 间隙锁,锁定一个范围,但不包括记录本身
  • Next-Key lock:Record Lock + Gap Lock 锁定一个范围,并且锁定记录本身
    • InnoDB对于行的查询都是采用的这种锁定算法

脏读

脏页是指在缓冲池中已经被修改的页,但是还没有刷新到磁盘中,即数据库实例内存中的页和磁盘中的页的数据是不一致的,当然在刷新到磁盘之前,日志都已经被写入到了重做日志文件中,而所谓脏数据是指事物对缓冲池中行记录的修改,并且还没有提交。

脏读指的是在不同事物下,当前事物可以读到另外事物未提交的数据。简单来说就是可以读到脏数据。

不可重复读

不可重复读指的是在一个事物内多次读取同一数据集合,在这个事物还没结束时,另外一个事物也访问了该同一数据集合,并做了DML操作,因此,在第一个事物中的两次读数据之间,由于第二个事物的修改,那么第一个事物两次读取的数据可能是不是一样的,这么就发生了在一个事物内的两次读到的数据是不一样的情况。

不可重复读和脏读的区别是:脏读是读到未提交的数据,而不可重复读读到的数据却是已经提交的数据,但其实违反了数据库事物的一致性的要求。

行锁升级为表锁的原因

  • SQL 语句中未使用到索引,或者说使用的索引未被数据库认可(相当于没有使用索引)。
  • 当“值重复率”低时,甚至接近主键或者唯一索引的效果,“普通索引”依然是行锁;当“值重复率”高时,MySQL 不会把这个“普通索引”当做索引,即造成了一个没有索引的 SQL,此时引发表锁

参考

面对信仰编程