MySQL的锁机制与MVCC机制

在一个高并发的数据库系统里,可能会遇到多个事务同一时刻修改某条数据的情况,这样就产生了资源冲突,解决冲突就需要用到锁。

从思想上来看有乐观锁和悲观锁,从粒度上来思考拥有表锁、行锁,  行锁又分为排它锁和共享锁.

关于数据库的第一类和第二类更新丢失问题:https://blog.csdn.net/chenyang1010/article/details/84425790

我们主要要通过乐观锁和悲观锁来解决第二类问题。

乐观锁:

整个数据处理过程都不加锁,直到处理完准备提交时,才通过版本号检查数据是否过期

优点: 避免了加锁的开销和死锁的出现,提高了系统的并发性能

缺点

1. ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。 (举例, 链表、栈等等,虽然取出来的还是同一个元素,但是上下文环境已经不对)

2. 循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。适用于写操作比较少,数据冲突较少的场景

3.只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

悲观锁:

数据处理开始时就对数据加锁,直到处理完成才释放锁,利用数据库本身提供的锁机制来实现,例如共享锁、排他锁.适用于写操作比较多,数据冲突严重的场景.

悲观锁每一次行数据的访问都是独占的,只有当正在访问该行数据的请求事务提交以后,其他请求才能依次访问该数据,否则将阻塞等待锁的获取。悲观锁可以严格保证数据访问的安全,但是缺点也明显,即每次请求都会额外产生加锁的开销且未获取到锁的请求将会阻塞等待锁的获取,在高并发环境下,容易造成大量请求阻塞,影响系统可用性。另外,悲观锁使用不当还可能产生死锁的情况。

表级锁和行级锁:

表级锁和行级锁是一种锁的粒度。一般来说,锁粒度越小,锁冲突就越少,系统的并发性能就更高,但同时数据库在管理锁方面的开销也越大,当管理锁的开销比数据存取的开销还要大时,反而可能会影响到系统的性能。 
在MySQL中,每个存储引擎都可以有自己的锁策略,例如MyISAM引擎仅支持表级锁,而InnoDB引擎除了支持表级锁外,也支持行级锁(默认)。行级锁可以最大程度地支持并发处理。.

然后innodb行锁分为三种情形:

1)Record lock :对索引项加锁,即锁定一条记录。

2)Gap lock:对索引项之间的‘间隙’、对第一条记录前的间隙或最后一条记录后的间隙加锁,即锁定一个范围的记录,不包含记录本身

3)Next-key Lock:锁定一个范围的记录并包含记录本身(上面两者的结合)。

行锁和索引的关系:查询字段没有走索引(主键索引、普通索引等)时,使用表锁

注:InnoDB行级锁基于索引实现。(以下场景可以先了解下文的意向锁机制来看就能理解了)

未加索引时,两种行锁情况为(使用表锁): 
- 事务1获取某行数据共享锁,其他事务可以获取不同行数据的共享锁,不可以获取不同行数据的排他锁 
- 事务1获取某行数据排他锁,其他事务不可以获取不同行数据的共享锁、排他锁

加索引后,两种行锁为(使用行锁):

  • 事务1获取某行数据共享锁,其他事务可以获取不同行数据的排他锁
  • 事务1获取某行数据排他锁,其他事务可以获取不同行数据的共享锁、排他锁

共享锁和排他锁:

ps:共享锁和排它锁在加锁期间其他的事务都可以进行select语句查询,只要不加锁

共享锁:

1.当事务A对某资源加了共享锁后,其它事务也只能对该资源加共享锁
2.若想加排他锁,需等待所有事务释放共享锁
3.持有共享锁的事务,都可以读取该资源,但不能修改

排它锁:

1.当事务A对某资源加了排他锁后,事务A可以读取和修改该资源
2.其它事务不能对该资源加任何锁,直到事务A释放排他锁

就是说共享锁比排它锁更"大方",它允许别人也对这行数据加共享锁.  共享锁和排它锁同时属于行级锁和悲观锁的范畴.

间隙锁,next-key锁统统属于排他锁。

间隙锁的目的是为了防止幻读,其主要通过两个方面实现这个目的:****
(1)防止间隙内有新数据被插入
(2)防止已存在的数据,更新成间隙内的数据

间隙锁场景:https://blog.csdn.net/zcl_love_wx/article/details/82382582  该博客的select 语句需要加个for update 才行, 不然的话普通的select语句是不带锁的(坑).

各种阻塞情况数据测试: https://blog.csdn.net/u012099869/article/details/52778728  我本人也测试过 文章的实验测试结果均正确.

三种种死锁场景:http://hedengcheng.com/?spm=a2c4e.11153940.blogcont332485.14.13ee2555sM44k5&p=771#_Toc374698322

InnoDB内部采用的死锁预防策略。 

找到满足条件的记录,并且记录有效,则对记录加X锁,No Gap锁(lock_mode X locks rec but not gap);
找到满足条件的记录,但是记录无效(标识为删除的记录),则对记录加next key锁(同时锁住记录本身,以及记录之前的Gap:lock_mode X);
未找到满足条件的记录,则对第一个不满足条件的记录加Gap锁,保证没有满足条件的记录插入(locks gap before rec);

https://blog.csdn.net/tr1912/article/details/81668423  对于最后一种情况比较复杂,只需了解下页面锁即可,InnoDB每个页面为16K,读取一个页面时,需要对页面加S锁,更新一个页面时,需要对页面加上X锁。任何情况下,操作一个页面,都会对页面加锁,页面锁加上之后,页面内存储的索引记录才不会被并发修改。

6、索引数据重复率太高会导致全表扫描:当表中索引字段数据重复率太高,则MySQL可能会忽略索引,进行全表扫描,此时使用表锁。可使用 force index 强制使用索引。

意向锁:

InnoDB为了让表锁和行锁共存而使用了意向锁.

举个粟子(此时假设行锁和表锁能共存): 事务A锁住表中的一行(写锁)。事务B锁住整个表(写锁)。

但你就会发现一个很明显的问题,事务A既然锁住了某一行,其他事务就不可能修改这一行。这与”事务B锁住整个表就能修改表中的任意一行“形成了冲突。所以,没有意向锁的时候,行锁与表锁共存就会存在问题!

意向锁之所以存在是为了提高事务加表锁时的检测性能,试想事务A对某一行加了锁(不管是共享锁还是排他锁),事务B来尝试加表锁,这时就要检测,如果是遍历检测每一行显然是性能极低的,所以引入意向锁,这样的话只要事务A尝试对行/表加锁,先加意向锁,然后事务B来加表锁时,只需要判断和当前表的意向锁是否兼容即可,兼容则可以加表锁,不兼容则进入阻塞状态.

MySQL的MVCC(多版本并发控制)

两阶段锁定协议:

将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)

  • 加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
  • 解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。

InnoDB采用的是两阶段锁定协议,事务执行过程中,根据需要不断的加锁,最后COMMIT或ROLLBACK的时候一次性释放所有锁,实质上是一种悲观并发控制(悲观锁),而悲观锁会降低系统的并发性能。为了提高并发性能,InnoDB同时还实现了多版本并发控制(MVCC)。MVCC是通过保存数据在某个时间点的快照来实现的;优点是:读不加锁,读写不冲突。

https://yq.aliyun.com/articles/644107

inodb事务日志包括redo log和undo log。redo log是重做日志,提供前滚操作,undo log是回滚日志,提供回滚操作。

undo log不是redo log的逆向过程,其实它们都算是用来恢复的日志:
1.redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
2.undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。


 

猜你喜欢

转载自blog.csdn.net/qq_38835878/article/details/89453196