MySQL事务隔离级别的最佳实践(上)

事务

一组SQL语句组成的逻辑处理单元。

  • 原子性(Actomicity)
    事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行
  • 一致性(Consistent)

在事务开始和完成时,数据都必须保持一致状态

这意味着所有相关的数据规则都必须应用于事务的修改,以保持完整性

事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的

隔离性(Isolation)

一个事务所做的修改在最终提交前对其他事务不可见

持久性(Durability)

一旦事务提交,它对于数据的修改会持久化到DB

事务的问题

相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持可以支持更多的用户

但并发事务处理也会带来一些问题,主要包括以下几种情况

更新丢失(Lost Update)

当多个事务选择同一行,然后基于最初选定值更新该行时,由于事务隔离性,最后的更新覆盖了其他事务所做的更新.

例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改保存其更改副本的编辑人员覆盖另一个编辑人员所做的修改;

如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题

脏读(Dirty Reads)

一个事务正在对一条记录做修改,在该事务提交前,这条记录的数据就处于不一致状态。

这时,另一个事务也来读取同一条记录,读取了这些未提交的数据,例如: 早期版本、回滚。

不可重复读(Non-Repeatable Reads)

一个事务在读取某些数据,发现已经发生改变或某些记录已被删除。

不加锁时,其他事务的 UPDATE 或 DELETE 会影响查询结果。

幻读(Phantom Reads)

加锁后,不锁定间隙,其他事务可以 INSERT。

比如一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据。

很多人容易搞混不可重复读和幻读,这两者确实非常相似:

  • 不可重复读主要是多次读取一条记录,发现该记录中某些列值被修改过
  • 幻读主要是说多次读取一个范围内的记录(包括直接查询所有记录结果或做聚合统计), 发现结果不一致。一般指记录增多!记录的减少也算幻读!

事务并发下的异常

SQL-92标准中定义了如下异常:

  • 脏读(Dirty Read)
  • 不可重复读(Nnrepeatable Read)
  • 幻读(Phantom Read)
  • 比如说我们有个英雄表heros_temp,如下所示:


这张英雄表,我们会记录很多英雄的姓名,假设我们不对事务进行隔离操作,那么数据库在进行事务的并发处理时会出现怎样的情况?


第一天,小张访问数据库,正在进行事务操作,往里面写入一个新的英雄“吕布”:


SQL> BEGIN;

SQL> INSERT INTO heros_temp values(4, ‘吕布’);

当小张还没有提交该事务的时候,小李又对数据表进行了访问,他想看下这张英雄表里都有哪些英雄:


SQL> SELECT * FROM heros_temp;

这时,小李看到的结果如下:


你有没有发现什么异常?这个时候小张还没有提交事务,但是小李却读到了小张还没有提交的数据,这种现象我们称之为“脏读”。


那么什么是不可重复读呢?


第二天,小张想查看id=1的英雄是谁,于是他进行了SQL查询:


SQL> SELECT name FROM heros_temp WHERE id = 1;

运行结果:


然而此时,小李开始了一个事务操作,他对id=1的英雄姓名进行了修改,把原来的“张飞”改成了“张翼德”:


SQL> BEGIN;

SQL> UPDATE heros_temp SET name = ‘张翼德’ WHERE id = 1;

然后小张再一次进行查询,同样也是查看id=1的英雄是谁:


SQL> SELECT name FROM heros_temp WHERE id = 1;

运行结果:


这个时候你会发现,两次查询的结果并不一样。小张会想这是怎么回事呢?他明明刚执行了一次查询,马上又进行了一次查询,结果两次的查询结果不同。实际上小张遇到的情况我们称之为“不可重复读”,也就是同一条记录,两次读取的结果不同。


什么是幻读?


第三天,小张想要看下数据表里都有哪些英雄,他开始执行下面这条语句:


SQL> SELECT * FROM heros_temp;


这时当小张执行完之后,小李又开始了一个事务,往数据库里插入一个新的英雄“吕布”:


SQL> BEGIN;

SQL> INSERT INTO heros_temp values(4, ‘吕布’);

不巧的是,小张这时忘记了英雄都有哪些,又重新执行了一遍查询:


SQL> SELECT * FROM heros_temp;


