MysqlJava技术升华面试精选

MySQL锁机制和MVCC版本控制

2021-05-02  本文已影响0人  CJ21

一、MySQL锁机制

1.1 锁分类

锁的粒度 划分:

使用的方式 划分:

思想 划分:

1.2 MySQL引擎的锁机制

MySQL引擎介绍以下两种,MyISAM和InnoDB。

1.2.1 MyISAM引擎

1.2.1.1 概念

MySQL的MyISAM储存引擎就支持表锁,MyISAM的表锁模式有两种:

行锁是InnoDB默认的支持的锁机制,MyISAM不支持行锁,这个也是InnoDB和MyISAM的区别之一。

1.2.1.2 测试表级锁

Mysql中可以通过以下sql来显示的在事务中显式的进行加锁和解锁操作:

-- 显式的添加表级读锁
LOCK TABLE 表名 READ
-- 显示的添加表级写锁
LOCK TABLE 表名 WRITE
-- 显式的解锁(当一个事务commit的时候也会自动解锁)
unlock tables;

建表

创建一个测试表student,这里要指定存储引擎为MyISAM,并插入两条测试数据:

CREATE TABLE IF NOT EXISTS student( \
    id INT PRIMARY KEY auto_increment, \
    name VARCHAR(40), \
    score INT \
)ENGINE MyISAM;

INSERT INTO student(name, score) VALUES('zs', 60);
INSERT INTO student(name, score) VALUES('ls', 80);

查看一下,表结果如下图所示:

mysql> select * from student;
+----+------+-------+
| id | name | score |
+----+------+-------+
|  1 | zs   |    60 |
|  2 | ls   |    80 |
+----+------+-------+

MyISAM表级写锁

1. 第一个窗口执行下面的sql,在session1中给表添加写锁:

LOCK TABLE student WRITE;

2. 与此同时再开启一个session2窗口,然后在可以在session2中进行查询或者插入、更新该表数据,可以发现都会处于等待状态,也就是session1锁住了整个表,导致session2只能等待:

mysql> select * from student;
   

3. 在session1中进行查询、插入、更新数据,都可以执行成功:

mysql> select * from student;
+----+------+-------+
| id | name | score |
+----+------+-------+
|  1 | zs   |    60 |
|  2 | ls   |    80 |
+----+------+-------+
2 rows in set (0.00 sec)

mysql> update student set score=100 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

4. 解锁student表

mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)

session2中的查询语句返回结果

mysql> select * from student;
+----+------+-------+
| id | name | score |
+----+------+-------+
|  1 | zs   |   100 |
|  2 | ls   |    80 |
+----+------+-------+
2 rows in set (3 min 27.09 sec)

总结:从上面的测试结果显示,当一个线程获取到表级写锁后,只能由该线程对表进行读写操作,别的线程必须等待该线程释放锁以后才能操作。

MyISAM表级读锁

1. 第一步还是在session1给表加读锁。

mysql> LOCK TABLE student READ;
Query OK, 0 rows affected (0.00 sec)

2. 然后在session1中尝试进行插入、更新数据,发现都会报错,只能查询数据。

mysql> insert into student value(4,"ww",99);
ERROR 1099 (HY000): Table 'student' was locked with a READ lock and can't be updated

3. 最后在session2中尝试进行插入、更新数据,程序都会进入等待状态,只能查询数据,直到session1解锁表session2才能插入、更新数据。

mysql> select * from student;
+----+------+-------+
| id | name | score |
+----+------+-------+
|  1 | zs   |   100 |
|  2 | ls   |    80 |
+----+------+-------+
2 rows in set (0.00 sec)

mysql> insert into student value(4,"ww",99);

总结:从上面的测试结果显示,当一个线程获取到表级读锁后,该线程只能读取数据不能修改数据,其它线程也只能加读锁,不能加写锁。

1.2.1.3 MyISAM表级锁竞争情况

MyISAM存储引擎中,可以通过查询变量来查看并发场景锁的争夺情况show status like 'table%'

mysql> show status like 'table%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Table_locks_immediate      | 81    |
| Table_locks_waited         | 1     |
| Table_open_cache_hits      | 9     |
| Table_open_cache_misses    | 1     |
| Table_open_cache_overflows | 0     |
+----------------------------+-------+
5 rows in set (0.00 sec)

主要是查看table_locks_waited和table_locks_immediate的值的大小分析锁的竞争情况。

1.2.1.4 并发插入

在我们平时执行select语句的时候就会隐式的加读锁,执行增、删、改的操作时就会隐式的执行加写锁。

MyISAM存储引擎中,虽然读写操作是串行化的,但是它也支持并发插入,这个需要设置内部变量concurrent_insert的值。

它的值有三个值0、1、2。可以通过以下的sql查看concurrent_insert的默认值为「AUTO(或者1)」。

mysql> show variables like '%concurrent_insert';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| concurrent_insert | AUTO  |
+-------------------+-------+
1 row in set (0.00 sec)

1.2.1.5 锁调度

MyISAM存储引擎中,假如同时一个读请求,一个写请求过来的话,它会优先处理写请求,因为MyISAM存储引擎中认为写请求比读请求重要。

这样就会导致,假如大量的读写请求过来,就会导致读请求长时间的等待,或者"线程饿死",因此MyISAM不适合运用于大量读写操作的场景,这样会导致长时间读取不到用户数据,用户体验感极差。

当然可以通过设置low-priority-updates参数,设置请求链接的优先级,使得Mysql优先处理读请求。

1.2.2 InnoDB引擎

1.2.2.1 概念

InnoDB和MyISAM不同的是,InnoDB支持表锁间隙锁Next-key Lock锁行锁,默认支持的是行锁。

需要注意的是:InnoDB中的行级锁是对索引加的锁,在不通过索引查询数据的时候,InnoDB就会使用表锁。若是Mysql觉得执行索引查询还不如全表扫描速度快,那么Mysql就会使用全表扫描来查询,这是即使sql语句中使用了索引,最后还是执行为全表扫描,加的是表锁。

1.2.2.2 测试行级锁

若想显式的给表加行级读锁和写锁,可以执行下面的sql语句:

-- 给查询sql显示添加读锁
select ... lock in share mode;
-- 给查询sql显示添加写锁
select ... for update;

建表

创建一个测试表,并插入两条数据:

-- 先把原来的MyISAM表给删除了
DROP TABLE IF EXISTS student;
CREATE TABLE IF NOT EXISTS student( \
    id INT PRIMARY KEY auto_increment, \
    name VARCHAR(40), \
    score INT \
)ENGINE INNODB;
-- 插入测试数据
INSERT INTO student(name, score) VALUES('zs', 60);
INSERT INTO student(name, score) VALUES('ls', 80);

InnoDB表级写锁(对非索引列加行级写锁)

  1. 创建的表中可以看出对表中的字段只有id添加了主键索引,接着就是在session1窗口执行begin开启事务,并执行下面的sql语句:
begin;
-- 使用非索引字段查询,并显式的添加写锁
select * from student where name='zs' for update;
+----+------+-------+
| id | name | score |
+----+------+-------+
|  1 | zs   |    60 |
+----+------+-------+
1 row in set (0.00 sec)
  1. 然后在session2中执行update语句和insert语句,上面更新上锁的是id=1的数据行,现在update的是id=1的数据,insert的是新的一行数据,会发现程序也都会进入等待状态并超时:
mysql> update student set score = 100 where id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into student value(3,"ww",99);

总结:可见若是使用非索引查询,直接就是使用的表级锁,锁住了整个表。

InnoDB行级写锁(对索引列加行级写锁)

  1. 若是session1使用的是id来查询
begin;
mysql> select * from student where id = 1 for update;
+----+------+-------+
| id | name | score |
+----+------+-------+
|  1 | zs   |    60 |
+----+------+-------+
1 row in set (0.00 sec)
  1. 然后在session2中执行update语句和insert语句,上面更新上锁的是id=1的数据行,现在update的是id=1的数据,insert的是新的一行数据,会发现update等待并超时,insert成功:
mysql> insert into student value(3,"ww",99);
Query OK, 1 row affected (0.00 sec)
mysql> update student set score = 100 where id = 1;

总结:若是使用索引查询,就是使用的行级锁,锁住了指定的行。但是是否执行索引还得看Mysql的执行计划,对于一些小表的操作,可能就直接使用全表扫描。

1.执行非索引条件查询执行的是表锁。
2.执行索引查询是否是加行锁,还得看Mysql的执行计划,可以通过explain关键字来查看。
3.用普通键索引的查询,遇到索引值相同的,也会对其他的操作数据行的产生影响。

InnoDB普通索引锁住相同行

  1. 添加普通索引
mysql> alter table student add index idx_name(name);
Query OK, 0 rows affected (6.94 sec)
  1. 修改name为相同值
mysql> update student set name = "zs" where id = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from student;
+----+------+-------+
| id | name | score |
+----+------+-------+
|  1 | zs   |    60 |
|  2 | zs   |    80 |
|  3 | ww   |    99 |
+----+------+-------+
3 rows in set (0.00 sec)
  1. 然后在session1中锁住name="zs"的行
begin;
select * from student where name = "zs" for update;
  1. 在session2中修改id=1的记录
mysql> update student set score = 0 where id = 1;

总结:用普通键索引的查询,遇到索引值相同的,也会对其他的操作数据行的产生影响。

结论:

1.2.2.3 InnoDB间隙锁和Next-Key锁

当我们使用范围条件查询而不是等值条件查询的时候,InnoDB就会给符合条件的范围索引加锁,在条件范围内并不存的记录就叫做"间隙(GAP)"

InnoDB中引入了间隙锁,解决了事务等级-不可重复读下会产生的幻读问题,只能通过提高隔离级别到串行化来解决幻读现象。

这里抛出几种情况来测试间隙锁:

例如我们执行下面的sql语句,就会对id大于100的记录加锁,在id>100的记录中肯定是有不存在的间隙:

Select * from student where id> 100 for update;

Next-Key锁是行锁加上记录前的间隙锁的结合。如果一个会话在索引中的记录R上有一个共享或独占的锁,则另一个会话不能按照索引顺序在R之前的间隙中插入新的索引记录。

建表

mysql> alter table student add num int not null default 0;
mysql> update student set num = 1 where id = 1;
mysql> update student set num = 1 where id = 2;
mysql> update student set num = 3 where id = 3;
mysql> insert into student values(4,'tq',100,5);

mysql> select * from student;
+----+------+-------+-----+
| id | name | score | num |
+----+------+-------+-----+
|  1 | zs   |    60 |   1 |
|  2 | zs   |    80 |   1 |
|  3 | ww   |    99 |   3 |
|  4 | tq   |   100 |   5 |
+----+------+-------+-----+
4 rows in set (0.00 sec)

为非唯一索引单行加锁

1.接着在session1的窗口开启事务,并执行下面操作:

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from student where num=3 for update;
+----+------+-------+-----+
| id | name | score | num |
+----+------+-------+-----+
|  3 | ww   |    99 |   3 |
+----+------+-------+-----+
1 row in set (0.00 sec)
  1. 同时打开窗口session2,并执行新增语句:
insert into student values(5,'ceshi',5000,2);  -- 程序出现等待
insert into student values(5,'ceshi',5000,4);  -- 程序出现等待
insert into student values(5,'ceshi',5000,1);  -- 程序出现等待
insert into student values(5,'ceshi',5000,5);  -- 程序出现等待
insert into student values(5,'ceshi',5000,6);  -- 新增成功
insert into student values(6,'ceshi',5000,0);  -- 新增成功

总结:从上面的测试结果显示在区间 [1,3] U [3,5] 之间加了锁,是不能够新增数据行,这就是新增num=1/2/4/5失败的原因,但是在这个区间以外的数据行是没有加锁的,可以新增数据行。

根据索引的有序性,而普通索引是可以出现重复值,那么当我们第一个sesson查询的时候只出现一条数据num=3,为了解决第二次查询的时候出现幻读,也就是出现两条或者更多num=3这样查询条件的数据。

为唯一索引单行加锁

因为主键索引具有唯一性,不允许出现重复,那么当进行等值查询的时候id=3,只能有且只有一条数据,是不可能再出现id=3的第二条数据。

因此它只要锁定这条数据(锁定索引),在下次查询当前读的时候不会被删除、或者更新id=3的数据行,也就保证了数据的一致性,所以主键索引由于他的唯一性的原因,是不需要加间隙锁的。

为非唯一索引范围加锁

mysql> select * from student;
+----+------+-------+-----+
| id | name | score | num |
+----+------+-------+-----+
|  1 | zs   |    60 |   1 |
|  2 | zs   |    80 |   1 |
|  3 | ww   |    99 |   3 |
|  4 | tq   |   100 |   5 |
+----+------+-------+-----+
4 rows in set (0.01 sec)

在session1中执行下面的sql语句:

begin;
select * from student where num>=3 for update;

在session2中新增数据:

insert into student values(6,'ceshi',5000,2);  -- 程序出现等待
insert into student values(7,'ceshi',5000,4);  -- 程序出现等待
insert into student values(8,'ceshi',5000,1);  -- 新增数据成功

原理:单查询num>=3的时候,在现有的student表中满足条件的数据行,那么在设计者的角度出发,我为了解决幻读的现象:在num>=3的条件下是必须加上间隙锁的。
而在小于num=3中,下一条数据行就是num=1了,为了防止在(1,3]的范围中加入了num=3的数据行,所以也给这个间隙加上了锁,这就是添加num=2数据行出现等待的原因。

使用不存在的检索条件是否会加上间隙锁?

假如是查询num>=8的数据行呢?因为student表并不存在中num=8的数据行,num最大num=6,所以为了解决幻读(6,8]与num>=8也会加上锁。

1.2.2.4 表锁

意向锁:当一个事务带着表锁去访问一个被加了行锁的资源,那么这个行锁就会升级为意向锁将表锁住。
自增锁:事务插入自增类型的列时,获取自增锁。如果一个事务正在往表中插入自增记录,其他事务都必须等待。

1.2.2.5 死锁

死锁在InnoDB中才会出现死锁,MyISAM是不会出现死锁,因为MyISAM支持的是表锁,一次性获取了所有的锁,其它的线程只能排队等候。

而InnoDB默认支持行锁,获取锁是分步的,并不是一次性获取所有的锁,因此在锁竞争的时候就会出现死锁的情况。

虽然InnoDB会出现死锁,但是并不影响InnoDB成为最受欢迎的存储引擎,MyISAM可以理解为串行化操作,读写有序,因此支持的并发性能低下。

死锁案例

举一个例子,现在数据库表employee中六条数据,如下所示:

+----+------+-------+-----+
| id | name | score | num |
+----+------+-------+-----+
|  1 | zs   |    60 |   1 |
|  2 | zs   |    80 |   1 |
|  3 | ww   |    99 |   3 |
|  4 | tq   |   100 |   5 |
+----+------+-------+-----+

其中name=ldc的有两条数据,并且name字段为普通索引,分别是id=2和id=3的数据行,现在假设有两个事务分别执行下面的两条sql语句:

-- session1执行
update student set num = 1 where name ='zs';
-- session2执行
select * from student where id = 1 or id = 2;

其中session1事务执行的sql获取的数据行是两条数据,假设先为id=1的数据行加排它读锁;然后cpu的时间分配给了session2事务,执行查询为id=2的数据行加排它读锁。
此时cpu又将时间分配给了session1事务,为id=2数据添加排他读锁时发现已经被添加了排它读锁,它就处于等待的状态。
当cpu把时间有分配给了session2事务,为id=1数据添加排他读锁时发现已经被添加了排它读锁,这样就行了死锁,两个事务彼此之间相互等待。

死锁的解决方案

首先要解决死锁问题,在程序的设计上,当发现程序有高并发的访问某一个表时,尽量对该表的执行操作串行化,或者锁升级,一次性获取所有的锁资源。

然后也可以设置参数innodb_lock_wait_timeout,超时时间,并且将参数innodb_deadlock_detect 打开,当发现死锁的时候,自动回滚其中的某一个事务。

1.3 事务类型

在默认 的自动事务提交 设置下 select 同Update,Insert,Delete一样都会启动一个事务,这种就是隐式事务。

问题:如更新库存语句如下

update stock set leave_stock=leave_stock-sale_stock  where id = 1 and leave_stock >= sale_stock;

那么是否存在一下问题:
如果隔离级别是可重复读,执行update库存操作时开启隐式事务,AB事务同时开启,A事务修改库存从6改到5,,但是B事务不可见还是在6基础上减库存,导致少卖的情况发送。

