Mysql锁机制(详解)

1.概述

怎么解决脏读不可重复读幻读这些问题呢?其实有两种可选的解决方案

方案一 :读操作利用多版本并发控制(MVCC),写操作进行加锁。

所谓的MVCC,就是生成一个ReadView,通过ReadView找到符合条件的记录版本(历史版本由undo日志构建)。查询语句只能到在生成ReadView之前已提交事务所做的更改,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突。

普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。

  • READ COMMITTED隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改,也就是避免了脏读现象;

  • REPEATABLE READ隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作才会生成一个ReadView,之后的SELECT操作都复用这个ReadView,这样也就避免了脏读,不可重复读和幻读的问题。

 方案二:读、写操作都采用加锁的方式。

比如,在银行存款的事务中,你需要先把账户的余额读出来,然后将其加上本次存款的数额最后再写到数据库中。在将账户余额读取出来后,就不想让别的事务再访问该余额,直到本次存款事务执行完成,其他事务才可以访问账户的余额。这样在读取记录的时候就需要对其进行加锁操作,这样也就意味着操作和操作也像写-写操作那样排队执行。

脏读  例如a事务读取了b未提交事务。如果a事务负责读,b事务负责写,b事务从事务开启到关闭中间的过程,事务a不能对事务b进行读,因为加锁了。

不可重复读 例如a事务先读取一条记录,b事务对该记录做了改动之后并提交之后,a事务再次读取时会获得不同的值。如果a事务读取记录时就给该记录加锁,那么另一个事务就无法修改该记录,自然也不会发生不可重复读了。

幻读 例如A事务读取了一个范围的内容,而同时B事务在此期间插入了一条数据.造成幻觉。如果采用加锁的方式解决幻读问题就有一些麻烦,因为当前事务在第一次读取记录时幻影记录并不存在,所以读取的时候加锁就有点尴尬(因为你并不知道给谁加锁)。

对比发现:

  • 采用MVCC方式的话,读-写操作彼此并不冲突,性能更高

  • 采用加锁方式的话,读-写操作彼此需要排队执行,影响性能。

一般情况下我们当然愿意采用MVCC来解决读-写操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用加锁的方式执行。下面就讲解下MySQL中不同类别的锁。

2. 锁的不同角度分类

锁的分类图,如下:

共享锁(S锁)

共享锁(Share Locks,简记为S锁)又被称为读锁,其他事务可以并发读取数据,但任何事务都不能获取数据上的排他锁(只能加共享锁,不能加排他锁),直到已释放所有共享锁。

列如:事务 A对数据对象 T 加上 S锁,则事务 A 只能读 T;其他事务只能再对 A 加 S锁,而不能加 X锁,直到A释放T上的S锁。这就保证了其他事务可以读A,但在A释放T上的S锁之前不能对T做任何修改。

select ... lock in share mode; #添加s锁

排他锁(X锁):

排它锁((Exclusive lock,简记为X锁))又称为写锁,当前写操作没有完成前,它会阻断其他事务写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。

列如:事务A对数据对象 T加上 X锁,则只允许A 读取和修改T,其它任何事务都不能再对 T 加任何类型的锁,直到A 释放T上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。再更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。

select ... for update; #添加X锁

 

MySQL 中,updatedeleteinsertalter 这些写的操作默认都会加上排他锁。select 默认不会加任何锁类型。一旦写数据的任务没有完成,数据是不能被其他任务读取的,这对并发操作有较大的影响。

表级锁

表级锁应用在 MyISAM、InnoDB、BDB 等存储引擎中,每次操作锁住整张表。

    优点:开销小,加锁快;不会出现死锁。
    缺点:锁定粒度大,发生锁冲突的概率最高,并发度最低。

1.表级别的S锁、X锁

MyISAM在执行查询语句(SELECT)前,会给涉及的所有表加读锁,在执行增删改操作前,会给涉及的表加写锁。InnoDB存储引擎是不会为这个表添加表级别的读锁或者写锁的。(有行锁,谁TM用表锁啊)

不过尽量避免在使用InnoDB存储引擎的表上使用LOCK TABLES这样的手动锁表语句,它们并不会提供什么额外的保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的行锁,关于InnoDB表级别的S锁和X锁大家了解一下就可以了。

