[数据库基础]事务及隔离级别的概念

Posted DBA成长之路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[数据库基础]事务及隔离级别的概念相关的知识,希望对你有一定的参考价值。

事务的概念

事务是数据库区别于文件系统的重要特性之一,当我们有了事务就会让数据库始终保持一致性,同时我们还能通过事务的机制恢复到某个时间点,这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失。事务保证了一次处理的完整性,也保证了数据库中的数据一致性。它是一种高级的数据处理方式,如果我们在增加、删除、修改的时候某一个环节出了错,它允许我们回滚还原。正是因为这个特点,事务非常适合应用在安全性高的场景里,比如金融行业等。

事务特性

事务的特性:要么完全执行,要么都不执行。不过要对事务进行更深一步的理解,还要从事务的 4 个特性说起,这 4 个特性用英文字母来表达就是 ACID。A,也就是 原子性(Atomicity)。原子的概念就是不可分割,你可以把它理解为组成物质的基本单位,也是我们进行数据处理操作的基本单位。C,就是 一致性(Consistency)。一致性指的就是数据库在进行事务操作后,会由原来的一致状态,变成另一种一致的状态。也就是说当事务提交后,或者当事务发生回滚后,数据库的完整性约束不能被破坏。一致性本身是由具体的业务定义的,也就是说,任何写入数据库中的数据都需要满足我们事先定义的约束规则。比如说,在数据表中我们将姓名字段设置为唯一性约束,这时当事务进行提交或者事务发生回滚的时候,如果数据表中的姓名非唯一,就破坏了事务的一致性要求。所以说,事务操作会让数据表的状态变成另一种一致的状态,如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。I,就是 隔离性(Isolation)。它指的是每个事务都是彼此独立的,不会受到其他事务的执行影响。也就是说一个事务在提交之前,对其他事务都是不可见的。最后一个 D,指的是 持久性(Durability)。事务提交之后对数据的修改是持久性的,即使在系统出故障的情况下,比如系统崩溃或者存储介质发生故障,数据的修改依然是有效的。因为当事务完成,数据库的日志就会被更新,这时可以通过日志,让系统恢复到最后一次成功的更新状态。持久性是通过事务日志来保证的。日志包括了回滚日志和重做日志。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。

隐式事务和显式事务

使用事务有两种方式,分别为隐式事务和显式事务。隐式事务实际上就是自动提交,Oracle 默认不自动提交,需要手写 COMMIT 命令,而 mysql 默认自动提交,当然我们可以配置 MySQL 的参数:

 
   
   
 
  1. set autocommit =0; --关闭自动提交

  2. set autocommit =1; --开启自动提交

当我们设置 autocommit=0 时,不论是否采用 START TRANSACTION 或者 BEGIN 的方式来开启事务,都需要用 COMMIT 进行提交,让事务生效,使用 ROLLBACK 对事务进行回滚。当我们设置 autocommit=1 时,每条 SQL 语句都会自动进行提交。不过这时,如果你采用 START TRANSACTION 或者 BEGIN 的方式来显式地开启事务,那么这个事务只有在 COMMIT 时才会生效,在 ROLLBACK 时才会回滚。

隔离级别

隔离性是事务的基本特性之一,它可以防止数据库在并发处理时出现数据不一致的情况。最严格的情况下,我们可以采用串行化的方式来执行每一个事务,这就意味着事务之间是相互独立的,不存在并发的情况。然而在实际生产环境下,考虑到随着用户量的增多,会存在大规模并发访问的情况,这就要求数据库有更高的吞吐能力,这个时候串行化的方式就无法满足数据库高并发访问的需求,我们还需要降低数据库的隔离标准,来换取事务之间的并发能力。隔离级别越低,意味着系统吞吐量(并发程度)越大,但同时也意味着出现异常问题的可能性会更大。在实际使用过程中我们往往需要在性能和正确性上进行权衡和取舍,没有完美的解决方案,只有适合与否。

 
   
   
 
  1. PostgreSQL:

  2. --查询当前数据库能支持的所有隔离级别

  3. select a.name,a.setting,a.short_desc,a.vartype from pg_settings a where a.name like '%isolation%';

  4. MySQL:

  5. --查看下当前会话的隔离级别

  6. SHOW VARIABLES LIKE 'transaction_isolation';

  7. --将隔离级别降到最低,设置为 READ UNCOMMITTED

  8. SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

