数据库隔离级别分为RU, RC, RR, Serialisable.以mysql 的InnoDB为例, 讲解一下数据库的隔离级别的原理是什么.
数据库实现隔离级别主要还是通过两个技术MVCC(多版本并发控制)和锁. 不同的隔离级别, MVCC的版本生成时机不同, 锁的范围和释放锁的时机也不同,就造成了不同的隔离级别.
首先我们来了解一下什么是MVCC和锁
基础知识
几种现象
我们先了解一下几个读现象: 脏读, 不可重复读, 幻读
脏读
脏读是指一个事务A能够读到另一个事务B还未提交的操作.这里的问题是如果事务B发生了Rollback显然会造成异常. 这是应该避免的一种错误现象.
不可重复读
不可重复读指的是, 事务中一条数据被重复读取的结果不一致. 假设如果有两个事务A, B并发执行. 事务A能够读取到事务B提交修改后的数据.这个现象针对的是update, delete操作
幻读
幻读指的是, 两个事务A, B并发执行, 事务A能够读取到事务B中新增的数据. 举个例子, 事务A执行select * from users where age > 0 and aget < 18;
返回的结果是10条记录. 同时事务B执行insert into users(name, age) values('allen', 15);
事务A再执行相同的select语句将会得到11条记录, 这就是幻读.
MVCC
多版本并发控制, 顾名思义就是在高并发的情况下, 数据库存在多个版本数据的情况.这个功能在mongoDB和HBase中都提供.如果没有提供MVCC的功能, 一个事务想要读取另一个事务正在修改的数据行的时候只能互斥等待, 这个时候并发能力就下降了, 而MVCC可以通过两种方式去构建出一个历史版本的数据.
- 实时保留数据的一个或多个版本
- 通过undo日志来构建版本
数据库的锁
首先我们要了解一个概念, 就是数据库的锁并不是加在数据行上面而是加在索引行上面的.
接着我们来了解一下, 什么情况下会加锁. 在一般修改数据记录的情况下就会触发加锁的操作比如insert, delete, update, 注意我们刚刚说过了加锁是加在索引行上面的, 如果这些操作没有走索引是不会加行级锁的而是加表级锁. 而select需要显示声明的情况下才会触发加锁操作:
-
select * from users where id = 1;
// 不加锁 -
select * from users where id = 1 for update;
// 加排它锁 -
select * from users where id = 1 lock in share mode;
// 加共享锁
锁的分类
- Share and Exclusive Locks: 共享锁相当于一种JAVA的读锁, 读不会上锁, 但是读写, 写写操作是互斥的.Exclusive loc则是一种排它锁, 都是互斥的.
- Record Locks: 行级锁, 之前说过锁是加在索引行上的, 这个就是加在固定的索引行上加锁.比如: select * from users where id = 1 and id = 10 for update; 就是在id = 1和id=10的索引行上面加锁. 需要注意的是, 如果执行select * from users where name = 'allen' for update; 如果name列上没有加索引, 那么就会触发表锁, 会锁住全表
- Gap Locks: 间隙锁, 它会锁住两个索引之间的区域, 比如select * from users where id > 1 and id < 10 for update; 那么会id在(1, 10)的索引区间加上锁.
- Next-key Locks: 也是一种间隙锁, Gap Locks配合Record Locks形成一个闭区间的锁, 比如select * from users where id >= 1 and id <= 10;
四种隔离机制
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read Uncommit | yes | yes | yes |
Read Commited | no | yes | yes |
Read Repeatable | no | no | yes |
Serialisable | no | no | no |
Read Uncommit
这个模式下, 数据在发生修改以后就会释放锁, 而不是等到事务提交以后才释放锁. 这就导致各种问题, 会出现脏读, 更别提幻读了.
Read Commited
这个时候的锁是在事务提交以后释放的, 我们之前说过事务中的写操作是需要上锁的, 所以在释放锁之前读也读不到才对呀(事务中的读使用排它锁),一个事务能够读取到另一个事务提交以后的变更, 这是为什么呢?
谜底就是因为使用MVCC, MVCC的出现使得能够获取到最新版本的数据快照, 也就是说读出来的数据实际是通过MVCC机制构建出来的, 这里就不存在锁的问题.那么由于使用了MVCC, 也就是说重复执行select能够得到最新版本的数据快照, 也就是说能够读取到别的事务提交以后的最新变更, 这就导致了不可重复读的问题.
此外Read Commited还存在幻读的问题, 这是因为没有用间隙锁, 比如事务A中执行 select * from users where age > 10 and age < 18 for update;
一旦事务B中insert一条age=15的数据, 那么事务A可以读到这个新增的数据, 这就是幻读.
Read Repeatable
可重复读, 这个模式改进了一下所以可以在不可重复读和避免幻读都进行了规避.
- 通过MVCC机制从事务开始时select获取最新版本的数据, 事务进行中的所有select都是用这个版本的数据, 所以能够保证可重复读
- 锁范围调整, 在行锁的基础上添加了Gap Locks形成next-key locks, 在遍历过的索引行范围内都能够进行上锁, 从而阻塞其他事务往这个范围内insert数据, 避免幻读
Serialisable
串行化, 会自动将所有的select都转化成select lock in share mode执行, 也就是针对所有的读写操作都会加锁, 可靠性加强, 但是并发度大大降低.