事务的隔离级别

为何需要设置数据库隔离级别?

在数据库操作中,在并发的情况下可能出现如下问题:

  • 更新丢失(Lost Update)
    1
    不论后面的数据库更新是否提交,都有可能让前面的更新丢失。

如果多个线程操作,基于同一个查询结构对表中的记录进行修改,那么后修改的记录将会覆盖前面修改的记录,前面的修改就丢失掉了,这就叫做更新丢失。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。

第 1 类丢失更新:事务A撤销时,把已经提交的事务B的更新数据覆盖了。

时间 取款事务A 取款事务B
T1 开始事务
T2 开始事务
T3 查询余额为1000元
T4 查询余额为1000元
T5 汇入100元,修改余额为1100元
T6 提交事务
T7 取款100元, 修改余额为900元
T8 撤销事务
T9 余额回复为1000元(丢失更新)

第 2 类丢失更新:事务A覆盖事务B已经提交的数据,造成事务B所做的操作丢失。

时间 取款事务A 取款事务B
T1 开始事务
T2 开始事务
T3 查询余额为1000元
T4 查询余额为1000元
T5 取款100元, 修改余额为900元
T6 提交事务
T7 汇入100元,修改余额为1100元
T8 提交事务
T9 余额回复为1100元(丢失更新)

解决方法:对行加锁,只允许并发一个更新事务。

  • 脏读(Dirty Read)

    脏读(Dirty Read):A事务读取B事务尚未提交的数据并在此基础上操作,而B事务执行回滚,那么A读取到的数据就是脏数据。

1
读到了别的事务回滚前的脏数据
时间 取款事务A 取款事务B
T1 开始事务
T2 开始事务
T3 查询余额为1000元
T4 取出500元,余额为500元
T5 查询余额为500元 (脏读)
T6 撤销事务,余额恢复为1000元
T7 汇入100元,修改余额600元
T8 提交事务

解决办法:如果在第一个事务提交前,任何其他事务不可读取其修改过的值,则可以避免该问题。

  • 不可重复读(Non-repeatable Reads)

    一个事务对同一行数据重复读取两次,但是却得到了不同的结果。事务T1读取某一数据后,事务T2对其做了修改,当事务T1再次读该数据时得到与前一次不同的值。

1
一个事务中两次读取的数据的内容不一致 (一般基于行而言)
1
2
3
4
事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,
然后事务A再次读取的时候,发现数据不匹配了,就是不可重复读

同一数据,在一个事务的两次读取之间存在另一更新、删除的事务。
时间 取款事务A 取款事务B
T1 开始事务
T2 开始事务
T3 查询余额为1000元
T4 查询余额为1000元
T5 取出100元,修改余额为900元
T6 提交事务
T7 查询余额为900元(不可重复读)

解决办法:如果只有在修改事务完全提交之后才可以读取数据,则可以避免该问题。

  • 幻读

    指两次执行同一条 select语句会出现不同的结果,第二次读会增加一数据行,并没有说这两次执行是在同一个事务中。一般情况下,幻象读应该正是我们所需要的。但有时候却不是,如果打开的游标,在对游标进行操作时,并不希望新增的记录加到游标命中的数据集中来。隔离级别为游标稳定性的,可以阻止幻象读。例如:目前工资为1000的员工有10人。那么事务1中读取所有工资为1000的员工,得到了10条记录;这时事务2向员工表插入了一条员工记录,工资也为1000;那么事务1再次读取所有工资为1000的员工共读取到了11条记录。

    1
    一个事务中两次读取的数据的数量不一致 (一般基于表而言)
1
2
3
4
5
事务A首先根据条件索引得到N条数据,
然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,
导致事务A再次搜索发现有N+M条数据了,就产生了幻读。

同一数据,在一个事务中的两次(读取)写入之间存在另一插入的事务。
时间 统计金额事务A 转账事务B
T1 开始事务
T2 开始事务
T3 统计总存款为10000元
T4 新增一个存款账户转入100元
T5 提交事务
T7 再次统计总存款为10100元

不可重复读和幻读比较:

两者有些相似,但是前者针对的是update或delete,后者针对的insert。

隔离级别包括哪些?

数据库事务的隔离级别有4个,由低到高依次为

  • Read uncommitted (未提交读 ==》 读未提交)

如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过 排他写锁 实现。这样就避免了更新丢失,却可能出现脏读。也就是说事务B读取到了事务A未提交的数据。

1
2
通俗来说就是,事务A开始写,事务B不能写,只能读。可能出现脏读。
(读到未提交的数据)
  • Read committed (提交读 ==》 读已提交)

读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。该隔离级别避免了脏读,但是却可能出现不可重复读。事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

1
2
3
通俗来讲就是,读数据都可以,一旦有事务开始写,会禁止其他事务访问该行。
避免脏读,可能出现不可重复读。
(同一事务读取一行数据,两次结果不一致,后面一次读取时,数据已经发生改变)
  • Repeatable read (可重复读)

可重复读是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,即使第二个事务对数据进行修改,第一个事务两次读到的的数据是一样的。这样就发生了在一个事务内两次读到的数据是一样的,因此称为是可重复读。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。这样避免了不可重复读取和脏读,但是有时可能出现幻象读。(读取数据的事务)这可以通过“共享读锁”和“排他写锁”实现。

1
2
3
4
5
6
7
8
9
10
11
12
在一个事务内,多次读同一数据。在事务未提交前,另一事务也在访问同一数据,
在第一个事务两次读之间,即使第二个事务修改数据,第一个数据的两次读到的数据仍然是一致的。

避免了不可重复读和脏读。

读事务,禁止写(允许读事务)。
写事务,禁止任何其他事务。

可能出现幻读 eg:
1. 先更新某一列为从‘1’更新为‘2
2. 插入一行,该里值仍未‘1
3. 再次读取仍读到列值为‘1’的数据
  • Serializable (串行化)

提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。

隔离级别 脏读 不可重复读 幻读 第一类丢失更新 第二类丢失更新
读未提交 允许 允许 允许 不允许 允许
读已提交 不允许 允许 允许 不允许 允许
可重复读 不允许 不允许 允许 不允许 不允许
串行化 不允许 不允许 不允许 不允许 不允许