Mysql数据库锁相关介绍
1.锁的类型
首先对mysql锁进行划分:
- 按照锁的粒度划分:行锁、表锁、页锁
- 按照锁的使用方式划分:共 享锁、排它锁(悲观锁的一种实现)
- 还有两种思想上的锁:悲观锁、乐观锁。
- InnoDB中有几种行级锁类型:Record Lock、Gap Lock、Next-key Lock
- Record Lock:在索引记录上加锁
- Gap Lock:间隙锁
- Next-key Lock:Record Lock+Gap Lock
1.1 行锁
行级锁是Mysql中锁定粒度最细的一种锁, 同时也只存在于InnoDB中,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能会出现死锁的情况。 行级锁按照使用方式分为共享锁和排他锁。
行锁又分为共享锁和排它锁
1.1.1 共享锁用法(S锁 读锁):
若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
select ... lock in share mode;
共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。
1.1.2 排它锁用法(X 锁 写锁):
若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。
为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:
- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的 IS 锁。
- 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的 IX 锁。
锁模式的兼容情况:
1.1.3 意向锁相关
-
意向锁是 InnoDB 自动加的, 不需用户干预。
-
对于 UPDATE、 DELETE 和 INSERT 语句, InnoDB会自动给涉及数据集加排他锁(X);
-
对于普通 SELECT 语句,InnoDB 不会加任何锁;
事务可以通过以下语句显式给记录集加共享锁或排他锁: -
- 共享锁(S):
SELECT * FROM table_name WHERE ...LOCK IN SHARE MODE
。 其他 session 仍然可以查询记录,并也可以对该记录加share mode
的共享锁。但是如果当前事务需要对该记录进行更新操作,则很有可能造成死锁。 - 排他锁(X):
SELECT * FROM table_name WHERE ... FOR UPDATE
。其他 session 可以查询该记录,但是不能对该记录加共享锁或排他锁,而是等待获得锁
- 共享锁(S):
- 隐式锁定:
InnoDB在事务执行过程中,使用两阶段锁协议:
- 随时都可以执行锁定,InnoDB会根据隔离级别在需要的时候自动加锁;
- 锁只有在执行commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。
-
显式锁定 :
-
https://zhuanlan.zhihu.com/p/29150809 转自该文章 较详细
select ... lock in share mode //共享锁
select ... for update //排他锁
select lock in share mode
: in share mode 子句的作用就是将查找到的数据加上一个 share 锁,这个就是表示其他的事务只能对这些数据进行简单的select 操作,并不能够进行 DML 操作。select *** lock in share mode
使用场景:为了确保自己查到的数据没有被其他的事务正在修改,也就是说确保查到的数据是最新的数据,并且不允许其他人来修改数据。但是自己不一定能够修改数据,因为有可能其他的事务也对这些数据 使用了 in share mode
的方式上了 S 锁。
select *** for update
的使用场景:为了让自己查到的数据确保是最新数据,并且查到后的数据只允许自己来修改的时候,需要用到 for update 子句。
for update 和 lock in share mode 的区别:
- 前一个上的是排他锁(X 锁),一旦一个事务获取了这个锁,其他的事务是没法在这些数据上执行 for update ;
- 后一个是共享锁,多个事务可以同时的对相同数据执行 lock in share mode。
1.1.4 InnoDB 行锁实现方式
- InnoDB 行锁是通过给索引上的索引项加锁来实现的,这一点 MySQL 与 Oracle 不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB 这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁!
- 不论是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。
- 只有执行计划真正使用了索引,才能使用行锁:即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了检查 SQL 的执行计划(可以通过 explain 检查 SQL 的执行计划),以确认是否真正使用了索引。(更多阅读:MySQL索引总结)
- 由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然多个session是访问不同行的记录, 但是如果是使用相同的索引键, 是会出现锁冲突的(后使用这些索引的session需要等待先使用索引的session释放锁后,才能获取锁)。 应用设计的时候要注意这一点。
1.2 表锁
表级锁是mysql锁中粒度最大的一种锁,表示当前的操作对整张表加锁,资源开销比行锁少,不会出现死锁的情况,但是发生锁冲突的概率很大。被大部分的mysql引擎支持,MyISAM和InnoDB都支持表级锁,但是InnoDB默认的是行级锁。
共享锁用法:
LOCK TABLE table_name [ AS alias_name ] READ
排它锁用法:
LOCK TABLE table_name [AS alias_name][ LOW_PRIORITY ] WRITE
解锁用法:
unlock tables;
1.3 页锁
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁
1.4 悲观锁/乐观锁
1.4.1 悲观锁
在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作对某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
悲观锁的实现流程:
在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)
- 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定。
- 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
- 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
在mysql/InnoDB中使用悲观锁
首先我们得关闭mysql中的 autocommit 属性,因为 mysql 默认使用自动提交模式,也就是说当我们进行一个sql操作的时候,mysql会将这个操作当做一个事务并且自动提交这个操作。
1.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
2.查询出商品信息
select ... for update;
4.提交事务
commit;/commit work;
通过下面的例子来说明:
1.当你手动加上排它锁,但是并没有关闭mysql中的autocommit。
SESSION1:
mysql> select * from user for update;
+----+------+--------+
| id | name | psword |
+----+------+--------+
| 1 | a | 1 |
| 2 | b | 2 |
| 3 | c | 3 |
+----+------+--------+
3 rows in set
这里他会一直提示Unknown
mysql> update user set name=aa where id=1;
1054 - Unknown column 'aa' in 'field list'
mysql> insert into user values(4,d,4);
1054 - Unknown column 'd' in 'field list'
2.正常流程
窗口1:
mysql> set autocommit=0;
Query OK, 0 rows affected
我这里锁的是表
mysql> select * from user for update;
+----+-------+
| id | price |
+----+-------+
| 1 | 500 |
| 2 | 800 |
+----+-------+
2 rows in set
窗口2:
mysql> update user set price=price-100 where id=1;
执行上面操作的时候,会显示等待状态,一直到窗口1执行commit提交事务才会出现下面的显示结果
Database changed
Rows matched: 1 Changed: 1 Warnings: 0
窗口1:
mysql> commit;
Query OK, 0 rows affected
mysql> select * from user;
+----+-------+
| id | price |
+----+-------+
| 1 | 400 |
| 2 | 800 |
+----+-------+
2 rows in set
上面的例子展示了排它锁的原理:一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁或者进行数据的操作。
1.4.2 乐观锁
乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。
数据版本 : 为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。
乐观锁优劣势 :
- 优势 : 任何锁和死锁. 效率快
- 劣势 : 如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。
1.5 Record Lock、Gap Lock、Next-key Lock锁
1.5.1 Record Lock
单条索引上加锁,record lock 永远锁的是索引,而非数据本身,如果innodb表中没有索引,那么会自动创建一个隐藏的聚集索引,锁住的就是这个聚集索引。所以说当一条sql没有走任何索引时,那么将会在每一条聚集索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。
1.5.2 Gap Lock
间隙锁,是在索引的间隙之间加上锁,这是为什么Repeatable Read隔离级别下能防止幻读的主要原因。
有关幻读的详细解释
InnoDB的间隙锁:
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)。
很显然,在使用范围条件检索并锁定记录时,InnoDB这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尤其是并发插入比较多的应用,我们要尽量优化业务逻辑,尽量使用相等条件来访问更新数据,避免使用范围条件。
什么叫间隙锁 , 直接通过例子说明:
mysql> select * from product_copy;
+----+--------+-------+-----+
| id | name | price | num |
+----+--------+-------+-----+
| 1 | 伊利 | 68 | 1 |
| 2 | 蒙牛 | 88 | 1 |
| 6 | tom | 2788 | 3 |
| 10 | 优衣库 | 488 | 4 |
+----+--------+-------+-----+
其中id为主键 num为普通索引
窗口A:
mysql> select * from product_copy where num=3 for update;
+----+------+-------+-----+
| id | name | price | num |
+----+------+-------+-----+
| 6 | tom | 2788 | 3 |
+----+------+-------+-----+
1 row in set
窗口B:
mysql> insert into product_copy values(5,'kris',1888,2);
这里会等待 直到窗口A commit才会显示下面结果
Query OK, 1 row affected
但是下面是不需要等待的
mysql> update product_copy set price=price+100 where num=1;
Query OK, 2 rows affected
Rows matched: 2 Changed: 2 Warnings: 0
mysql> insert into product_copy values(5,'kris',1888,5);
Query OK, 1 row affected
通过上面的例子可以看出Gap 锁的作用是在1,3的间隙之间加上了锁。而且并不是锁住了表,我更新num=1,5的数据是可以的.可以看出锁住的范围是(1,3]U[3,4)。
为什么说gap锁是RR隔离级别下防止幻读的主要原因?
解决幻读的方式很简单,就是需要当事务进行当前读的时候,保证其他事务不可以在满足当前读条件的范围内进行数据操作。
根据索引的有序性,我们可以从上面的例子推断出满足where条件的数据,只能插入在num=(1,3]U[3,4)两个区间里面,只要我们将这两个区间锁住,那么就不会发生幻读。
扩展:
-
主键索引/唯一索引+当前读会加上Gap锁吗?
窗口A: mysql> select * from product_copy where id=6 for update; +----+------+-------+-----+ | id | name | price | num | +----+------+-------+-----+ | 6 | tom | 2788 | 3 | +----+------+-------+-----+ 窗口B:并不会发生等待 mysql> insert into product_copy values(5,'kris',1888,3); Query OK, 1 row affected
例子说明的其实就是行锁的原因,我只将id=6的行数据锁住了,用Gap锁的原理来解释的话:因为主键索引和唯一索引的值只有一个,所以满足检索条件的只有一行,故并不会出现幻读,所以并不会加上Gap锁。
-
通过范围查询是否会加上Gap锁
窗口A: mysql> select * from product_copy where num>3 for update; +----+--------+-------+-----+ | id | name | price | num | +----+--------+-------+-----+ | 10 | 优衣库 | 488 | 4 | +----+--------+-------+-----+ 窗口B:会等待 mysql> insert into product_copy values(11,'kris',1888,5); Query OK, 1 row affected 不会等待 mysql> insert into product_copy values(3,'kris',1888,2); Query OK, 1 row affected
其实原因都是一样,只要满足检索条件的都会加上Gap锁
检索条件并不存在的当前读会加上Gap吗?
窗口A:
mysql> select * from product_copy where num=5 for update;
Empty set
窗口B:6 和 4都会等待
mysql> insert into product_copy values(11,'kris',1888,6);
Query OK, 1 row affected
mysql> insert into product_copy values(11,'kris',1888,4);
Query OK, 1 row affected
原因一样会锁住(4,5]U[5,n)的区间
范围查询
这里就会有点不一样
窗口A:
mysql> select * from product_copy where num>6 for update;
Empty set
窗口B:8 和 4 都会锁住
mysql> insert into product_copy values(11,'kris',1888,4);
Query OK, 1 row affected
mysql> insert into product_copy values(11,'kris',1888,8);
Query OK, 1 row affected
上面的2例子看出当你查询并不存在的数据的时候,mysql会将有可能出现区间全部锁住。
1.5.3 Next-Key Lock
这个锁机制其实就是前面两个锁相结合的机制,既锁住记录本身还锁住索引之间的间隙。