首先对mysql 锁进行划分:

  1. 按照锁的粒度划分:行锁,表锁,页锁
  2. 按照锁的使用方式划分:共享锁、排它锁(悲观锁的一种实现)
  3. 两种思想上的锁:悲观锁、乐观锁
  4. InnoDB中有几种行级锁类型:Recode Lock、Gap Lock、Next-key Lock
  5. Recode Lock:在索引记录上加锁
  6. Gap Lock:间隙锁
  7. Next-key Lock:Recode Lock + Gap Lock

1.行锁

行级锁是Mysql中粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。有可能出现死锁。

共享锁用法(S锁 读锁)

若事务T对数据对象 A 加上 S 锁,则事务T 可以读A 但不能更改 A,其他事务只能再对 A 加 S 锁,而不能加 X 锁,直到 T 释放 A 上的 S 锁。在 T 释放 A 上的 S 锁之前不能对 A 做任何修改

  1. select ... lock in share mode;

共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有

排它锁用法(X锁 写锁)

若事务 T 对数据对象 A 加上 X 锁,事务 T 可以读 A 也可以修改 A,其他事务不能再对 A 加任何锁,直到 T 释放 A 上的锁。保证了其他事务在 T 释放 A 上的锁之前不能再读取和修改 A

  1. select ... for update

排它锁,也称独占锁,一个锁在某一个时刻只能被一个线程占有,其他线程必须等待锁被释放后才能获取到锁

2.表锁

表级锁是mysql 锁中粒度最大的一种锁,表示当前操作对整张表加锁,资源开销比行锁少,不会出现死锁情况,但是发送锁冲突的概率很大。MyISAM 和 InnoDB 都支持表级锁,InnoDB 默认是行级锁。

共享锁用法

  1. LOCK TABLE table_name [ AS alias_name ] READ

排它锁用法

  1. LOCK TABLE table_name [ AS alias_name][ LOW_PRIORITY ] WRITE

解锁用法

  1. unlock tables

3.页锁

页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但是冲突多,行级锁冲突少,但速度慢。所以取折中的页级,一次锁定相邻的一组记录。DBD支持页级锁

4.乐观锁和悲观锁

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。

乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。像memcache、hibernate、tair等都有类似的概念

4.1悲观锁

是一种并发控制的方法。他可以组织一个事务以影响其他用户的方式来修改数据。悲观锁并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。
悲观锁的实现,往往依靠数据库提供的锁机制。

4.1.1悲观锁流程

对任意数据修改前,先尝试加排它锁,加锁失败,说明记录正在被修改。抛出异常
成功加锁,可以对记录进行修改

4.1.2在mysql/InnoDB中使用悲观锁

关闭myslq的 autocommit 属性。

  1. 1.开始事务
  2. begin;/begin work;/start transaction(三者选一就可以)
  3. 2.查询出商品信息
  4. select ... for uodate;
  5. 3.提交事务
  6. cmmit;/commit work;

4.1.3悲观锁的优点和不足

效率方面,产生额外的开销,并增加了死锁的机会。降低了并发性。

4.2乐观锁

假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理个字影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会回滚。

乐观锁一般情况下不会造成冲突,所以在数据进行提交更新时,才会正式对数据的冲突进行检查
乐观锁不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

4.2.1乐观锁的优点和不足

假如两个事务都读取了数据库的某一行,经过修改以后写会数据库,这时就遇到了问题。