官网定义

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

读已提交就是只能读到已经提交的内容,可以避免脏读的产生,属于 RDBMS 中常见的默认隔离级别(比如说 Oracle 和 SQL Server),但如果想要避免不可重复读或者幻读,就需要我们在 SQL 查询的时候编写带加锁的 SQL 语句(我会在进阶篇里讲加锁)。

可重复读,保证一个事务在相同查询条件下两次查询得到的数据结果是一致的,可以避免不可重复读和脏读,但无法避免幻读。MySQL 默认的隔离级别就是可重复读,oracle和postgresql的默认隔离级别是读已提交。

可串行化,将事务进行串行化,也就是在一个队列中按照顺序执行,可串行化是最高级别的隔离等级,可以解决事务读取中所有可能出现的异常情况,但是它牺牲了系统的并发性。在该隔离级别下,所有的select语句后都自动加上lock in share mode。因此,在该隔离级别下,无论你如何进行查询,都会使用next-key locks(间隙锁)。所有的select操作均为当前读!

脏读:检索操作出来的数据是不可靠的,是可以被另一个未提交的事务修改的!(即读到了其他事务还没有提交的数据)

不可重复读:一个查询语句检索数据,随后又有一个查询语句在同一个事务中检索数据,两个数据应该是一样的,但是实际情况返回了不同的结果。(同时被另一个正在提交的事务修改了)!ps:作者注,这里的不同结果,指的是在行不变的情况下(专业点说,主键索引没变),但是主键索引指向的磁盘上的数据内容变了。如果主键索引变了,比如新增一条数据或者删除一条数据,就不是不可重复读。

