脏读 & 不可重复读 & 幻读

数据库并发事务可能出现脏读、重复读、幻读等问题

数据库并发事务可能会带来以下问题:

脏读 (Dirty Read)

脏读是指一个事务读取了另一个事务还未提交的数据。如果该事务回滚了,那么之前读取的数据就会变为无效,这种读取的情况就被称为脏读。

场景

  • 事务A读取了事务B修改但未提交的数据。如果事务B随后回滚,事务A读取到的数据就成了无效数据。
  • 例如,在一个电商系统中,事务A读取到某商品的库存为100件,而此时事务B将库存减少到90件但还未提交。如果事务B回滚,库存依然是100件,但事务A可能已经根据错误的90件库存做出决策。

解决方法

  • 设置事务的隔离级别为 Read Committed 或更高,这样每个事务只能读取到已经提交的数据。

不可重复读 (Non-repeatable Read)

不可重复读是指在一个事务中,多次读取同一行数据时,数据内容可能不一致。出现这种现象是因为在两个读取操作之间,另一个事务修改了该行数据并提交了。

场景

  • 事务A在某一时间点读取了某条记录。此时,事务B修改了这条记录并提交。当事务A再次读取该记录时,发现数据已经发生变化。
  • 例如,在银行系统中,事务A读取用户账户余额为1000元,随后事务B将余额改为1200元并提交,事务A再次读取余额时发现已经变成1200元。这会导致事务A在前后操作中的数据不一致。

解决方法

  • 设置事务的隔离级别为 Repeatable Read 或更高,这样可以保证在事务内多次读取同一行数据时数据的一致性。

幻读 (Phantom Read)

幻读是指在一个事务中,执行相同地查询语句两次时,第二次查询返回的结果集包含了第一次查询没有看到的新数据。这通常是因为在两个查询之间,另一个事务插入了新的数据行。

场景

  • 事务A第一次查询条件 WHERE age > 20,查询结果有10条记录。此时事务B插入了一条新的记录,使得 age > 20 的条件下多了一条新记录。当事务A再次执行相同的查询时,结果集变成了11条记录,出现了“幻觉”般的新数据。

解决方法

  • 设置事务的隔离级别为 Serializable,这种隔离级别会对读取的行进行锁定,防止其他事务插入新行。

隔离级别总结

  • Read Uncommitted:最低的隔离级别,可能出现脏读、不可重复读和幻读。
  • Read Committed:防止脏读,但可能出现不可重复读和幻读。大部分数据库默认使用此级别。
  • Repeatable Read:防止脏读和不可重复读,但可能出现幻读。
  • Serializable:最高的隔离级别,防止脏读、不可重复读和幻读,但会降低并发性能。

Read Uncommitted(读未提交)

特点:

这是最低的隔离级别,一个事务可以读取到其他事务尚未提交的数据。 可能发生的问题:脏读(Dirty Read)。 通常用于不要求数据一致性的场景,比如只需要快速查询数据而不在意数据的准确性。

Read Committed(读已提交)

特点:

事务只能读取其他事务已经提交的数据,避免了脏读。 可能发生的问题:不可重复读(Non-repeatable Read)。 这是大多数数据库系统的默认隔离级别,如Oracle。适用于需要避免脏读但可以容忍不可重复读的场景。

Repeatable Read(可重复读)

特点:

在事务开始后,该事务内的所有查询都只能看到事务开始时的数据状态,即使其他事务修改并提交了数据。 可能发生的问题:幻读(Phantom Read)。 用于需要避免不可重复读的场景,如一些金融交易系统。MySQL的InnoDB引擎默认使用这个隔离级别,并通过锁机制来避免幻读。

Serializable(可串行化)

特点:

这是最高的隔离级别。通过加锁和排序等方式,使得并发事务看起来像是按顺序执行的。 解决了脏读、不可重复读和幻读问题。

适用场景:

适用于要求最高数据一致性的场景,通常在银行转账等关键操作中使用。

选择隔离级别的考虑因素

  • 性能与一致性:隔离级别越高,数据一致性越好,但并发性能越差。
  • 具体业务需求:根据业务的实际需求权衡选择适合的隔离级别。