一、问题
在并发操作数据库数据时,可能会出现很多意外的情况,对于这些情况大致可以分为以下四种类型:
1、 丢失修改:两个事务A1和A2同时取数据并修改。A1提交的数据被A2的数据覆盖了,导致A1的修改丢失。
2、 脏读:事务A1修改某个数据并写回磁盘,事务A2读取同一数据,但A1由于某种原因撤销了,这时A1修改过的数据恢复原来的值,A2读取的数据就与数据库中的数据不一致。
3、 不可重复读:事务A1读取数据后,事务A2读取数据并修改,导致事务A1多次读取同一数据,结果不一致。
4、 幻读:事务在操作过程中进行两次查询,第二次查询结果,包含了第一次的查询中未出现的数据,这里是因为在两次查询过程中有另外一个事务插入数据造成的。
二、隔离级别
为了避免上面几种情况,在标准的SQL规范中定义了4种事务隔离级别,不同隔离级别,处理的问题不同,下面具体看下:
1、未提交(Read Uncommitted)
读未提交是最低的隔离级别。允许脏读,但不允许修改丢失,事务可以读取到其他事务未提交的修改。
2、 读提交(Read Uncommitted)
允许不可重复读,但不允许脏读。读取数据的事务允许其他事物继续访问数据,但是未提交的写事务将会禁止其他事务访问改行。
3、 可重复度(Repeatable Read)
禁止不可重复读和脏读,但不能避免幻读。
4、 可序列化(Serializable)
最高的隔离级别,它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。
隔离级别能处理的问题对应表如下:
三、 并发控制器
隔离级别的实现就是通过使用并发控制机制对在同一时间执行的事务进行控制,限制不同的事务对于同一资源的访问和更新。下面介绍三个重要而常见的并发控制器:
1、 锁
在一个事务中,我们并不会把整个数据库都加锁,而是对那些需要访问的数据项进行加锁,Mysql和常见的数据库中的锁都分为两种:
1)、共享锁,也叫读锁/S锁:保证了读操作可以并发执行,互不影响。
2)、互斥锁,也叫写锁/X锁。保证了更新数据库数据时,不会有其他事务访问或者更改同一条记录造成不可预知的问题。
2、 时间戳
时间戳也可以实现事务的隔离性,使用时间戳实现事务的数据库,会为每一条记录保留两个字段:
读取时间戳:包含了所有访问该记录的事务中的最大时间戳,
写的时间戳:保存了将记录改成当前值的事务的时间戳。
使用时间戳实现事务的隔离性时,往往都会使用乐观锁,先对数据进行修改,在写回时再去判断当前值,也就是时间戳是否改变过,如果没有改变过,就写入,否则生成一个新的时间戳再次更新数据。
3、 多版本和快照隔离
通过维护多个版本的数据,数据库可以允许事务在数据被其他事务更新时对旧版本的数据进行读取,很多数据库都对这一机制进行了实现,因为所有的读操作不再需要等待写锁的释放,所有能够显著地提升读的性能。MySQL就是通过undo log实现了MVCC(多版本并发控制),保证事务并发执行时能够不等待互斥锁的释放直接获取数据。
四、MySQL中隔离级别对应的实现原理
1、未提交(Read Uncommitted)
对于读操作不加锁。而对写操作加共享锁,且直到事务结束之后才释放。
2、 读提交(Read Uncommitted)
对于写操作加排他锁。读操作使用MVCC机制,通过MVCC获取当前数据的最新快照。快照是在每次select操作的时候。
注:当一个事务中有多次select操作,且在这些select操作之间,有更新数据,就会导致后面后面生成的版本的数据与前面生成的版本不一样,就会重现不可重复读的情况;
3、 可重复度(Repeatable Read)
对于写操作加排他锁。和读提交(Read Uncommitted)一样,读操作也是使用MVCC机制,但不同的地方在于,在一个事务中,快照的生成只在第一次select操作时生成,后面多个select都是在这个版本上进行查询,这样就不会出现不可重复读了。
注:事务A中MVCC在第一次select操作后生成最新的快照,此后其他事务对数据库的修改,插入和删除的数据对于事务A是不可见的,但如果其他事务在此后插入的数据被事务A修改了,那此数据对于事务A就可见了(具体可以去了解MySQL中的MVCC),这样就出现了幻读。如下图:
4、 序列化(Serializable)
隐性的将所有普通select转化为select ... lock in share mode执行,即针对同一数据的所有读写都变成互斥的了,可靠性大大提高,并发性大大降低。