幻读:在一次查询的结果集里出现了某一行数据,但是该数据并未出现在更早的查询结果集里。例如,在一次事务里进行了两次查询,同时另一个事务插入某一行或更新某一行数据后(该数据符合查询语句里where后的条件),并提交了!(事务 A 根据条件查询得到了 N 条数据,但此时事务 B 更改或者增加了 M 条符合事务 A 查询条件的数据,这样当事务 A 再次进行查询的时候发现会有 N+M 条数据,产生了幻读。如果发生了脏读或者不可重复读,那么幻读是一定发生的。InnoDB默认用了REPEATABLE READ。在这种情况下,使用next-key locks解决幻读问题!通过上述定义,我们可以看出如果发生了脏读,那么不可重复读和幻读是一定发生的。因为拿脏读的现象,用不可重复读,幻读的定义也能解释的通。但是反过来,拿不可重复读的现象,用脏读的定义就不一定解释的通了!

因此,三者关系如下:

幻读的解决

 
   
   
 
  1. --在可重复读隔离级别下为快照读;串行化为当前读

  2. select * from tx_tb where pId >= 1;

  3. --在可重复读隔离级别下加上next-key locks,可解决幻读问题。将pId=1这条索引记录,和(1,++∞)这个间隙锁住了。其他事务要在这个间隙上插数据,就会阻塞,从而防止幻读发生!

  4. select * from tx_tb where pId >= 1 lock in share mode;

  5. --in share mode 子句的作用就是将查找到的数据加上一个 share 锁,这个就是表示其他的事务只能对这些数据进行简单的select 操作,并不能够进行 DML 操作。

如下图所示

mysql与postgresql的区别

在PostgreSQL事务隔离级别中,不支持READ UNCOMMITTED,只有三种隔离级别。PostgreSQL默认隔离级别是READ COMMTIED,而MySQL默认为REPEATABLE READ,另MySQL是有READ UNCOMMITTED这种级别的,不过一般这种事务隔离级别基本上没有什么用。MySQL 在REPEATABLE READ隔离级别下,普通的select语句是看不到在事务启动之后已经提交的数据,但select for update却能看到,也就是说普通select与select for update看到的结果是不一样的,这是特别需要让开发人员注意的地方。而PostgreSQL在REPEATABLE READ隔离级别,select for udpate语句和select语句的结果是一样的,都看不到在事务启动之后已经提交的数据。在MySQL中,一旦设置了SERIALIZABLE级别后,在事务中一旦查询表,就会把查询的记录都锁住,而PostgreSQL中的SERIALIZABLE也与MySQL中完全不一样。查询不会锁表。在PostgreSQL中,在窗口1中先开始一个事务,然后在窗口2中再开始一个事务,窗口2中修改id=3的记录,再到窗口1中修改id=3的记录时,会报错,由此实现了SERIALIZABLE的隔离级别。由此可见在SERIALIZABLE的级别上,PostGreSQL的并发会比MySQL好很多。

面试经典题

1:在Oracle,SqlServer中都是选择读已提交(Read Commited)作为默认的隔离级别,为什么Mysql不选择读已提交(Read Commited)作为默认隔离级别,而选择可重复读(Repeatable Read)作为默认的隔离级别呢?那Mysql在5.0这个版本以前,binlog只支持STATEMENT这种格式!而这种格式在读已提交(Read Commited)这个隔离级别下主从复制是有bug的,因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别!接下来,就要说说当binlog为STATEMENT格式,且隔离级别为读已提交(Read Commited)时,有什么bug呢?如下图所示,在主(master)上执行如下事务:

此时在主(master)上执行下列语句

 
   
   
 
  1. select * from test

  2. +---+

  3. | b |

  4. +---+

  5. | 3 |

  6. +---+

  7. 1 row in set

但是,你在此时在从(slave)上执行该语句,得出输出如下 Empty set 这样,你就出现了主从不一致性的问题!原因其实很简单,就是在master上执行的顺序为先删后插!而此时binlog为STATEMENT格式,它记录的顺序为先插后删(插入语句更先提交)!从(slave)同步的是binglog,因此从机执行的顺序和主机不一致!就会出现主从不一致!解决方案:(1)隔离级别设为可重复读(Repeatable Read),在该隔离级别下引入间隙锁。当Session 1执行delete语句时,会锁住间隙。那么,Ssession 2执行插入语句就会阻塞住!(2)将binglog的格式修改为row格式,此时是基于行的复制,自然就不会出现sql执行顺序不一样的问题!奈何这个格式在mysql5.1版本开始才引入。因此由于历史原因,mysql将默认的隔离级别设为可重复读(Repeatable Read),保证主从复制不出问题!

2:互联网项目中MySQL应该选什么事务隔离级别?读已提交。原因一:在RR(RR可重复读,RC读已提交)隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多;原因二:在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行;原因三:在RC隔离级别下,半一致性读(semi-consistent)特性增加了update操作的并发性!

3:可重复读(Repeatable Read)解决了幻读的问题?不是,通过间隙锁才解决

4:在可重复读级别下,不可重复读问题需要解决么?不用解决,这个问题是可以接受的!毕竟你数据都已经提交了,读出来本身就没有太大问题!Oracle的默认隔离级别就是RC,你们改过Oracle的默认隔离级别么?

5:在RC级别下,主从复制用什么binlog格式?在该隔离级别下,用的binlog为row格式,是基于行的复制!Innodb的创始人也是建议binlog使用该格式!

参考

https://time.geekbang.org/column/article/107143 https://time.geekbang.org/column/article/107401 https://mp.weixin.qq.com/s/_cTfULUQUK046Erc3gHpHg https://mp.weixin.qq.com/s/Xdsw8TzZEBti-ocQmUNJfg http://bbs.chinaunix.net/thread-4072034-1-1.html


以上是关于[数据库基础]事务及隔离级别的概念的主要内容,如果未能解决你的问题,请参考以下文章

事务的特性及事务的隔离级别(转)

MySQL事物隔离级别及搜索引擎

MySQL索引事务及存储引擎

MySQL索引事务及存储引擎

CRUD工程师——数据库事物ACID特性及隔离级别

数据库基本操作--------MySQL事务