升级篇-MySQL锁及并发控制

锁及并发控制

锁时用来控制用户对同一资源的并发访问的。不同的数据库系统对锁的实现是不同的。如MyISAM引擎的锁是表锁设计。InnoDB存储引擎是基于行锁的设计,当然可以升级为表锁。InnoDB引擎内部也会使用锁用来控制对多种不同的资源提供并发访问。如,操作缓存池中的LRU列表,删除、添加、移动LRU列表中的元素。SQL Server低版本是基于页锁的,并发性能比表锁要高;高版本改为乐观并发和悲观并发两种方式,乐观并发下开始支持行级锁。但是实现方式与InnoDB却完全不一样。

InnoDB中锁机制的实现方式类似于Oracle数据库,提供了一致性的非锁定读、行级锁支持。且行级锁没有相关额外的开销,并可以同时得到并发性和一致性。

lock与latch

用c语言写的InnoDB分为轻量级锁latch和真正的锁lock。latch又可分为mutex(互斥量)和rwlock(读写锁)。其目的是用来保证并发线程操作的临界资源的正确性,并且没有死锁检测机制。

lock的对象是事务,用来锁定的是数据库中的对象。如表、页、行。根据不同的事务隔离级别,一般lock的对象仅在事务commit或rollback后进行释放。并且是有死锁检测机制的。

InnoDB存储引擎中的锁

并发控制机制

并发控制机制有两种思想,乐观锁和悲观锁。乐观锁并不是真正意义上的锁,它先尝试对资源进行修改,在写回是判断资源是否有改变,如果没有改变就表示修改成功,否则表示有其他线程对该资源进行了修改,修改失败。修改失败后有两种处理方式,第一种是直接放弃处理,第二种方式是不断重试。整个过程中并没有对共享资源进行加锁。而悲观锁是一种真正的锁,它会先获取共享资源的锁,然后对共享资源进行修改,最后释放锁。在加锁和释放锁中间,其他线程都会进入等待状态。等待锁的释放然后再尝试获取锁。悲观锁为了减少锁资源的占有,又可以细分为排他锁(写锁)和共享锁(读锁)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mzPXPC8g-1604718391939)(E:\study\mysql\mysql\img\currency control.jpg)]

乐观锁因为没有用锁,所以不会存在死锁问题的。但是由于更新后验证,所以当冲突频率和重试成本较高时更推荐使用悲观锁,而需要非常高的响应速度并且并发量非常大的时候使用乐观锁就能较好的解决问题,在这时使用悲观锁就可能出现严重的性能问题;在选择并发控制机制时,需要综合考虑上面的四个方面(冲突频率、重试成本、响应速度和并发量)进行选择。 这点与Java中的锁机制的选择是类似的。

InnoDB存储引擎可支持乐观锁和悲观锁两种方式。

还有一种方式采用时间戳或版本控制的乐观锁方式。时间戳就是在数据库表中单独加一列时间戳,比如“TimeStamp”, 每次读出来的时候,把该字段也读出来,当写回去的时候,把该字段加1,提交之前 ,跟数据库的该字段比较一次,如果比数据库的值大的话,就允许保存,否则不允许保存,这种处理方法虽然不使用数据库系统提供的锁机制,但是这种方法可以大大提高数据库处理的并发量。在消费金融系统中很多地方有用到这种乐观锁的方式。

锁的类型及粒度

InnoDB实现了两种标准的行级锁,共享锁(Shared Lock)和互斥锁(Exclusive Lock)。

  1. 共享锁(读锁、S Lock):允许事务对一条行数据进行读取;

  2. 互斥锁(写锁、X Lock):允许事务对一条行数据进行删除或更新;

而它们的名字也暗示着各自的另外一个特性,共享锁之间是兼容的,而互斥锁与其他任意锁都不兼容:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uKrGYHLL-1604718391941)(E:\study\mysql\mysql\img\locktype.jpg)]

稍微对它们的使用进行思考就能想明白它们为什么要这么设计,因为共享锁代表了读操作、互斥锁代表了写操作,所以我们可以在数据库中并行读,但是只能串行写,只有这样才能保证不会发生线程竞争,实现线程安全。

参考:

https://draveness.me/mysql-innodb

https://www.cnblogs.com/zhaoyl/p/4121010.html

http://blog.codinglabs.org/articles/theory-of-mysql-index.html

通过保存数据在某个时间点得快照版本,MVCC其实是行级锁的一个变种 。

一致性非锁定读

一致性非锁定读是指InnoDB引擎通过多版本控制读取当前时间数据库中的行数据,如果读取的数据正在被更新或删除,则读取该行的一个快照版本。因为读取操作不需要等待行上X锁的释放。直接读取该行之前一个版本的数据。所以称为非锁定读。该实现是通过前面说的回滚段来完成的。而回滚段是预先将要修改的数据读取到了回滚段,此时回滚段已经在内存中,因此快照数据本身是没有额外的开销的。

