• 欢迎来到本博客,希望可以y一起学习与分享

Mysql锁机制介绍

MySQL benz 1个月前 (07-14) 28次浏览 0个评论 扫描二维码
文章目录[隐藏]

锁的必要性及分类

当用户访问量增长时,数据库往往是一个系统的性能瓶颈,但不光光是性能瓶颈,数据安全也会随之浮现,这时候锁机制就非常有必要。

并发造成的数据安全问题主要分为3个方面:脏读、幻读、不可重复读

一. 脏读

脏读就是一个事务读取到了另一个事务未提交的数据。

二. 幻读

幻读就是一个事务读到了另一个事务insert的数据

三. 不可重复读

不可重复读就是多次读取统一数据返回的结果不一致。同脏读不同,这是读取已经提交的数据;同幻读也不同,这边是更新数据,幻读是插入数据。

MySQL通过对事务的隔离来解决以上三种问题
共有4种隔离级别

MySQL通过锁机制来实现事务的隔离
锁的分类如下

Mysql几种锁定机制类型

MySQL 主要在两个层面上实现并发控制:服务器层和存储引擎层。
当读写有冲突的时候,一般对于MySQL来说,是进行锁表操作的。

读写锁

读锁:读锁是共享的,多个用户同一时刻读取同一资源,他们之前互不影响。

写锁:写锁则是排他的,也就是说一个写锁会阻塞其他的写锁和读锁,这是出于安全策略的考虑,只有这样,才能确保在给定的时间里,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源。

锁粒度

MySQL 各存储引擎使用了三种类型(级别)的锁定机制:行级锁定,页级锁定和表级锁定。

1.行级锁定

锁定对象的颗粒度很小,只对当前行进行锁定,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力;弊端就是获取锁释放锁更加频繁,系统消耗更大,同时行级锁定也最容易发生死锁;
行级锁定的主要是Innodb存储引擎和NDB Cluster存储引擎;

2.页级锁定

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

3.表级锁定

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

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

两段锁协议(2PL)

两段锁协议规定所有的事务应遵守的规则:
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.已提交读

3.1准备测试表

3.2查看和设置隔离级别

3.3模拟多个事务交叉执行

Session1执行查询

Session2更新数据

Session1执行查询

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

Session1更新数据

Session2更新数据

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

4.可重复读

4.1查看和设置隔离级别

4.2模拟多个事务交叉执行

Session1执行查询

Session2更新数据

Session1执行查询

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

Session3插入数据

Session1执行查询

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

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

5.悲观锁SQL使用

5.1共享锁使用(lock in share mode)

Session1查询数据

Session2查询数据

Session3更新数据

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

5.2排他锁使用(for update)

Session1查询数据

Session2查询数据

Session3更新数据

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

多版本并发控制MVCC

从上面的例子中分析出,mysql内部使用了其他的并发控制机制,这种机制就是多版本并发控制(MVCC);
为什么要引入此机制,首先通过悲观锁来处理读请求是很耗性能的,其次数据库的事务大都是只读的,读请求是写请求的很多倍,最后如果没有并发控制机制,最坏的情况也是读请求读到了已经写入的数据,这对很多应用完全是可以接受的;
多版本并发控制(Multiversion Concurrency Control):每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适的结果直接返回;读写操作之间的冲突就不再需要被关注,而管理和快速挑选数据的版本就成了MVCC需要解决的主要问题。

再来看一下可重复读(Repeatable read)现象,通过MVCC机制读操作只读该事务开始前的数据库的快照(snapshot), 这样在读操作不用阻塞写操作,写操作不用阻塞读操作的同时,避免了脏读和不可重复读;

当然并不是说悲观锁就没有用了,在数据更新的时候数据库默认还是使用悲观锁的,所以MVCC是可以整合起来一起使用的(MVCC+2PL),用来解决读-写冲突的无锁并发控制;
MVCC使用快照读的方式,解决了不可重复读和幻读的问题,如上面的实例所示:select查询的一直是快照信息,不需要添加任何锁;
以上实例中使用的select方式把它称为快照读(snapshot read),其实事务的隔离级别的读还有另一层含义:读取数据库当前版本数据–当前读(current read)

当前读和Gap锁

区别普通的select查询,当前读对应的sql包括:

以上sql本身会加悲观锁,所以不存在不可重复读的问题,剩下的就是幻读的问题;
Session1执行当前读

Session2执行插入

为什么明明锁住的是type=2的数据,当插入type=1也会锁等待,因为InnoDB对于行的查询都是采用了Next-Key锁,锁定的不是单个值,而是一个范围(GAP);
如果当前type类型包括:1,2,4,6,8,10锁住type=2,那么type=1,2,3会被锁住,后面的不会,锁住的是一个区间;这样也就保证了当前读也不会出现幻读的现象;
注:type字段添加了索引,如果没有添加索引,gap锁会锁住整张表;

乐观锁

乐观锁是一种思想,认为事务间争用没有那么多,和悲观锁是相对的,乐观锁在java的并发包中大量的使用;一般采用以下方式:使用版本号(version)机制来实现,版本号就是为数据添加一个版本标志,一般在表中添加一个version字段;当读取数据的时候把version也取出来,然后version+1,更新数据库的时候对比第一次取出来的version和数据库里面的version是否一致,如果一致则更新成功,否则失败进入重试,具体使用大致如下:

先查询后更新,需要保证原子性,要么使用悲观锁的方式,对整个事务加锁;要么使用乐观锁的方式,如果在读多写少的系统中,乐观锁性能更好;

总结

本文首先从Mysql的悲观锁出发,然后介绍了悲观锁和事务隔离级别之间的关系,并分析为什么没有使用悲观锁来实现隔离级别;然后从问题出发分别介绍了MVCC和Gap锁是如何解决了不可重复读的问题和幻读的问题;最后介绍了乐观锁经常被用在读数据远大于写数据的系统中。


文章 Mysql锁机制介绍 转载需要注明出处
喜欢 (0)

您必须 登录 才能发表评论!