他发现这一次查询多了一个英雄,原来只有3个,现在变成了4个。这种异常情况我们称之为“幻读”。


脏读:读到了其他事务还没有提交的数据。

不可重复读:对某数据进行读取,发现两次读取的结果不同,也就是说没有读到相同的内容。这是因为有其他事务对这个数据同时进行了修改或删除。

幻读:事务A根据条件查询得到了N条数据,但此时事务B更改或者增加了M条符合事务A查询条件的数据,这样当事务A再次进行查询的时候发现会有N+M条数据,产生了幻读。

事务隔离级别

在并发事务问题中,“更新丢失”应该完全避免,但防止更新丢失,并不能单靠数据库事务控制器,需需要应用程序对要更新的数据加必要的锁来解决。

因此,防止更新丢失应该是应用的责任。


脏读、不可重复读和幻读都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制。


数据库实现事务隔离的方式如下:


在读取数据前,对其加锁,防止其他事务对数据进行修改

不加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照,并用这个快照来提供一定级别(语句级或事务级)的一致性读取

从用户的角度,好像是数据库可以提供同一数据的多个版本,又称为数据多版本并发控制(MultiVersion Concurrency Control,MVCC)或多版本数据库

数据库的事务隔离级别越严格,并发副作用越小,但付出的代价也越大。因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”矛盾。


为解决“隔离”与“并发”的矛盾,ANSI SQL定义了4种隔离级别:

隔离级别/读数据一致性及允许的并发副作用 读数据一致性 脏读 不可重复读 幻读
未提交读(Read uncommitted) 最低级别,只能保证不读取物理上损坏的数据
已提交度(Read committed) 语句级
可重复读(Repeatable read) 事务级
可序列化(Serializable) *别,事务级

隔离性用于防止数据库在并发时的数据不一致。

最严格时,可用串行化执行每个事务,事务之间相互独立,不存在并发。

但是在实际生产环境下,考虑到随着用户量的增多,会存在大规模并发访问,要求数据库有更高吞吐量,这时串行化无法满足数据库高并发访问需求,还要降低隔离标准,换取事务间的并发力。


有时需牺牲一定的正确性换取效率的提升,即需要通过设置不同的隔离等级,以便在正确性和效率之间进行平衡。同时,随着RDBMS种类和应用场景的增多,数据库的设计者需要统一对数据库隔离级别进行定义,说明这些隔离标准都解决了哪些问题。

事务隔离级别

脏读、不可重复读和幻读这三种异常情况,是在SQL-92标准中定义的,同时SQL-92标准还定义了4种隔离级别来解决这些异常情况。

解决异常数量从少到多的顺序(比如读未提交可能存在3种异常,可串行化则不会存在这些异常)决定了隔离级别的高低,这四种隔离级别从低到高:

读未提交(READ UNCOMMITTED,RU )

允许读到未提交的数据,这种情况下查询是不会使用锁的,可能会产生脏读、不可重复读、幻读,不能保证一致性,很少使用。


读已提交(READ COMMITTED,RC)

只能读到已经提交的内容,可以避免脏读的产生。

如果想避免不可重复读或幻读,就需要在SQL查询时编写加锁的SQL语句。

特点

  • 每次查询都会设置和读取自己的新快照
  • 仅支持基于行的 bin-log
  • UPDATE 优化
  • 半一致读(semi-consistent read)
  • 不可重复读
  • 幻读(Phantom)
    加锁后,不锁定间隙,其他事务可以 INSERT

锁定索引记录,而不锁定记录之间的间隙。

可重复读(REPEATABLE READ)

InnoDB默认的隔离级别。

保证同一事务在相同查询条件下两次查询得到的数据结果是一致的,可以避免不可重复读和脏读,但无法避免幻读。

原理

  • 使用事务第一次读取时创建的快照
  • 多版本技术

使用的锁

  • 使用唯一索引的唯一查询条件时,只锁定查找到的索引记录,但不锁定间隙
  • 其他查询条件, 会锁定扫描到的索引范围,通过间隙锁或next-key锁阻止其他会话在这个范围插值

可能的问题

InnoDB 不能保证没有幻读,需要加锁。

上一篇:Swift 值类型和引用类型


下一篇:Android开发软件架构思考以及经验总结