但是并不是所有事物隔离级别都采用非锁定的一致性读。在事务隔离级别为READ COMMITTED和REPEATABLE READ下,InnoDB存储引擎采用非锁定一致性读。但是对于快照的定义却不一样。在READ COMMITTED事务隔离级别下,对于快照数据,非一致性读总是读取被锁定行的最新一份快照数据。而在REPEATABLE READ事务隔离级别下,对于快照数据,非一致性读总是读取事务开始时的行数据版本。

一致性锁定读

在默认情况下,事务的隔离级别为REPEATABLE READ模式下,InnoDB存储引擎的SELECT操作使用一致性非锁定读。但是,在某些情况下,用户需要显示地对数据库读取操作进行加锁以保证数据逻辑的一致性。如对于SELECT的一致性锁定读操作的语句有:

SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODE

SELECT FOR UPDATE对读取的行记录加一个X锁,其他事务不能对已锁定的行加上任何锁,必须等待该锁释放。SELECT LOCK IN SHARED MODE 对行记录加S锁,其他事务可以向被锁定的行加S锁,但是如果加X锁,则当前事务被阻塞。两种语句必须要先开起一个事务,在使用两句SELECT语句时,务必加上BEGIN,START TRANSACTION或者SET AUTOCOMMIT=0;

自增长与锁

在InnoDB引擎的内存结构中,对每个含有自增长值得表都有一个自增长计数器。当我们插入一条记录时,先执行以下语句得到计数器的值,然后填充到自增长列。这种实现方式称为AUTO-INC Locking。

SELECT MAX(auto_inc_column) FROM table FOR UPDATE;

这是一种特殊的表锁机制,需要等待插入的SQL语句执行完后才能是否X锁。显然,这是一种很低效的方式,尤其是在INSERT … SELECT的大数据量的插入会影响插入的性能,其他事务的插入将会不长时间阻塞。

从MySQL 5.1.22版本开始,InnoDB存储引擎通过轻量级互斥量的自增长机制,这种机制大大提高了自增长插入的性能。并且提供参数innodb_autoinc_lock_mode来控制自增长的模式,该参数默认值为1。

外键与锁

外键主要用于引用完整性的约束检查。

行锁的算法

行锁算法有如下三种。

Record Lock

单个行记录上的锁。Record Lock总是会去锁住索引记录。如果没有设置任何索引,则隐式采用主键索引进行锁定。

Gap Lock

间隙锁,多个行的锁,但不包括本身。

Next-Key Lock

锁定一个范围,用Gap Lock+Record Lock实现,但包含本身。Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。例如一个索引中包含0,1,3,6这4个值,那么该索引会被Next-Key Locking的区间划分为左开右闭的区间。

(-,0] (0,1] (1,3] (3,6] (6,+)

锁的分类

乐观锁,悲观锁。悲观锁分共享锁和排它锁。乐观锁适用于并发量较小的情况。并发量较大的情况使用悲观锁。这是因为当并发量较大时,会使得竞争资源争抢严重,使用乐观锁会导致操作失败的概率非常大。这些操作运行的最终结果为失败,前面的工作就相当于白做了。但如果用悲观锁就不一样,没有获取到锁的线程会等待。所以高并发下适用于悲观锁。

1.读写锁

​ 在处理并发读或写时,可以通过实现一个由两种类型的锁组成的系统来解决问题。这两种类型的锁通常被称为共享锁和排他锁。也叫读锁和写锁。–类比java的读锁和写锁。出于安全的考虑,确保在给定时间内,只有一个用户能执行写入,并防止其他用户读取正在写入的同一资源。在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改某一部分数据时,MySQL会通过锁定防止其他用户读取同一数据。

2.锁粒度与锁策略

​ 提高共享资源并发性的方式就是让锁定对象更有选择性。尽量只锁定需要修改的部分数据,而不是所有的资源。任何时刻,在给定的资源上,锁定的数据量越少,则系统的并发程度越高,只要相互之间不发生冲突即可。但问题是加锁也需要消耗资源。如果花大量时间管理锁,而不是存储数据,那么系统的性能可能会因此受到影响。所以出现了锁策略,即在锁的开销和数据的安全性之间寻求平衡。大多数商业数据库是在表上增加行级锁。而MySQL提供了多种策略。每种存储引擎都可以实现自己的锁策略和锁粒度。在上面的图中,可以看出存储引擎负责锁的管理。下面介绍MySQL三个重要的锁策略。
(1)页级锁
页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。
(2)表锁。锁定整张表,开销最小。它会锁定整张表。一个用户在对表进行写操作前,需要先获得写锁,但会阻塞其他用户对该表的所有读写操作。写锁有更高的优先级。尽管存储引擎可以管理自己的锁,MySQL本身还是会使用各种有效的表锁来实现不同的目的。比如ALTER TABLE就会使用表锁,而忽略存储引擎的锁机制。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。

