无题/Innodb事物隔离级别及其锁 美团技术学习

Created Sun, 13 Jun 2021 12:40:53 +0800 Modified Wed, 13 Dec 2023 07:03:38 +0000
2138 Words

本文是阅读美团技术团队的记录形文章,更详细内容请访问美团技术团队官方网站

两段锁

数据库遵循的是两段锁协议,将事务分成两个阶段:加锁和解锁

  • 加锁阶段:在该阶段对任何数据进行读操作之前都要申请并获得S锁,在进行写操作之前要获取X锁
  • 解锁阶段:当事务释放一个封锁后,进入解锁阶段,在该阶段只能进行解锁操作不能进行加锁阶段

两段锁可以保证事务的并发调度是串行化的

四种隔离级别

隔离级别脏读不可重复读幻读
未提交读(RU)可能可能可能
已提交读(RC)不可能可能可能
可重复读(RR)不可能不可能可能
可串行化(SE)不可能不可能不可能
  • RU:允许脏读,可能读取到其他会话中未提交事务修改的数据
  • RC:只能读取到已提交的数据,Oracle默认级别
  • RR:在同一个事务内的查询都是事务开始时刻一致性的,InnoDB默认级别,真正消除了不可重复读,但存在幻读
  • SE:完全串行执行,读写相互阻塞

MySQL锁的种类

MySQL中有很多种锁,行锁、表锁等等。表锁是对一整张表加锁,但是会锁住整张表导致并发能力下降,一般在DDL时使用

行锁则锁住数据行,只锁住有限的数据,并发能力强

已提交读(RC)

在RC中,数据的读取是不加锁的,但是写入、修改和删除是需要加锁的

事务A事务B
begin;begin;
update class_teacher set class_name=‘Class1’ where teacher_id=1;update class_teacher set class_name=‘初三三班’ where teacher_id=1;
commit;

为了防止并发过程中修改冲突,事务A中会给teacher_id=1的数据行加锁,并一直不commit,这样事务B就一直拿不到该行锁,wait直到超时

需要注意的是,teacher_id是有索引的,如果查询的是没有索引的字段呢,那么MySQL会给整张表的所有数据行加行锁(表锁?)。这是因为在SQL运行过程中,MySQL并不知道哪些数据行是需要查询的,如果一个条件无法通过索引快速过滤,就会将所有记录加锁后返回,再由MySQL Server层进行过滤

但MySQL在这种情况做了一些改进,在Server过滤时会将不满足条件的数据调用unlock_row的方法释放该记录的锁,这就保证了只有持有满足条件的行会上锁

这种情况同样适用于RR级别,因此在对一个数据量很大的表做批量修改时,如果无法使用索引,MySQL的过滤速度将会特别慢

可重复读(RR)

RR是InnoDB的默认隔离级别

读(Read)

可重读是指在一个事务的多个实例并发读取数据时保持一致性

例如在RC隔离级别下,如果事务A对数据表的某一行进行读取后,事务B也对该行做了修改并提交该事务。此时事务A又对该行进行了读取,最终返回的结果和第一次读取是不一致的,这就是不可重复读RC

但在RR级别下,即使别的事务对数据进行了修改,后面读取的数据和前面是保持一致的,这就是RR,而如何实现的呢,那就是MVCC了

不可重复读和幻读的区别

不可重复读重点在于update和delete,而幻读重点在于insert

如果使用锁来实现的话,在可重复读中sql第一次读取数据后就对数据加锁,其他事务无法修改就可实现可重复读但是无法锁住insert的数据,因此事务A如果先读取数据或修改数据,事务B还是可以insert提交数据,这时A中就会发现凭空多出了一条数据,这就是幻读,简单的行锁是不能避免幻读的需要更高级的串行隔离级别,读写用各自的锁并互斥,这样就能有效避免幻读、不可重复读、脏读等问题但会影响数据库的并发性能

悲观锁和乐观锁

  • 悲观锁
    指数据对修改持保持态度,在整个数据处理过程中都把数据锁住其他事务不能干扰只能等待。悲观锁往往凭靠数据库提供的锁来实现,在悲观锁的情况下,为了保证事务的隔离性就需要一致性锁定读。读取时也需要加锁,保证其他事务不能修改该数据;修改时也要加锁,保证其他事务在修改时不能读取数据
  • 乐观锁
    乐观锁采用更为宽松的情况下,大部分基于数据版本记录来实现。版本记录就是在数据添加一个版本字段,通常为version。在读取数据时,同时将版本号一并读出,在更新该数据时对其版本号加1。此提交的数据版本会和数据表对应记录的当前版本信息对比,如果提交数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据

InnoDB中的MVVC

在InnoDB中,每行数据都会新增两个隐藏字段用于记录何时创建和过期(删除)

在实际中,存储的并不是时间,而是事务的版本号,每开启一个事务,版本号就会递增,在RR级别下

  • SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号
  • INSERT时,保存当前事务版本号为行的创建版本号
  • DELETE时,保存当前事务版本号为行的删除版本号
  • UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行

MVVC会占用额外的控件,进行更多的行检查和维护工作,但能减少行的使用,大多数读操作都不用加锁

从理论上说RR也不能解决幻读问题,只有串行化能够解决幻读。但在MySQL中,RR级别下不存在幻读情况

写(当前读)

在MySQL中,事务采用了Next-Key锁来解决幻读问题

Next-Key

Next-Key锁是行锁和GAP的合并

串行化

在串行化读写互斥并一并加锁,并使用悲观锁作为机制因此实现简单且更为安全,但是严重影响并发性能造成严重的性能问题

如果业务并发特别少且要求数据可靠,可以考虑该隔离级别