5.1InnoDB锁的特征

  1. 在不通过索引条件查询的时候,InnoDB使用的是表锁
  2. 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以如果使用相同的索引键,是会出现锁冲突的。
  3. 当表有多个索引,不同事务可以使用不同的索引锁定不同的行,无论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据进行加锁
  4. MySQL认为全表扫描效率更高,他就不会使用索引。这种情况下InnoDB将使用表锁。

    1.通过非索引项检索数据,加表锁!

    ```php 窗口1: mysql> set autocommit=0; Query OK, 0 rows affected

mysql> select * from product where num=1 and price=68 for update; +——+———+———-+——-+ | id | name | price | num | +——+———+———-+——-+ | 1 | 伊利 | 68 | 1 | +——+———+———-+——-+

窗口2: mysql> update product set price=price+100 where num=1 and price=88; 这里会发生等待,直到窗口1 commit 显示下面结果 Query OK, 1 row affected Rows matched: 1 Changed: 1 Warnings: 0 mysql> select * from product; +——+—————+———-+——-+ | id | name | price | num | +——+—————+———-+——-+ | 1 | 伊利 | 68 | 1 | | 2 | 蒙牛 | 188 | 1 | +——+—————+———-+——-+ 123456789101112131415161718192021222324123456789101112131415161718192021222324

  1. <a name="T7UNB"></a>
  2. #### 2.使用相同索引值但是不同行引发的冲突
  3. ```php
  4. 这里的num属性 加上了普通索引,price属性并没有索引
  5. 窗口1:
  6. mysql> set autocommit=0;
  7. Query OK, 0 rows affected
  8. mysql> select * from product where num=1 and price=68 for update;
  9. +----+------+-------+-----+
  10. | id | name | price | num |
  11. +----+------+-------+-----+
  12. | 1 | 伊利 | 68 | 1 |
  13. +----+------+-------+-----+
  14. 窗口2:
  15. mysql> update product set price=price+100 where num=1 and price=88;
  16. 这里会发生等待,直到窗口1 commit 显示下面结果
  17. Query OK, 1 row affected
  18. Rows matched: 1 Changed: 1 Warnings: 0
  19. mysql> select * from product;
  20. +----+----------+-------+-----+
  21. | id | name | price | num |
  22. +----+----------+-------+-----+
  23. | 1 | 伊利 | 68 | 1 |
  24. | 2 | 蒙牛 | 188 | 1 |
  25. +----+----------+-------+-----+
  26. 123456789101112131415161718192021222324123456789101112131415161718192021222324

3.当使用索引检索数据时不同事务可以操作不同行数据

  1. 锁一行数据,DML操作其他行并没有影响
  2. 窗口1
  3. mysql> select * from user where id=1 for update;
  4. +----+-------+
  5. | id | price |
  6. +----+-------+
  7. | 1 | 400 |
  8. +----+-------+
  9. 窗口2
  10. mysql> update user set price=price+100 where id=2;
  11. 无需等待窗口1 commit
  12. Database changed
  13. Rows matched: 1 Changed: 1 Warnings: 0
  14. 12345678910111213141234567891011121314

6.Record Lock、Gap Lock、Next-key Lock锁

6.1Record Lock

单条索引上加锁,record lock永远锁的是索引,如果innodb表中没有索引,那么会自动创建一个隐藏的聚集索引。

6.2Gap Lock

间隙锁,是在索引的间隙之间加上锁。

6.2.1什么叫间隙锁

  1. mysql> select * from product_copy;
  2. +----+--------+-------+-----+
  3. | id | name | price | num |
  4. +----+--------+-------+-----+
  5. | 1 | 伊利 | 68 | 1 |
  6. | 2 | 蒙牛 | 88 | 1 |
  7. | 6 | tom | 2788 | 3 |
  8. | 10 | 优衣库 | 488 | 4 |
  9. +----+--------+-------+-----+
  10. 其中id为主键 num为普通索引
  11. 窗口A
  12. mysql> select * from product_copy where num=3 for update;
  13. +----+------+-------+-----+
  14. | id | name | price | num |
  15. +----+------+-------+-----+
  16. | 6 | tom | 2788 | 3 |
  17. +----+------+-------+-----+
  18. 1 row in set
  19. 窗口B
  20. mysql> insert into product_copy values(5,'kris',1888,2);
  21. 这里会等待 直到窗口A commit才会显示下面结果
  22. Query OK, 1 row affected
  23. 但是下面是不需要等待的
  24. mysql> update product_copy set price=price+100 where num=1;
  25. Query OK, 2 rows affected
  26. Rows matched: 2 Changed: 2 Warnings: 0
  27. mysql> insert into product_copy values(5,'kris',1888,5);
  28. Query OK, 1 row affected
  29. 123456789101112131415161718192021222324252627282930123456789101112131415161718192021222324252627282930

