专栏名称: 唤之
目录
相关文章推荐
程序猿  ·  问问DeepSeek,你和ChatGPT谁厉 ... ·  昨天  
OSC开源社区  ·  2024年AI编程工具的进化 ·  2 天前  
OSC开源社区  ·  敢自称Java版PyTorch,EasyAi ... ·  2 天前  
程序员的那些事  ·  感谢 ... ·  6 天前  
51好读  ›  专栏  ›  唤之

Mysql锁机制分析

唤之  · 掘金  · 程序员  · 2018-05-17 05:52

正文

前言
数据库锁定机制简单来说就是数据库为了保证数据的一致性而使各种共享资源在被并发访问访问变得有序所设计的一种规则;对于任何一种数据库来说都需要有相应的锁定机制,Mysql也不例外。

Mysql几种锁定机制类型
MySQL 各存储引擎使用了三种类型(级别)的锁定机制:行级锁定,页级锁定和表级锁定。
1.行级锁定
锁定对象的颗粒度很小,只对当前行进行锁定,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力;弊端就是获取锁释放锁更加频繁,系统消耗更大,同时行级锁定也最容易发生死锁;
行级锁定的主要是Innodb存储引擎和NDB Cluster存储引擎;

2.页级锁定
锁定颗粒度介于行级锁定与表级锁之间,每页有多行数据,并发处理能力以及获取锁定所需要的资源开销在两者之间;
页级锁定主要是BerkeleyDB 存储引擎;

3.表级锁定
一次会将整张表锁定,该锁定机制最大的特点是实现逻辑非常简单,带来的系统负面影响最小,而且可以避免死锁问题;弊端就是锁定资源争用的概率最高,并发处理能力最低;
使用表级锁定的主要是MyISAM,Memory,CSV等一些非事务性存储引擎。

本文重点介绍Innodb存储引擎使用的行级锁定;

两段锁协议(2PL)
两段锁协议规定所有的事务应遵守的规则:
1.在对任何数据进行读、写操作之前,首先要申请并获得对该数据的封锁;
2.在释放一个封锁之后,事务不再申请和获得其它任何封锁;

即事务的执行分为两个阶段:
第一阶段是获得封锁的阶段,称为扩展阶段;第二阶段是释放封锁的阶段,称为收缩阶段;

begin;
insert ...   加锁1
update ...   加锁2
commit;      事务提交时,释放锁1,锁2

如果在加锁2的时候,加锁不成功,则进入等待状态,直到加锁成功才继续执行;
如果有另外一个事务获取锁的时候顺序刚好相反,是有可能导致死锁的;为此有了一次性封锁法,要求事务必须一次性将所有要使用的数据全部加锁,否则就不能继续执行;

定理:若所有事务均遵守两段锁协议,则这些事务的所有交叉调度都是可串行化的(串行化很重要,尤其是在数据恢复和备份的时候);

行级锁定(悲观锁)
1.共享锁和排他锁
Innodb的行级锁定同样分为两种类型:共享锁和排他锁;
共享锁:当一个事务获得共享锁之后,它只可以进行读操作,所以共享锁也叫读锁,多个事务可以同时获得某一行数据的共享锁;
排他锁:而当一个事务获得一行数据的排他锁时,就可以对该行数据进行读和写操作,所以排他锁也叫写锁,排他锁与共享锁和其他的排他锁不兼容;

既然数据库提供了共享锁和排他锁,那具体用在什么地方:
1.1在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别,隔离级别就使用了锁机制;
1.2提供了相关的SQL,可以方便的在程序中使用;

2.事务隔离级别和锁的关系
数据库隔离级别:未提交读(Read uncommitted),已提交读(Read committed),可重复读(Repeatable read)和可串行化(Serializable);
未提交读(Read uncommitted):可能读取到其他会话中未提交事务修改的数据,会出现脏读(Dirty Read);
已提交读(Read committed):只能读取到已经提交的数据,会出现不可重复读(NonRepeatable Read);
可重复读(Repeatable read):InnoDB默认级别,不会出现不可重复读(NonRepeatable Read),但是会出现幻读(Phantom Read);
可串行化(Serializable):强制事务排序,使之不可能相互冲突,从而解决幻读问题,使用表级共享锁,读写相互都会阻塞;

常用的2种隔离级别是:已提交读(Read committed)和可重复读(Repeatable read);

3.已提交读(Read committed)
3.1准备测试表

CREATE TABLE `test_lock` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `type` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
 
mysql> insert into test_lock values(null,'zhaohui',1);
mysql> insert into test_lock values(null,'zhaohui2',2);

3.2查看和设置隔离级别

mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set
 
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected
 
mysql> SELECT @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+

3.3模拟多个事务交叉执行
Session1执行查询

mysql> begin;
Query OK, 0 rows affected
mysql> select * from test_lock where id=1;
+----+---------+------+
| id | name    | type |
+----+---------+------+
|  1 | zhaohui |    1 |
+----+---------+------+
1 row in set

