概述

对于后端Java开发人员来说,锁主要有Java锁和DB锁。Java锁,请参考​​一文总结Java开发各种锁​​。本文所述的DB锁,可能会局限于MySQL数据库。

隔离级别与锁的关系

  • 在RU级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突
  • 在RC级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁
  • 在RR级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁
  • SERIALIZABLE是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成

行锁,表锁,页锁

锁粒度:

  • 表锁:table-level locking,MYISAM引擎,开销小,加锁快;不会出现死锁;锁粒度大,发生锁冲突概率高,并发度低;
  • 行锁:row-level locking,INNODB引擎,开销大,加锁慢;会出现死锁;锁粒度小,发生锁冲突的概率低,并发度高;
  • 页锁:BDB引擎,锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。取折衷的页级,一次锁定相邻的一组记录。开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

不同的存储引擎支持的锁粒度是不一样的:

  • InnoDB行锁和表锁都支持,默认为行级锁
  • MyISAM只支持表锁

乐观锁、悲观锁

乐观锁,是一种思想,而不是数据库层面上的锁,是需要自己手动去加的锁。通过版本号(时间戳),加字段或CAS算法方式实现。

共享锁和排它锁是悲观锁的不同的实现,都是行级锁。
要使用悲观锁,需要关闭MySQL数据库的自动提交属性,因为MySQL默认使用autocommit模式,即执行一个更新操作后,MySQL会立刻将结果进行提交。
设置MySQL为非autocommit模式:​​​set autocommit=0;​

共享锁、排它锁

共享锁

share lock,也叫read lock,S锁,读锁。读取操作创建的锁。共享锁指的就是对于多个不同的事务,对同一个资源共享同一个锁。

其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获得共享锁的事务只能读数据,不能修改数据。

在查询语句后面加上​​lock in share mode​​​,MySQL会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用共享锁的表,而且这些线程读取的是同一个版本的数据。
加上共享锁后,对于​​​update,insert,delete​​语句会自动加排它锁。

排它锁

exclusive lock,也叫writer lock,X锁,写锁。排它锁指对于多个不同的事务,对同一个资源只能有一把锁。若事务1对数据对象A加上X锁,事务1 可以读A也可以修改A,其他事务不能再对A加任何锁,直到事务1 释放A上的锁。这保证其他事务在事务1释放A上的锁之前不能再读取和修改A。排它锁会阻塞所有的排它锁和共享锁。

读取为什么要加读锁呢:防止数据在被读取的时候被别的线程加上写锁。

对于​​update, insert, delete​​​语句会自动给涉及到的数据集加排它锁。执行语句后面加上​​for update​​​就可以。
执行事务时关键字​​​select…for update​​会锁定数据,防止其他事务更改数据。但是锁定数据也是有规则的。查询条件与锁定范围:

  1. 具体的主键值为查询条件
    比如查询条件为主键ID=1等等,如果此条数据存在,则锁定当前行数据,如果不存在,则不锁定。
  2. 不具体的主键值为查询条件
    比如查询条件为主键ID>1等等,此时会锁定整张数据表。
  3. 查询条件中无主键
    会锁定整张数据表。
  4. 如果查询条件中使用索引为查询条件
    明确指定索引并且查到,则锁定整条数据。如果找不到指定索引数据,则不加锁。

总结

行锁又分共享锁和排他锁。
(当然,说的是InnoDB引擎)行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁。
行级锁的缺点:由于需要请求大量的锁资源,速度慢,内存消耗大。

表锁下又分为两种模式:表读锁(表共享读锁,Table Read Lock)、表写锁(表独占写锁,Table Write Lock)
在表读锁和表写锁的环境下:读读不阻塞,读写阻塞,写写阻塞。读锁和写锁是互斥的,读写操作是串行。
如果某个进程想要获取读锁,同时另外一个进程想要获取写锁。在MySQL里边,写锁是优先于读锁的。写锁和读锁优先级,可以通过参数调节的:​​​max_write_lock_count​​​和​​low-priority-updates​​。

InnoDB引擎的行锁是怎么实现的?
InnoDB是基于索引来完成行锁,​​​select * from tab_with_index where id = 1 for update;​​​,​​for update​​可根据条件来完成行锁锁定,要求id是有索引键的列。如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起。

InnoDB存储引擎的锁的算法有三种

  • Record lock:单个行记录上的锁
  • Gap lock:间隙锁,锁定一个范围,不包括记录本身
  • Next-key lock:record+gap 锁定一个范围,包含记录本身

相关知识点:

  1. innodb对于行的查询使用next-key lock
  2. Next-locking keying为了解决Phantom Problem幻读问题
  3. 当查询的索引含有唯一属性时,将next-key lock降级为record key
  4. Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
  5. 两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数​​innodb_locks_unsafe_for_binlog​​设置为1

间隙锁GAP

当用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做间隙,GAP。InnoDB对间隙加锁,即间隙锁。间隙锁只会在Repeatable read隔离级别下使用。
间隙锁的目的:

  1. 为了防止幻读(Repeatable read隔离级别下再通过GAP锁即可避免幻读)
  2. 满足恢复和复制的需要

MySQL的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读

总结

一文总结MySQL各种锁_共享锁

  1. MyISAM存储引擎执行SQL语句自动加锁。查询语句给涉及的所有表加读锁,更新操作(UPDATE、DELETE、INSERT等)给涉及的表加写锁,这个过程并不需要用户干预;
  2. 乐观锁其实是一种思想;
  3. 悲观锁用的就是数据库的行锁;

死锁

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
常见的解决死锁的方法

  1. 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会;
  2. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
  3. 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;

如果业务处理不好可以用分布式事务锁或者使用乐观锁

参考

​MySQL的锁​​​​很全面的InnoDB锁机制​