show open tables where in_use > 0; # 查看哪些表被锁了

lock tables student read # 加写锁
lock tables student write;

unlock tables; # 表解锁

以下是MySQL的表级锁有两种模式:(以MyISAM表进行操作的演示)  

锁类型自己可读自己可写自己可操作其他表他人可读他人可写
读锁否,等
写锁否,等否,等

2.意向锁 (intention lock)

InnoDB 支持多粒度锁(multiple granularity locking),它允许行级锁与表级锁共存,而 意向锁 就是其中的一种表锁

1、意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。

2、意向锁是一种不与行级锁冲突表级锁,这一点非常重要。

3、表明“某个事务正在某些行持有了锁或该事务准备去持有锁”

1.意向锁要解决的问题

在数据表的场景中,如果我们给某一行数据加上了排它锁,数据库会自动给更大一级的空间,比如数据页或数据表加上意向锁,告诉其他人这个数据页或数据表已经有人上过排它锁了(不这么做的话,想上表锁的那个程序),这样当其他人想要获取数据表排它锁的时候,只需要了解是否有人已经获取了这个数据表的意向排他锁即可

意向共享锁(IS):事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁。

意向互斥锁(IX):事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁。

3.自增锁(AUTO-INC锁)

是一种特殊的表级别的锁,专门针对事务插入 auto-increment 类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。

4.元数据锁(MDL锁)

MySQL5.5引入了meta data lock,简称MDL锁,MDL 的作用是,保证读写的正确性。比如 一个查询正在一个表中的数据,而执行期间另一个线程对这个表结构做变更,增加了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的,因此,当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写锁,不需要显式使用,在访问一个表的时候会被自动加上。

举例:会话A:

mysql> begin;
Query 0K,0 rows affected (8.00 sec)
# 默认加了MDL 读锁
mysql> select count( 1 ) from teacher ;

 会话B:

begin;
# 这个时候就会阻塞在这里,有其他事务在读数据。
# 修改表结构 需要加MDL 写锁
alter table teacher add age int;

元数据带来的并发问题:

Session ASession BSession C

begin;

select * from teacher;

begin;

alter table teacher add age int;

begin;

select * from teacher;

#会阻塞#这个时候会阻塞在这里。本来两个S锁可以并发执行的,但是由于会话B添加X锁导致了阻塞

 行级锁

行级锁应用在 InnoDB 存储引擎中,每次锁住一行数据。

    优点:锁定粒度小,发生锁冲突的概率最低,并发度最高。
    缺点:开销大,加锁慢;会出现死锁。

记录锁(Record Locks)

1,记录锁也就是仅仅把一条记录锁上。记录锁总是会锁住索引记录,如果 InnoDB 存储引擎表,比如我们把id值为 8 的 那条记录加一个记录锁。仅仅是锁住了id值为 8 的记录,对周围的数据没有影响。

2,在建立的时候没有设置任何一个索引,那么InnoDB存储引擎会使用隐式的主键来进行锁定。

记录锁是有S锁和X锁之分的,称之为S型记录锁X型记录锁

  • 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;

  • 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。

间隙锁(Gap Locks)  

图中id值为 8 的记录加了gap锁,意味着不允许别的事务在id值为 8 的记录前边的间隙插入新记录,其实就是id列的值( 3 , 8 )这个区间的新记录是不允许立即插入的。比如,有另外一个事务再想插入一条id值为 4 的新记录,它定位到该条新记录的下一条记录的id值为 8 ,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间( 3 , 8 )中的新记录才可以被插入。

1,这里是会阻塞,是前面的行锁知识点。   

session 1session 2
select *from student where id =8 lock in share mode;
select * from student where id = 8 for update;

2,这里session 2并不会被堵住。因为表里并没有id=5这个记录,因此session 1加的是间隙锁(3,8)。而session 2也是在这个间隙加的间隙锁。它们有共同的目标,即:保护这个间隙,不允许插入值。但,它们之间是不冲突的。

session 1session 2
select *from student where id =5 lock in share mode;
select * from student where id =5 for update;

3,这里就会阻塞了,以为加了间隙锁。

session 1session 2
select *from student where id =5 lock in share mode;
insert into student(id, name, class) values (6, 'tom', '三班');

临键锁(Next-Key Locks)

