前言
关于数据库锁,是一个很重要的知识点;mysql
很多人在开发的时候,应该不多会注意到这些锁的问题,也不多会给程序加锁(除了库存这些对数量准确性要求极高的状况下);程序员
通常也就听过常说的乐观锁和悲观锁,了解过基本的含义以后就没了,没有去实际的操做过,本文将简单的整理一下数据库锁的知识,但愿对你们有所帮助;sql
引入
本文参考文章:<a href="https://juejin.im/post/5b55b842f265da0f9e589e79?tdsourcetag=s_pctim_aiomsg#heading-11">数据库的两大神器</a>数据库
数据库锁
简介
在MySQL中锁看起来是很复杂的,由于有一大堆的东西和名词:排它锁,共享锁,表锁,页锁,间隙锁,意向排它锁,意向共享锁,行锁,读锁,写锁,乐观锁,悲观锁,死锁。这些名词有的博客又直接写锁的英文的简写--->X锁,S锁,IS锁,IX锁,MMVC等等之类。锁的相关知识又跟存储引擎,索引,事务的隔离级别都是关联的;并发
以上的一大堆锁可能不少人都只是知道一些概念,可是咱们的程序在通常状况下仍是能够跑得好好的。由于这些锁数据库隐式帮咱们加了:post
- 对于
UPDATE、DELETE、INSERT
语句,InnoDB会自动给涉及数据集加排他锁(X),也就是咱们常说的写锁; - MyISAM在执行查询语句
SELECT
前,会自动给涉及的全部表加读锁,在执行更新操做(UPDATE、DELETE、INSERT
等)前,会自动给涉及的表加写锁,这个过程并不须要用户干预
表锁和行锁
从锁的粒度咱们能够分为两大类,它们各自的特色以下:学习
- 表锁:开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突几率高,并发度低;
- 行锁:开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突的几率低,并发度高;
一样,不一样的存储引擎支持的锁的力度也不同:测试
- InnoDB:表锁行锁都支持(InnoDB的行锁是基于索引的 ,稍后会演示);
- MyISAM:只支持表锁;
表锁
表锁也分为两种模式:spa
- 表读锁(Table Read Lock)
- 表写锁(Table Write Lock)
- 读读不阻塞:当前用户读取数据,其余用户也在读取数据,不会加锁;
- 读写阻塞:当前用户在读取数据的时候,其余用户不能修改当前用户读的数据;
- 写写阻塞:当前用户在修改数据,其余用户不能修改当前用户正在修改的数据;
总结获得:线程
- 读读不阻塞,读写阻塞,写写阻塞 ;
- 读锁和写锁是互斥的,读写操做是串行 ;
- 在mysql里边,写锁是优先于读锁的 ;
行锁
咱们使用MySQL通常是使用的InnoDB引擎,上面也提到了InnoDB和MyISAM的一些区别:
- InnoDB行锁表锁都支持,MyISAM只支持表锁;
- InnoDB支持事务,MyISAM不支持;
InnoDB实现了如下两种类型的行锁:
- 共享锁(s锁):容许一个事务去读一行,会阻止其余事务获取相同数据集的排他锁(读取数据的时候不容许修改)
- 也被称为读锁:读锁是共享的,多个线程能够同时读取统一数据集,可是不容许其余线程进行修改(也就是不容许其余事务获取相同数据集的排他锁);
- 排他锁(x锁):容许获取排他锁去作更新操做,阻止其余事务获取相通数据的共享锁和排他锁(一个事务修改数据的时候,阻止其余事务对相同数据集作更新或者查询操做);
- 也被称为写锁:写锁是排他的,写锁会阻塞其余的写锁和读锁;
意向锁
为了容许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:
- 意向共享锁(IS):事务打算给数据行加共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁;
- 意向排他锁(IX):事务打算给数据行加排他锁,事务再给一个数据行加排他锁前必须先取得该表的IX锁;
意向锁也是数据库隐式帮咱们作了,不须要程序员操心!
表锁行锁测试
准备
上面咱们提到了InnoDB支持行锁,可是是基于索引的状况,下面咱们来实际的看一下:
-
首先咱们用客户端链接上MySQL数据库,为了测试锁的效果,咱们须要打开两个或者两个以上的客户端(我打开了两个)而后建立一个数据库;
CREATE DATABASE test CHARACTER SET utf8;
-
而后咱们须要创建一个表:
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `age` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB CHARSET=utf8;
-
咱们简单的建了一个user表,表中有三个字段,其中id为自增主键,你们都知道主键是自带索引的,也就是聚簇索引(主键索引),其余的字段都是不带索引的。
-
mysql> show tables; +----------------+ | Tables_in_test | +----------------+ | user | +----------------+ 1 row in set (0.01 sec)
-
如今咱们简单的往里面添加几条数据:
INSERT INTO `user`(username,age) VALUES ('tom',23),('joey',22),('James',21),('William',20),('David',24);
-
mysql> select * from user; +----+----------+-----+ | id | username | age | +----+----------+-----+ | 1 | tom | 23 | | 2 | joey | 22 | | 3 | James | 21 | | 4 | William | 20 | | 5 | David | 24 | +----+----------+-----+ 5 rows in set (0.00 sec)
-
测试
好的,如今前提都已经弄好了,咱们能够开始测试了:
咱们知道MySQL的事务是自动提交的,为了测试,咱们须要把事务的自动提交关闭;
mysql> set autocommit = 0; Query OK, 0 rows affected (0.01 sec)
如今咱们来查看一下MySQL的事务提交状态:
mysql> show VARIABLES like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | OFF | +---------------+-------+ 1 row in set, 1 warning (0.04 sec)
从上面能够看出,咱们把事务的自动提交已经关闭了,下面咱们开始测试(打开的窗口都须要关闭事务的自动提交);
行锁测试
首先,我打开了两个窗口,分别为A和B,如今,咱们两个窗口的状态都已经调整完毕(关闭事务自动提交)。咱们在A窗口,输入如下语句:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id = 1 for update; +----+----------+-----+ | id | username | age | +----+----------+-----+ | 1 | tom | 23 | +----+----------+-----+ 1 row in set (0.02 sec) mysql>
很明显,以上语句中,打开了事务,而后执行了一条SQL语句,在select 语句后边加了 for update
至关于加了排它锁(写锁),加了写锁之后,其余的事务就不能对它修改了!须要等待当前事务修改完提交以后才能够修改;
如今咱们在窗口B执行相同的操做:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id = 1 for update; -
注意到了吗,窗口B并无数据出现,由于窗口A执行的时候加了排他锁,可是窗口A并无提交事务,因此锁也没有获得释放,如今咱们在窗口A提交事务:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id = 1 for update; +----+----------+-----+ | id | username | age | +----+----------+-----+ | 1 | tom | 23 | +----+----------+-----+ 1 row in set (0.02 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec)
同时,窗口B出现了如下状况:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id = 1 for update; +----+----------+-----+ | id | username | age | +----+----------+-----+ | 1 | tom | 23 | +----+----------+-----+ 1 row in set (4.34 sec) mysql>
没错,由于窗口A提交了事务,释放的排他锁,因此窗口B获取到了数据并从新为该数据添加了排他锁,因此此时你在A窗口在重复以前操做的时候仍是会阻塞,由于窗口B没有提交事务,也就是没有释放排他锁;
如今,咱们在窗口A执行如下语句:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id = 2 for update; +----+----------+-----+ | id | username | age | +----+----------+-----+ | 2 | joey | 22 | +----+----------+-----+ 1 row in set (0.00 sec) mysql>
有的同窗可能会说,不对啊,我窗口B尚未提交事务,释放排他锁啊。
可是,你们注意看个人SQL语句,此次查的是id = 2的数据;
这是InnoDB的一大特性,我上面说了,InnoDB的行锁是基于索引的 ,由于此时咱们的条件是基于主键的,而主键是自带索引的,因此加的是行锁,这个时候窗口A锁的是id = 2的这条数据,窗口B锁的是id = 1的这条数据,他们互不干扰;
表锁测试
如今,咱们再来测试一下,没有索引,走表锁的状况;
咱们上面有提过,InnoDB的行锁是基于索引,没有索引的话,锁住的就是整张表:
咱们在窗口A输入执行如下操做:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where age = 20 for update; +----+----------+-----+ | id | username | age | +----+----------+-----+ | 4 | William | 20 | +----+----------+-----+ 1 row in set (0.04 sec) mysql>
你们注意,此次的条件是使用的age,可是age是没有索引的,因此咱们在B窗口执行相同的操做:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where age = 20 for update; -
很清楚的能看到,窗口B处于阻塞状态,咱们换个条件继续执行:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where age = 22 for update; -
一样,尽管查询的数据换成了age = 22,可是仍是会阻塞住,也就证实看不是锁的行;
咱们再来试试换一个列做为条件:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id = 1 for update; -
一样的结果,咱们如今在A窗口提交事务,再来看一下B窗口:
A:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where age = 20 for update; +----+----------+-----+ | id | username | age | +----+----------+-----+ | 4 | William | 20 | +----+----------+-----+ 1 row in set (0.04 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec)
B:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id = 1 for update; +----+----------+-----+ | id | username | age | +----+----------+-----+ | 1 | tom | 23 | +----+----------+-----+ 1 row in set (0.00 sec) mysql>
当窗口A提交事务后,也就释放了锁,这个时候窗口B获取到了锁,获得了数据,并锁住了id = 1的这一行数据;
联合索引测试
关于联合索引中,须要注意的一点就是最左匹配原则 ,说白了就是查询是否走了索引,若是走了索引,一样加的仍是行锁,不然锁的仍是表,下面咱们来看一下。首先,咱们须要把表中的username和age建一个联合索引:
mysql> create index index_username_age on user(username,age); Query OK, 0 rows affected (0.04 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> show index from user; +-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | +-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | user | 0 | PRIMARY | 1 | id | A | 5 | NULL | NULL | | BTREE | | | | user | 1 | index_username_age | 1 | username | A | 4 | NULL | NULL | | BTREE | | | | user | 1 | index_username_age | 2 | age | A | 5 | NULL | NULL | | BTREE | | | +-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ 3 rows in set (0.00 sec) mysql>
上面能够看出,咱们创建联合索引成功,下面咱们开始测试,首先,咱们在窗口A执行如下操做:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where username='tom' and age = 20 for update; +----+----------+-----+ | id | username | age | +----+----------+-----+ | 1 | tom | 20 | +----+----------+-----+ 1 row in set (0.00 sec) mysql>
能够看出,和咱们以前的操做没啥两样,一样是打开事务进行操做,如今咱们在窗口B执行如下操做:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where username='tom' and age = 20 for update; -
很清楚的看到B窗口被锁住了,可是咱们如今肯定的是加的锁,并不知道是行锁仍是表锁,不要紧,咱们换个条件:
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where username='joey' and age = 22 for update; +----+----------+-----+ | id | username | age | +----+----------+-----+ | 2 | joey | 22 | +----+----------+-----+ 1 row in set (0.00 sec) mysql>
这样,咱们很清楚的就能看到走的是行锁了。
只不过你们要注意联合索引的命中规则也就是最左匹配原则,咱们能够试一试单独使用username做为条件看看走的什么锁,也能够看看单独使用age走的什么锁,这里就再也不演示了,你们能够自行的尝试。
总结
前提:必须在事务里面
样例:select * from table where column = condition for update;
结果:
- 当coulmn是索引列的时候,也就是查询走的索引的时候,这个时候锁的就是行(行锁);
- 当coulmn不是索引的时候,也就是查询没走索引的时候,这个时候锁的就是整个表(表锁);
悲观锁
含义
悲观锁是从数据库层面加锁。老是假设最坏的状况,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它释放锁;
例子
上面其实关于行锁和表锁的测试那里咱们使用的排他锁也就是悲观锁;
select * from table where xxx for update
在上面咱们举的例子够多了,这里再也不多说;
乐观锁
含义
老是假设最好的状况,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据;
例子
表中有一个版本字段,第一次读的时候,获取到这个字段。处理完业务逻辑开始更新的时候,须要再次查看该字段的值是否和第一次的同样。若是同样就更新,反之拒绝。之因此叫乐观,由于这个模式没有从数据库加锁,等到更新的时候再判断是否能够更新。
update table set xxx where id = 1 and version = 1;
上面的语句就很清楚的说明了乐观锁,在对id = 1的数据进行更新的同时添加了version = 1的条件,version是当前事务开始以前查询出来的版本号,若是这个时候其余事务对id = 1的数据进行了更新会将version+1,因此若是其余事务进行了更新,这条语句是执行不成功的;
参考文章:http://www.javashuo.com/article/p-zglzmmfr-dp.html
间隙锁GAP
当咱们用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫作“间隙(GAP)”。InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
值得注意的是:间隙锁只会在Repeatable read
隔离级别下使用~
例子:假如emp表中只有101条记录,其empid的值分别是1,2,...,100,101
Select * from emp where empid > 100 for update;
上面是一个范围查询,InnoDB不只会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
InnoDB使用间隙锁的目的有两个:
- 为了防止幻读(上面也说了,
Repeatable read
隔离级别下再经过GAP锁便可避免了幻读) - 知足恢复和复制的须要
- MySQL的恢复机制要求:在一个事务未提交前,其余并发事务不能插入知足其锁定条件的任何记录,也就是不容许出现幻读 ;
死锁
并发的问题就少不了死锁,在MySQL中一样会存在死锁的问题。
但通常来讲MySQL经过回滚帮咱们解决了很多死锁的问题了,但死锁是没法彻底避免的,能够经过如下的经验参考,来尽量少遇到死锁:
- 1)以固定的顺序访问表和行。好比对两个job批量更新的情形,简单方法是对id列表先排序,后执行,这样就避免了交叉等待锁的情形;将两个事务的sql顺序调整为一致,也能避免死锁。
- 2)大事务拆小。大事务更倾向于死锁,若是业务容许,将大事务拆小。
- 3)在同一个事务中,尽量作到一次锁定所须要的全部资源,减小死锁几率。
- 4)下降隔离级别。若是业务容许,将隔离级别调低也是较好的选择,好比将隔离级别从RR调整为RC,能够避免掉不少由于gap锁形成的死锁。
- 5)为表添加合理的索引。能够看到若是不走索引将会为表的每一行记录添加上锁,死锁的几率大大增大。
总结
本文介绍了MySQL数据锁以及事务的一些知识点,下面咱们来总结一下;
不一样的存储引擎支持的锁的力度也不同:
- InnoDB:表锁行锁都支持(也作了演示);
- 当查询走的索引的时候,这个时候锁的就是行;
- 当查询没走的索引的时候,这个时候锁的就是表;
- MyISAM:只支持表锁;
数据库锁从锁的粒度咱们能够分为两大类,它们各自的特色以下::
- 表锁:开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突几率高,并发度低;
- 行锁:开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突的几率低,并发度高;
悲观锁:老是假设最好的状况,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据;
乐观锁:老是假设最好的状况,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据;
最后
最后说一下,本文的参考文章:<a href="https://juejin.im/post/5b55b842f265da0f9e589e79?tdsourcetag=s_pctim_aiomsg#heading-11">数据库的两大神器</a>
你们能够去看一下原文,本人也是小菜鸡一枚,说的有问题还望你们指出来;
你们共同窗习,一块儿进步。