(3)行级锁。行级锁时一种排他锁,防止其他事务修改此行。最大程度支持并发,但是带来了最大的锁开销。InnoDB和XtraDB中实现了行级锁。
在使用以下语句时, Oracle 会自动应用行级锁:

  1. INSERT、 UPDATE、 DELETE、 SELECT … FOR UPDATE [OF columns] [WAIT n | NOWAIT];
  2. SELECT … FOR UPDATE 语句允许用户一次锁定多条记录进行更新
  3. 使用 COMMIT 或 ROLLBACK 语句释放锁。

事务

MySQL可用START TRANSACTION语句开始一个事务,然后要么使用COMMIT提交事务将修改的数据持久保留,要么使用ROLLBACK撤销所有的修改。

一个运行良好的事务处理系统,必须具有ACID这些标准特性。

1.原子性。

2.一致性。

3.隔离性。

4.持久性。

1.3.1 隔离级别

​ 4种不同隔离级别是对大事务性能的妥协,通过MVCC只有写写冲突。

​ 分布式系统保证了高可用和安全性,高安全,高水平扩展。

​ map套map==nosql

​ 解决扩容和缩容的问题:一致性hash

​ 7-15s不可用,用于选择主机(RAFT)

​ MHA-parox-解决mysql主死从竞争

​ 每种存储引擎实现的隔离级别不尽相同。下面简单介绍下四种隔离级别。

READ UNCOMMITTED(未提交读)

​ 事务可以读取未提交的数据。这也被称为脏读。性能也不会比其他好很多。在实际应用中一般很少使用。

READ COMMITED(提交读)

​ 大多数数据库系统的默认隔离级别都是READ COMMITTED(但MySQL不是)。一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。所以会出现两次执行同样的查询,可能会得到不一样的结果。

REPEATABLE READ(可重复读)

​ 可重复读解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但是从理论上无法解决幻读的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻读。InnoDB和XtraDB存储引擎通过多版本并发控制解决了幻读的问题。可重复读是MySQL的默认事务隔离级别。

SERIALIAZABLE(可串行化)

​ SERIALIAZABLE是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题,简单来说,SERIALIAZABLE会在读取的每一行数据上都加锁,所以,可能导致大量的超时和锁争用的问题。

1.3.2 死锁

​ 死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。

​ 为了解决死锁,数据库系统实现了各种死锁检测和死锁超时机制。越复杂的系统,比如InnoDB存储引擎,越能检测到死锁的循环依赖。并立即返回一个错误。InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚(这是相对比较简单的死锁回滚算法)。

​ 所以应用程序在并发情况下必须考虑如何处理死锁。

1.3.3 事务日志

​ 事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不是每次都将修改的数据本身持久到磁盘。事务日志采用的是追加的方式。因此写日志的操作是磁盘上一小块区域内的顺序I/O。大都数存储引擎采用预写式日志,修改数据需要写两次磁盘。如果数据的修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。

1.3.4 MySQL中的事务

  1. 自动提交

​ MySQL默认采用自动提交模式。也就是说,如果不是显示地开始一个事务,则每个查询都被当做一个事务执行提交操作。通过show variables like ‘autocommit’;查看。当AUTOCOMMIT=0时,所有的查询都是在一个事务中,直到显式地执行COMMIT提交或者ROLLBACK回滚,该事务结束,同时又开始了另一个新事务。

​ 有一些命令,比如ALTER TABLE,LOCK TABLE会在执行之前执行COMMIT提交当前的活动事务。MySQL可以通过执行SET TRANSACTION ISOLATION LEVEL命令来设置隔离级别。

  1. 不支持在事务中混合使用存储引擎

​ MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。如果在一个事务中使用了事务型和非事务型的表,如果该事务需要回滚,非事务型的表上的变更就会无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。

1.1 多版本并发控制

MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。它们一般都同时实现了多版本并发控制(MVCC)。可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。MVCC的典型实现有乐观并发控制和悲观并发控制。下面同构InnoDB的简化版行为来说明MVCC是如何工作的。

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存了行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号。每开始一个事务,系统版本号都会自动递增,事务开始时刻的系统版本号会作为事务的版本号,用来查询到每行记录的版本号进行比较。下面分析下REPEATABLE READ隔离级别下,MVCC具体是如何操作的。

SELECT

​ InnoDB会根据以下两个条件检查每行记录:

​ a. InnoDB只查找版本早于当前事务版本的数据行

行锁:数据库默认存储引擎InnoDB(show variables like ‘%engine%’;命令查看)的默认隔离级别Repeatable Read(select @@tx_isolation;命令查看)的默认锁定方式,之所以说明存储引擎,是因为Mysql中每一种数据引擎的锁定方式以及实现都是相互独立的。

如果 SQL 是精确查询,不管是 RR 还是 RC 隔离级别下,都会在命中的索引上加 record lock(行锁)。X 锁是排它锁(Exclusive Lock),也有人称作是写锁。S 锁是共享锁(Shared Lock),也有人称作是读锁。不管是 X 锁,还是 S 锁,都是加在行上的。注意,行锁不是加在记录上的,而是加在索引上的!
————————————————
版权声明:本文为CSDN博主「业余草」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xmtblog/article/details/92470543

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值