锁机制
并发事务访问,分为以下几种
1.读-读 允许
2.写-写 对相同的数据进行写入 可能出现脏写
一条事务加一个锁? 三个事务开三把锁 等待执行is_waitting
如果为true 就让下一个事务等它执行完再继续执行操作。
3.读-写
脏读,不可重复读,幻读
脏读
指事务A读取到了事务B更新了但是未提交的数据,然后事务B由于某种错误发生回滚,那么事务A读取到的就是脏数据。
举个例子
我查要这个人的salary原本应该是10000,记为线程A(read),另一个线程B(write)在这个线程A之前 把salary修改了5000,此时事务还未提交,还没来得及进行回滚,然后我就查到了线程B的数据 5000。 结果线程B内部错误,导致整个事务回滚,回溯到10000。线程A继续执行命令 将这个人的脏读的salary加上10000
脏读的数据:5000+10000
实际的应该的数据:10000+10000
不可重复读
两次读发现结果不一样。线程A最开始查一次 中间插进来一个线程B 直接把你查的东西改掉,可能是木马病毒,导致你这个任务最后再读的时候发现这个查的和第一次不一样。
幻读:
两次读发现结果多了几条。直接将你查的东西数据乱插入几条。导致你这个任务最后再读的时候发现这个查的和第一次不一样。
并发问题解决方案1:MVCC
MVCC生成一个ReadView 读操作只能查到ReadView生成之前已提交事务所做的更改 未提交的事务都是看不到的,写操作枷锁
ReadView的存在保证了事务不可以读取到未提交的事务所做的更改。
并发问题解决方案2:读写都加锁
性能:w,r 彼此不冲突。MVCC方案效率高
MYSQL:
1.对数据的操作类型进行划分
读锁/写锁 通常被称为共享锁/排他锁 S锁 X锁
旧版本加S锁 SELECT….. LOCK IN SHARE MODE
8.0 FOR SHARE
X锁 FOR UPDATE
1.表锁
1.1表级别的s锁和x锁
LOCK TABLES t READ
LOCK TABLES t WRITE
unlock tables ;
粒度大枷锁的速度快 在开启另一个事务的时候锁会被释放
begin ;
lock tables user_profile read ;
select * from user_profile;
commit ;
begin;
insert into user_profile value (9,1231,'male',12,'besadida',3.2,20,2,20);
rollback ;
关于DDL 就算你给它开启了事务 你是无法回滚的 因为是对表的结构进行操作 所以慎用drop!truncate好像也不能回滚!
s锁 两者均可读 均不可写
x锁 自己可写可读 他人写读统统阻塞
1.2元数据锁
不需要手动开启,在访问一个表的时候会自动加上
当对一个表做增删查改的时候,加MDL读锁,当对表结构做更改操作的时候,加MDL写锁
- 读锁之间不互斥,所以可以多个线程同时对一张表增删查改(DML DQL);
- 读写锁之间,写锁之间是互斥的,如果有多个线程要同时给一个表加字段,其中一个要等待另外一个执行完成才能开始执行;
- 事物中的MDL锁,在语句执行时开始申请,但是语句结束后并不会马上释放,而是等到这个事物提交后才释放; (⭐)
所以说开启了事务 没问题的话就赶紧commit掉 不然就rollback重新改sql。不然会造成数据库崩溃等严重后果,你就等着跑路吧
InnoDB的厉害之处还是实现了更细粒度的 行锁 表锁不推荐使用
Innodb支持多粒度锁,允许行锁和表锁共存,意向锁就是其中的一种
1.3意向锁
作用: 加意向锁的目的是为了表明某个事务正在锁定一行或者将要锁定一行。表名加锁的“意图”。
因为mysql中表锁和行锁是可以共存的
数据库对表进行操作的时候 需要判断两点条件
1.这个表是否被锁住
2.表中的行是否被锁住
IS意向共享锁 :对表中某行上s锁,默认这个表会开一个IS锁
IX意向共享锁:对表中某行上x锁,默认这个表会开一个Ix锁
意向锁之间是互相兼容的,意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性
事务A 查询id =5 并上S锁 。事务B 也查询id=6 并上X锁 是被允许的
举例:事务A要修改这个表中的某一行
流程:
1.检查这个表是否上了读锁或者是写锁
2.然后A向这个表申请IX锁或IS锁,如果申请不到,就说明有另外事务在对这个表中的行进行加锁
IX锁可以看做一把钥匙,钥匙在其他事务手上,也抢不过来,只能等它的事务一提交,钥匙就
没人占用了,事务A就可以拿到这个IX锁了
3.一拿到这个IX锁就可以获得这个表的最高权限,你就可以进行修改了
总结:
- IX IS 自己之间不会冲突,都在上锁,没什么关系
- IS S 之间不冲突,其他都冲突。
- 意向锁的目的是为了避免全表扫描。
SELECT column FROM table … FOR UPDATE;
DBA对某行上X锁 那么InooDB就会为这个表上IX锁
由于意向锁是由InooDB维护的,我们并不能去更改它
从上图我们可以看见
提示:如果我们对表加锁,机器是不知道你哪行是上了锁的 !!它只能从头到尾遍历 效率极低
才引出意向锁
(X1) IX (X2) IX 都不冲突
那么事务A、B同时修改你这条记录呢? 那么就跟你这个意向锁没什么关系 直接被行X锁 X锁之间阻塞了
第二种情况流程:
事务A在执行 对某行进行查询并上了X锁 默认表就会有一个意向排他锁IX
事务B插进来 想要将表上X锁 首先它要拿到这个表的IX锁 才可以上锁
但是IX锁还在事务A中 事务B就抢不过来 就没有权限,直接阻塞。
等你A执行完 然后执行B
commit提交后事务结束 就释放这个锁
2.行锁
MySQL的行锁又分为共享锁(S锁)和排他锁(X锁)。
当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可 以继续获取X型记录锁; 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不 可以继续获取X型记录锁。
间隙锁(Gap Locks)
MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用 MVCC 方 案解决,也可以采用 加锁 方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读 取操作时,那些幻影记录尚不存在,我们无法给这些 幻影记录 加上 记录锁 。InnoDB提出了一种称之为 Gap Locks 的锁,官方的类型名称为: LOCK_GAP ,我们可以简称为 gap锁 。
临键锁(Next-Key Locks)
跟间隙锁差不多 只是不允许在这个行记录的前面加值
一般普通的select语句,InnoDB不加任何锁,我们称之为快照读
select * from test;
通过加S锁和X锁的select语句或者插入/更新/删除操作,我们称之为当前读
select * from test lock in share mode;
select * from test for update;
insert into test values(…);
update test set …;
delete from test …;
特殊说明:以上的当前读,读取的都是记录的最新版本。对读取记录都会加锁,除了第一条语句lock in share mode是对记录加S锁(共享锁)外,其他的操作都是加X锁(排他锁)。两阶段锁协议
传统的关系型数据库加锁都要遵循一个原则:
两阶段锁原则
两阶段锁是将锁的操作分为两个阶段,加锁阶段和解锁阶段,并且保证加锁阶段和解锁阶段不相交。
分享一个例子说明两阶段锁协议:
事务隔离级别
不同事务隔离级别对应的行锁也是不同的,所以我们需要先了解事务的隔离级别后,再来演示不同隔离级别的行锁如何加上去。
MySQL的4种隔离级别:
READ UNCOMMITTED(读未提交):任何一个事务当中,都可以看见其他事务的执行情况,会出现脏读现象。
READ COMMITTED(读已提交,简称:RC):在当前事务中只能看见已经提交事务的执行结果,当同一事务在读取期间出现新的commit操作,会出现不可重复读现象。
REPEATABLE READ(可重复读,简称:RR):这是MySQL默认的隔离级别,得益于MVCC,
它能在同一事务在多实例并发读取数据时看到相同的数据行,消除了脏读、不可重复读,默认不会出现幻读
(MySQL的行锁+间隙锁解决了快照读的幻读,未解决当前读的幻读)。
SERIALIZABLE(串行):MySQL的最高隔离级别,通过加锁,强制事务执行顺序,保证不会出现幻读问题。
没有索引的情况下,InnoDB的当前读会对所有记录都加锁。所以在实际开发中,如果是当前读或者是插入/更新/删除等操作一定要使用索引,否则会产生大量的锁等待。
RC隔离级别+where唯一索引
避免了当前读,所有记录都枷锁的情况,造成业务等待
3.乐观和悲观锁
乐观锁(Optimistic Lock): 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作),乐观锁适用于多读的应用类型,这样可以提高吞吐量
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本(version)或者是时间戳来实现,不过使用版本记录是最常用的。
悲观锁(Pessimistic Lock): 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
但是在效率方面,处理加锁的机制会产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,如果已经锁定了一个线程A,其他线程就必须等待该线程A处理完才可以处理
4.MVCC
MVCC
**MVCC
**,全称 Multi-Version Concurrency Control
,即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
解决:堵塞问题
当前读
像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
快照读
像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
MVCC 多版本并发控制是 「维持一个数据的多个版本,使得读写操作没有冲突」 的概念,只是一个抽象概念,并非实现
因为 MVCC 只是一个抽象概念,要实现这么一个概念,MySQL 就需要提供具体的功能去实现它,「快照读就是 MySQL 实现 MVCC 理想模型的其中一个非阻塞读功能」。而相对而言,当前读就是悲观锁的具体功能实现
3.1.1数据库并发场景?
读-读
:不存在任何问题,也不需要并发控制读-写
:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读写-写
:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失
3.1.2MVCC组合
MVCC + 悲观锁
:MVCC解决读写冲突,悲观锁解决写写冲突MVCC + 乐观锁
:MVCC解决读写冲突,乐观锁解决写写冲突我们数据库中的每行数据,除了我们肉眼看见的数据,还有几个隐藏字段,得开天眼才能看到。分别是trx_id、db_roll_pointer。
trx_id
6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID。
roll_pointer(版本链关键)
7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
如上图,
trx_id
是当前操作该记录的事务ID
,而roll_pointer
是一个回滚指针
,用于配合undo日志
,指向上一个旧版本
6.2 undo日志
Undo log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log里。
当事务进行回滚时可以通过undo log 里的日志进行数据还原。
Undo log 的用途
保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。
用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。
1.insert undo log
代表事务在insert新记录时产生的undo log , 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
2.update undo log(主要)
事务在进行update或delete时产生的undo log ; 不仅在事务回滚时需要,在快照读时也需要;
所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
————————————————
3.1.3侧视图
事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。
记录并维护系统当前活跃事务的ID(trx_id)(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。
Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
trx_ids: 当前系统活跃(未提交)事务版本号集合。
low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”。
up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”
creator_trx_id: 创建当前read view的事务版本号;