1.LBCC 方案
既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要操作的数据,不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制 Lock Based Concurrency Control(LBCC)。
PS:关于 MySQL 中的锁,可以参考我的这篇文章…
读未提交(Read Uncommitted)
解决更新丢失问题。如果一个事务已经开始写操作,那么其他事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现,即事务需要对某些数据进行修改必须对这些数据加 X 锁,读数据不需要加 S 锁。
PS:排它锁(Exclusive),又称为X 锁,写锁。共享锁(Shared),又称为S 锁,读锁。
读已提交(Read Committed)
解决了脏读问题。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。这可以通过“瞬间共享读锁”和“排他写锁”实现, 即事务需要对某些数据进行修改必须对这些数据加 X 锁,读数据时需要加上 S 锁,当数据读取完成后立刻释放 S 锁,不用等到事务结束。
可重复读取(Repeatable Read)
禁止不可重复读取和脏读取,但是有时可能出现幻读数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
Mysql默认使用该隔离级别。这可以通过“共享读锁”和“排他写锁”实现,即事务需要对某些数据进行修改必须对这些数据加 X 锁,读数据时需要加上 S 锁,当数据读取完成并不立刻释放 S 锁,而是等到事务结束后再释放。
串行化(Serializable)
解决了幻读的问题的。提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
问题
如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地影响操作数据的效率。
所以我们还有另一种解决方案,如果要让一个事务前后两次读取的数据保持一致,那么我们可以在修改数据的时候给它建立一个备份或者叫快照,后面再来读取这个快照就行了。这种方案我们叫做多版本的并发控制 Multi Version Concurrency Control(MVCC)。
2.MVCC 优化
多版本并发控制(Multi-Version Concurrency Control, MVCC)是MySQL中基于乐观锁理论实现隔离级别的方式,用于实现读已提交和可重复读取隔离级别的实现。
PS:关于 MVCC 机制,可以参考我的这篇文章…
以 InnoDB 为例,每一行中都冗余了两个字断。一个是行的创建版本,一个是行的删除(过期)版本。具体的版本号(trx_id)存在 information_schema.INNODB_TRX 表中。版本号(trx_id)随着每次事务的开启自增。
事务每次取数据的时候都会取创建版本小于当前事务版本的数据,以及过期版本大于当前版本的数据。
PS:在 InnoDB 中,MVCC 是通过 Undo log 实现的;Oracle、Postgres等等其他数据库都有MVCC的实现。
MVCC 作用及问题?
最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。
但只使用 MVCC 仍然是有问题的,因为它只是解决了快照读的问题,但是对于多个进程同时写入(当前读),它没法保证并发安全。
所以,MVCC 只能算上一个性能上的优化。
PS:对于 MySQL 的读可以分为两类:
- 快照读:普通 select
- 当前读:
- insert
- delete
- update
- select … for update / lock in share mode
3.最终策略:LBCC+MVCC
在 InnoDB 中,MVCC 和锁是协同使用的,这两种方案并不是互斥的。
PS:那 MVCC 与锁是什么关系呢?在 InnoDB 中,锁和 MVCC 都是用于并发控制。MVCC 诞生的原因就是改善基于锁的方式带来的效率低的问题。使得读写之间互不阻塞,提高了单纯的基于锁的并发效率。
问题一:各个隔离级别如何实现?
1)Read Uncommited:RU 隔离级别,不做并发控制
2)Read Commited:RC 隔离级别
- 快照读:MVCC,每次快照读时都生成 ReadView,读完就销毁
- 当前读:记录锁(RC 不支持 Gap Lock),直到事务结束才释放
- 隐式:insert、delete、update 默认使用
- 显式:select for update / lock in share mode
小结:
- 正因为是 ReadView 是一致性视图是会变化的,所以快照读下 RC 会出现不可重复读问题
- 正因为是记录锁,锁的只是单条记录,所以 RC 在当前读下会出现幻读的问题。
注:RC 不管单行还是范围查找,都不会用到gap lock;除了两种特殊情况——外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会使用间隙锁封锁区间。
3)Repeatable Read:RR 隔离级别
- 快照读:MVCC,第一次快照读时生成 ReadView,事务提交时销毁
- 当前读:当前元素临键锁(左开右闭) + 相邻间隙锁,直到事务结束才释放
- 隐式:insert、delete、update 默认使用
- 显示:select for update / lock in share mode
小结:
- 正因为视图的一致性,所以快照读下 RR 解决了不可重复读问题
- 正因为间隙锁,锁住了元素及左右区间(无法添加系元素),所以当前读下 RR 解决了幻读问题(InnoDB,有争议)
4)Serializable:串行化
所有的 select 语句都会被隐式的转化为 select … in share mode,会和 insert、update、delete 互斥。
问题二:所以,事务隔离级别怎么选?
RU 和 Serializable 肯定不能用。为什么有些公司要用 RC,或者说网上有些文章推荐有 RC?
RC 和 RR 主要有几个区别:
- RR 的间隙锁会导致锁定范围的扩大
- 条件列未使用到索引,RR 锁表,RC 锁行
- RC的“半一致性”(semi-consistent)读可以增加 update 操作的并发性。
在RC中,一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足 update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)。
实际上,如果能够正确地使用锁(避免不使用索引去加锁),只锁定需要的数据,用默认的RR级别就可以了。
文末再放两个博主觉得不错的参考链接: