本文是阅读美团技术团队的记录形文章,更详细内容请访问美团技术团队官方网站
两段锁
数据库遵循的是两段锁协议,将事务分成两个阶段:加锁和解锁
- 加锁阶段:在该阶段对任何数据进行读操作之前都要申请并获得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的合并
串行化
在串行化读写互斥并一并加锁,并使用悲观锁作为机制因此实现简单且更为安全,但是严重影响并发性能造成严重的性能问题
如果业务并发特别少且要求数据可靠,可以考虑该隔离级别