有时候我们既想锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新记录,所以InnoDB就提出了一种称之为Next-Key Locks的锁,官方的类型名称为:LOCK_ORDINARY,我们也可以简称为next-key锁。Next-Key Locks是在存储引擎innodb、事务级别在可重复读的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。

begin;
select * from student where id <= 8 and id > 3 for update;

插入意向锁(Insert Intention Locks)

我们说一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了间隙锁next-key锁 也包含间隙锁),如果有的话,插入操作需要等待,直到拥有间隙锁的那个事务提交。但是 InnoDB规 定事务在等待的时候也需要在内存中生成一个锁结构 ,表明有事务想在某个间隙插入新记录,但是 现在在等待。InnoDB就把这种类型的锁命名为Insert Intention Locks,官方的类型名称为:LOCK_INSERT_INTENTION,我们称为插入意向锁。插入意向锁是一种Gap,不是意向锁,在insert 操作时产生。

插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁。

事实上 插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。

 页级锁

页级锁应用在 DBD 存储引擎中,每次锁住一页数据 - 16KB左右。

    特点:开销和加锁时间介于表级和行级之间;会出现死锁,锁定力度介于表锁和行锁之间,并发度一般。

悲观锁和乐观锁

乐观锁

乐观的假定大概率不会发生并发更新冲突,访问、处理数据的过程中不加锁,只在更新数据时根据版本号时间戳判断是否有冲突,有则处理,无责提交事务乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。

1.乐观锁的版本号机制

在表中设计一个版本字段 version,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行UPDATE ... SET version=version+1 WHERE version=version。此时如果已经有事务对这条数据进行了更改,修改就不会成功。

2. 乐观锁的时间戳机制

时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行 比较,如果两者一致则更新成功,否则就是版本冲突。

你能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或 者时间戳),从而证明当前拿到的数据是否最新。

悲观锁

就是很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层

面阻止其他事务对该数据的操作权限,防止读 - 写写 - 写的冲突。

悲观锁总是假设最坏的情况,每次在拿数据的时候都会上锁,都会认为别人会修改,所以,这样别人想拿这个数据就会阻塞直到它拿到锁( 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程 )。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

按加锁的方式划分:显式锁、隐式锁

隐式锁

  • 情景一: 对于聚簇索引记录来说,有一个trx_id隐藏列,该隐藏列记录着最后改动该记录的事务id。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的trx_id隐藏列代表的的就是当前事务的事务id,如果其他事务此时想对该记录添加S锁或者X锁时,首先会看一下该记录的trx_id隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个X锁(也就是为当前事务创建一个锁结构,is_waiting属性是false),然后自己进入等待状态(也就是为自己也创建一个锁结构,is_waiting属性是true)。

  • 情景二: 对于二级索引记录来说,本身并没有trx_id隐藏列,但是在二级索引页面的PageHeader部分有一个PAGE_MAX_TRX_ID属性,该属性代表对该页面做改动的最大的事务id,如果PAGE_MAX_TRX_ID属性值小于当前最小的活跃事务id,那么说明对该页面做修改的事务都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复情景一的做法。

 session 1:

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

mysql> insert INTO student VALUES( 34 ,"周八","二班");
Query OK, 1 row affected (0.00 sec)

session 2:  

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

mysql> select * from student lock in share mode; #执行完,当前事务被阻塞

执行下述语句,输出结果:   

mysql> SELECT * FROM performance_schema.data_lock_waits\G;
*************************** 1. row ***************************
ENGINE: INNODB
REQUESTING_ENGINE_LOCK_ID: 140562531358232 :7:4:9:
REQUESTING_ENGINE_TRANSACTION_ID: 422037508068888
REQUESTING_THREAD_ID: 64
REQUESTING_EVENT_ID: 6
REQUESTING_OBJECT_INSTANCE_BEGIN: 140562535668584
BLOCKING_ENGINE_LOCK_ID: 140562531351768 :7:4:9:
BLOCKING_ENGINE_TRANSACTION_ID: 15902
BLOCKING_THREAD_ID: 64
BLOCKING_EVENT_ID: 6
BLOCKING_OBJECT_INSTANCE_BEGIN: 140562535619104
1 row in set (0.00 sec)

 显式锁

通过特定的语句进行加锁,我们一般称之为显示加锁,例如:

显示加共享锁:

select ....  lock in share mode

显示加排它锁:

select ....  for update

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: MySQL的MVCC(Multi-Version Concurrency Control)机制是通过为每个读操作创建一个版本(Version)并保留旧版本来实现的。这个机制允许多个事务同时访问同一数据行,同时确保它们不会互相干扰或产生冲突。 MVCC在MySQL中的实现方式是,对于每一行数据,在表中存储一个隐藏的系统版本号(system versioning),并将每个操作(包括SELECT查询)的时间戳与该行的版本号进行比较。当读取一行数据时,MySQL会根据当前的事务时间戳和行的版本号来决定该行是否可见。如果行的版本号早于当前事务的时间戳,则说明该行是旧版本,不可见;如果行的版本号晚于当前事务的时间戳,则说明该行是新版本,可见。 在MVCC机制下,读操作不会阻塞写操作,写操作也不会阻塞读操作。因此,MVCC机制可以提高并发性能和可伸缩性,使得多个事务可以同时访问同一数据库而不会产生锁定和阻塞问题。 但是,MVCC机制也有一些限制。例如,如果事务A在读取某个数据行的同时,事务B修改了该行的值,那么事务A在提交时就会检测到该数据行已经被修改,从而回滚该操作。此外,MVCC机制也会占用更多的存储空间来存储旧版本的数据行。 ### 回答2: MySQL的MVCC(多版本并发控制)是一种用于处理并发访问的机制。MVCC是通过在数据库的各种操作(如事务的开启、读取和写入)中使用隐藏的时间戳来实现的。 MVCC的主要目标是避免读取和写入操作之间的冲突,从而提高数据库的并发性能和资源利用率。它通过在内部为每个事务提供一个唯一的时间戳来实现。每个事务在开始时都会获得一个时间戳,并且事务中的每个操作都使用这个时间戳。 当一个事务读取数据时,它只能读取它开始时间之前的数据版本。这样可以避免读取到其他事务正在写入或修改的数据,从而保证读取操作的一致性和隔离性。 当一个事务写入数据时,它会创建一个新的数据版本,并将其与事务的时间戳关联。这个新版本的数据不会立即覆盖旧的数据,而是以一种类似于快照的方式存在。其他事务在读取数据时仍然可以访问旧版本的数据。 MVCC还使用了回滚段(undo log)来处理事务的回滚操作。当一个事务被回滚时,数据库会使用回滚段将所有该事务做出的修改逆转回去,从而恢复到事务开始之前的状态。 需要注意的是,MVCC机制对于并发性能和资源利用率的提升是有限的。在高并发的情况下,数据库可能会出现锁等待和资源竞争的问题。为了进一步优化并发性能,可以考虑使用其他技术,如乐观并发控制(Optimistic Concurrency Control)和分布式数据库。 ### 回答3: MySQL的MVCC(Multi-Version Concurrency Control)机制是一种并发控制技术,用于处理数据库中的读写冲突。它允许多个事务同时读取数据库,同时也使得读写冲突被有效地解决。 MVCC机制基于以下两个重要的概念:版本号和快照。 首先,每个表中的每个行都有一个版本号。当一个事务对某行进行修改时,会为该事务创建一个新的版本,并将旧版本标记为过期。这样,读取该行的事务会读取到未过期的版本,而不会受到写用户的影响。同时,这也避免了仅读用户被阻塞的情况。 其次,为了实现读取未过期版本的行,MVCC机制通过创建快照来实现。快照是数据库在某个时间点的一个镜像,其中包含了未过期的行版本。当一个读取事务开始时,会生成一个当前的数据库快照,并基于这个快照来读取数据行。这样,读取事务不会看到在其开始时(即快照生成时)已提交的写入事务,从而实现了读写并发。 MVCC机制对于提高数据库的并发性能非常重要。它允许多个事务同时进行读操作,提高了数据库的并发处理能力。此外,它也避免了读写冲突和阻塞的情况,提高了数据库的效率和稳定性。 总之,MySQL的MVCC机制通过使用版本号和快照来实现读写并发控制和冲突的解决。它是提高数据库并发性能和减少阻塞的关键技术之一,并且在实际的数据库应用中扮演着非常重要的角色。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有心不在迟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值