Session2更新数据

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui_new' where id=1;
  
Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings: 0
mysql> commit;
Query OK, 0 rows affected

Session1执行查询

mysql> select * from test_lock where id=1;
+----+-------------+------+
| id | name        | type |
+----+-------------+------+
|  1 | zhaohui_new |    1 |
+----+-------------+------+
1 row in set
 
mysql> commit;
Query OK, 0 rows affected

Session1中出现了不可重复读(NonRepeatable Read),也就是在查询的时候没有锁住相关的数据,导致出现了不可重复读,但是写入、修改和删除数据还是加锁了,如下所示:

Session1更新数据

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui_new2' where id=1;
Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings: 0

Session2更新数据

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui_new3' where id=1;
1205 - Lock wait timeout exceeded; try restarting transaction

Session2更新在更新同一条数据的时候超时了,在更新数据的时候添加了排他锁;

4.可重复读(Repeatable read)
4.1查看和设置隔离级别

mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected
 
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set

4.2模拟多个事务交叉执行
Session1执行查询

mysql> begin;
Query OK, 0 rows affected
 
mysql> select * from test_lock where type=2;
+----+----------+------+
| id | name     | type |
+----+----------+------+
|  2 | zhaohui2 |    2 |
+----+----------+------+
1 row in set

Session2更新数据

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui2_new' where type=2;
Query OK, 1 row affected
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> commit;
Query OK, 0 rows affected

Session1执行查询

mysql> select * from test_lock where type=2;
+----+----------+------+
| id | name     | type |
+----+----------+------+
|  2 | zhaohui2 |    2 |
+----+----------+------+
1 row in set

可以发现2次查询的数据结果是一样的,实现了可重复读(Repeatable read),再来看一下是否有幻读(Phantom Read)的问题;

Session3插入数据

mysql> begin;
Query OK, 0 rows affected
 
mysql> insert into test_lock values(null,'zhaohui3',2);
Query OK, 1 row affected
 
mysql> commit;
Query OK, 0 rows affected

Session1执行查询

mysql> select * from test_lock where type=2;
+----+----------+------+
| id | name     | type |
+----+----------+------+
|  2 | zhaohui2 |    2 |
+----+----------+------+
1 row in set

可以发现可重复读(Repeatable read)隔离级别下,也不会出现幻读的现象;

分析一下原因:如何通过悲观锁的方式去实现可重复读和不出现幻读的现象,对读取的数据加共享锁,对同样的数据执行更新操作就只能等待,这样就可以保证可重复读,但是对于不出现幻读的现象无法通过锁定行数据来解决;
最终看到的现象是没有幻读的问题,同时如果对读取的数据加共享锁,更新相同数据应该会等待,上面的实例中并没有出现等待,所以mysql内部应该还有其他锁机制,后面介绍;

5.悲观锁SQL使用
5.1共享锁使用(lock in share mode)
Session1查询数据

mysql> begin;
Query OK, 0 rows affected
 
mysql> select * from test_lock where type=2 lock in share mode;
+----+--------------+------+
| id | name         | type |
+----+--------------+------+
|  2 | zhaohui2_new |    2 |
|  3 | zhaohui3     |    2 |
+----+--------------+------+
2 rows in set

Session2查询数据

mysql> begin;
Query OK, 0 rows affected
 
mysql> select * from test_lock where type=2 lock in share mode;
+----+--------------+------+
| id | name         | type |
+----+--------------+------+
|  2 | zhaohui2_new |    2 |
|  3 | zhaohui3     |    2 |
+----+--------------+------+
2 rows in set

Session3更新数据

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui3_new' where id=3;
1205 - Lock wait timeout exceeded; try restarting transaction

Session1和Session2使用了共享锁,所以可以存在多个,并不冲突,但是Session3更新操作需要加上排他锁,和共享锁不能同时存在;

5.2排他锁使用(for update)
Session1查询数据

mysql> begin;
Query OK, 0 rows affected
 
mysql> select * from test_lock where type=2 for update;
+----+--------------+------+
| id | name         | type |
+----+--------------+------+
|  2 | zhaohui2_new |    2 |
|  3 | zhaohui3     |    2 |
+----+--------------+------+
2 rows in set

Session2查询数据

mysql> begin;
Query OK, 0 rows affected
 
mysql> select * from test_lock where type=2 for update;
Empty set

Session3更新数据

mysql> begin;
Query OK, 0 rows affected
 
mysql> update test_lock set name='zhaohui3_new' where id=3;
1205 - Lock wait timeout exceeded; try restarting transaction

排他锁只能有一个同时存在,所有Session2和Session3都将等等超时;

多版本并发控制MVCC
从上面的例子中分析出,mysql内部使用了其他的并发控制机制,这种机制就是多版本并发控制(MVCC);
为什么要引入此机制,首先通过悲观锁来处理读请求是很耗性能的,其次数据库的事务大都是只读的,读请求是写请求的很多倍,最后如果没有并发控制机制,最坏的情况也是读请求读到了已经写入的数据,这对很多应用完全是可以接受的;







请到「今天看啥」查看全文