关于数据库锁,是一个很重要的知识点;mysql
很多人在开发的时候,应该不多会注意到这些锁的问题,也不多会给程序加锁(除了库存这些对数量准确性要求极高的状况下);程序员
通常也就听过常说的乐观锁和悲观锁,了解过基本的含义以后就没了,没有去实际的操做过,本文将简单的整理一下数据库锁的知识,但愿对你们有所帮助;sql
本文参考文章:数据库的两大神器数据库
在MySQL中锁看起来是很复杂的,由于有一大堆的东西和名词:排它锁,共享锁,表锁,页锁,间隙锁,意向排它锁,意向共享锁,行锁,读锁,写锁,乐观锁,悲观锁,死锁。这些名词有的博客又直接写锁的英文的简写--->X锁,S锁,IS锁,IX锁,MMVC等等之类。锁的相关知识又跟存储引擎,索引,事务的隔离级别都是关联的;并发
以上的一大堆锁可能不少人都只是知道一些概念,可是咱们的程序在通常状况下仍是能够跑得好好的。由于这些锁数据库隐式帮咱们加了:post
UPDATE、DELETE、INSERT
语句,InnoDB会自动给涉及数据集加排他锁(X),也就是咱们常说的写锁;SELECT
前,会自动给涉及的全部表加读锁,在执行更新操做(UPDATE、DELETE、INSERT
等)前,会自动给涉及的表加写锁,这个过程并不须要用户干预从锁的粒度咱们能够分为两大类,它们各自的特色以下:学习
一样,不一样的存储引擎支持的锁的力度也不同:测试
表锁也分为两种模式:spa
总结获得:线程
咱们使用MySQL通常是使用的InnoDB引擎,上面也提到了InnoDB和MyISAM的一些区别:
InnoDB实现了如下两种类型的行锁:
为了容许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:
意向锁也是数据库隐式帮咱们作了,不须要程序员操心!
上面咱们提到了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;
结果:
悲观锁是从数据库层面加锁。老是假设最坏的状况,每次去拿数据的时候都认为别人会修改,因此每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它释放锁;
上面其实关于行锁和表锁的测试那里咱们使用的排他锁也就是悲观锁;
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,因此若是其余事务进行了更新,这条语句是执行不成功的;
当咱们用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,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数据锁以及事务的一些知识点,下面咱们来总结一下;
不一样的存储引擎支持的锁的力度也不同:
数据库锁从锁的粒度咱们能够分为两大类,它们各自的特色以下::
悲观锁:老是假设最好的状况,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据;
乐观锁:老是假设最好的状况,每次去拿数据的时候都认为别人不会修改,因此不会上锁,可是在更新的时候会判断一下在此期间别人有没有去更新这个数据;
最后说一下,本文的参考文章:数据库的两大神器
你们能够去看一下原文,本人也是小菜鸡一枚,说的有问题还望你们指出来;
你们共同窗习,一块儿进步。