6.2.2为什么说gap锁是RR隔离级别下防止幻读的主要原因

解决幻读方式很简单,就是需要当事务进行当前读的时候,保证其他事务不可以在满足当前读条件的范围内进行数据操作。
根据索引的有序性,我们可以从上面的例子推断出满足where条件的数据,只能插入在num=(1,3]U[3,4)两个区间里面,只要我们将这两个区间锁住,那么就不会发生幻读。

6.2.3主键索引/唯一索引 + 当前读会加上Gap锁吗?

直接通过例子来说明

  1. 窗口A
  2. mysql> select * from product_copy where id=6 for update;
  3. +----+------+-------+-----+
  4. | id | name | price | num |
  5. +----+------+-------+-----+
  6. | 6 | tom | 2788 | 3 |
  7. +----+------+-------+-----+
  8. 窗口B:并不会发生等待
  9. mysql> insert into product_copy values(5,'kris',1888,3);
  10. Query OK, 1 row affected
  11. 12345678910111234567891011

6.2.4通过范围查询是否会加上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
12345678910111213141234567891011121314

其实原因都是一样,只要满足检索条件的都会加上Gap锁

6.2.5 检索条件并不存在的当前读会加上Gap吗?

6.2.5.1.等值查询

窗口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
1234567891012345678910

原因一样会锁住(4,5]U[5,n)的区间

6.2.5.2.范围查询
这里就会有点不一样

窗口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
123456789123456789

上面的2例子看出当你查询并不存在的数据的时候,mysql会将有可能出现区间全部锁住。

6.3Next-Key Lock

这个锁机制其实就是前面两个锁相结合的机制,既锁住记录本身还锁住索引之间的间隙。

7.死锁的原理和分析

7.1MVCC

MySQL InnoDB 存储引擎,实现的是基于多版本并发控制协议——MVCC(multi Version Concurrency Control)
MVCC最大的好处:读不加锁,读写不冲突。在读多写少的OLTP 应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持MVCC。

7.2.2PL:Two-Phase Locking

2PL说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交

transaction mysql
begin; 加锁阶段
insert into 加insert对应的锁
update table 加update对应的锁
delete from 加delete对应的锁
commit 解锁阶段

| 将insert、update、delete的锁全部解开 |

上面的例子可以看出2PL就是将加锁、解锁分为两个阶段,并且互相不干扰。加锁阶段只加锁,解锁阶段只解锁。


7.3为什么会发生死锁

MyISAM是不会产生死锁的,因为MyISAM总是一次性获得所需的全部锁,要么全部满足,要么全部等待。而InnoDB中,所是逐步获得的,就造成了死锁的可能。

在InnoDB中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定非主键索引,再锁定相关的主键索引。

当两个事务同时执行,一个锁住了主键索引,在等待其他相关索引。另一个锁定了非主键索引,在等待主键索引。这样就会发生死锁。

7.3.1.两个session的两条语句

MySQL的锁机制和加锁原理 - 图1
这种情况很好理解,首先session1获得 id=1的锁 session2获得id=5的锁,然后session想要获取id=5的锁 等待,session2想要获取id=1的锁 ,也等待!

7.3.2.两个session的一条语句

MySQL的锁机制和加锁原理 - 图2
这种情况需要我们了解数据的索引的检索顺序原理简单说下:普通索引上面保存了主键索引,当我们使用普通索引检索数据时,如果所需的信息不够,那么会继续遍历主键索引

假设默认情况是RR隔离级别,针对session 1 从 name 索引出发,检索到的是(hdc,1) (hdc,6) 不仅会加name 索引上的记录 X 锁,而且会加聚簇索引上的记录X 锁,加锁顺序为先[1,hdc,100],后[6,hdc,10] 这个顺序是因为B+数结构的有序性。
而Session 2,从pubtime索引出发,[10,6],[100,1]均满足过滤条件,同样也会加聚簇索引上的记录X锁,加锁顺序为[6,hdc,10],后[1,hdc,100]。发现没有,跟Session 1的加锁顺序正好相反,如果两个Session恰好都持有了第一把锁,请求加第二把锁,死锁就发生了。

避免死锁,这里介绍常见的三种

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