概述
对于后端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
会锁定数据,防止其他事务更改数据。但是锁定数据也是有规则的。查询条件与锁定范围:
- 具体的主键值为查询条件
比如查询条件为主键ID=1等等,如果此条数据存在,则锁定当前行数据,如果不存在,则不锁定。 - 不具体的主键值为查询条件
比如查询条件为主键ID>1等等,此时会锁定整张数据表。 - 查询条件中无主键
会锁定整张数据表。 - 如果查询条件中使用索引为查询条件
明确指定索引并且查到,则锁定整条数据。如果找不到指定索引数据,则不加锁。
总结
行锁又分共享锁和排他锁。
(当然,说的是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 锁定一个范围,包含记录本身
相关知识点:
- innodb对于行的查询使用next-key lock
- Next-locking keying为了解决Phantom Problem幻读问题
- 当查询的索引含有唯一属性时,将next-key lock降级为record key
- Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
- 两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数
innodb_locks_unsafe_for_binlog
设置为1
间隙锁GAP
当用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做间隙,GAP。InnoDB对间隙加锁,即间隙锁。间隙锁只会在Repeatable read隔离级别下使用。
间隙锁的目的:
- 为了防止幻读(Repeatable read隔离级别下再通过GAP锁即可避免幻读)
- 满足恢复和复制的需要
MySQL的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录,也就是不允许出现幻读
总结
- MyISAM存储引擎执行SQL语句自动加锁。查询语句给涉及的所有表加读锁,更新操作(UPDATE、DELETE、INSERT等)给涉及的表加写锁,这个过程并不需要用户干预;
- 乐观锁其实是一种思想;
- 悲观锁用的就是数据库的行锁;
死锁
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
常见的解决死锁的方法
- 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会;
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
- 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
如果业务处理不好可以用分布式事务锁或者使用乐观锁