1.3.1 显示事务

显示事务是一种由你自己指定的事务.这种事务允许你自己决定哪批工作必须成功完成,
否则所有部分都不完成.为了给自己的事务定界,可以使用关键字BEGIN TRANSACTION和
ROLLBACK TRANSACTION或COMMIT TRANSACTION.
BEGIN TRANSACTION—这个关键词用来通知SQL Server一个事务就要开始了.
BEGIN TRANSACTION后面发生的每一条S Q L语句都是同一个事务中的一部分.
ROLLBACK TRANSACTION—这个关键词用来通知SQL Server自BEGIN TRANSACTION
后的所有工作都应取消,对数据库中任何数据的改变都被还原,任何已经创建或删除的对
象被清除或恢复.
COMMIT TRANSACTION—这个关键词用来通知SQL Server自BEGIN TRANSACTION
后的全部工作都要完成并成为数据库的一个永久性部分.在同一个事务中,你不能同时
使用ROLLBACK TRANSACTION和COMMIT TRANSACTION.
你必须意识到,即使你的脚本中有错误,而你又让SQL Server提交事务,该事务也将执
行.如果你打算依赖于现实事务保证数据完整性,必须在脚本中建立错误检查机制.

1.3.2 隐式事务

隐式事务是SQL Server为你而做的事务.隐式事务又称自动提交事务.如果运行一条
I N S E RT语句,SQL Server将把它包装到事务中,如果此I N S E RT语句失败,SQL Server将回滚
或取消这个事务.每条S Q L语句均被视为一个自身的事务.

1.4 总结

需要注意的是:事务添加了排它锁,不代表其他事务不能无锁读取该数据(无锁可以读取添加排它锁的数据)。

- 无锁
>select ... from ...;

- 共享锁
>select ... from ... lock in share mode;

- 排它锁
>update ...
>delete ...
>insert ...
>select ... from ... for update

查看锁信息:
show status like 'innodb_row_lock%';

名称 解释
Innodb_row_lock_current_waits 当前正在等待锁定的数量
Innodb_row_lock_time 从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg 每次等待所花平均时间
Innodb_row_lock_time_max 从系统启动到现在等待最长的一次所花的时间
Innodb_row_lock_waits 系统启动后到现在总共等待的次数

二、MVCC快照

2.1 概念

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法。
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。

2.2 流程

  1. 当修改或删除记录时,会插入一条新记录并指向前一个版本的记录,并标记该记录的操作类型和事务id。
  2. 当开启事务并查询表时,会为该事务保存一个快照read-view,保存了当前还未提交的事务id的列表和最大的事务id。
  3. 如果是可重复度,那么该事务再次读取数据时会根据保存的read-view,去undo日志查找对其可见的版本列并返回。
  4. 如果查找到的最新记录的事务id在read-view的事务id列表中,那么根据指针查找上一版本的记录,直到找到对其可见的记录。
read-view流程图

例:同时开启两个事务,同时不加锁查询同一条记录,在一个事务中修改记录并提交,记录修改成功,但是在另一个事务中读取该记录发现还是修改前的值,直到提交事务后查询该记录,才能读到修改后的记录;因为该事务中读取记录后会保存快照,事务中再次读取都会读取该快照,直到提交事务。

image.png

三、隔离级别

3.1 读问题

3.1.1 隔离级别的实现

通过锁实现了读未提交和串行化;通过锁配合MVCC实现了读已提交和可重复读。

3.1.1.1 锁机制

读未提交:读数据时不加锁;
读已提交:读数据加共享锁,但是读完就释放锁而不是等到事务结束;
可重复读:读数据时添加共享行锁,事务结束释放锁;
可串行化:读数据时添加共享表锁,事务结束释放锁。

3.1.1.2 MVCC机制(MySQL主要使用快照保证隔离级别)

MVCC机制生成一个数据快照,并用这个快照来提供一定级别的一致性的读取,也成为了多版本数据控制。MVCC实现的是普通读取不加锁,并且读写分离,保证了读写并发。

3.2 写问题

脏写(丢失更新):事务一和事务二读取到数据后进行修改,最后提交的事务覆盖了之前提交的事务,造成丢失更新。

写问题解决方案

上一篇下一篇

猜你喜欢

热点阅读