Oracle数据库的数据一致性与事务隔离级别
1. 前言
在单用户环境下,在操作数据库是不需要考虑其他用户会修改同一个数据。但是在多用户的情况下,多个事务可能会修改同一个数据,最终会得到错误的数据结果。Oracle数据库是通过 multiversion consistency model
(多版本数据一致性模型)、还有不同类型的锁、事务隔离保证数据的一致性。
通过这种方式,数据库可以向多个并发用户提供在某一个时间点所对应的数据库数据。由于不同版本的数据块可以同时存在,事务可以查询所需时间点已经提交的数据版本,并返回对应时间点已提交的数据查询结果。(数据块的头部存储了历史事务信息)
那什么是数据一致性呢?
在数据库中,数据一致性是指在多个并发事务同时访问数据库时,确保读取操作的结果对于所有事务都是一致的。这意味着当一个事务正在读取数据时,如果其他事务正在对相同的数据进行修改或写入操作,读取操作不应该看到未完成的或部分更改的数据。相反,读取操作应该看到已经提交的、完整的数据,以确保事务之间的数据一致性。
2. 数据读一致性
数据的读一致性分为两个层面,一个是语句级别的读一致性,另外一个是事务级别的读一致性。
2.1 语句级别读一致性
Statement-Level Read Consistency
Oracle数据库强制使用语句级别的读一致性,这样保证每次返回的数据是在这个时间节点之前已经提交的数据。机制是怎么样的呢?举个例子来说明。
当进行一次查询时,本次查询发生在SCN 10023(关于SCN后面会进行说明,现在可以理解为,它是Oracle数据库的一种顺序标识,代表事务先后),只会看到10023之前已经提交的数据。在图中有两个大于SCN10023的数据块,是SCN1024,这是Oracle数据库会将这两个数据块拷贝到一个缓冲区中,然后根据undo segment(撤销段)中的数据重新构建这两个数据块(CR,consistent read clones)在1024前已经提交的数据,最后正确的检索就是SCN100021->SCN100021->SCN10006->SCN10006->SCN10021->SCN10011->SCN10021。通过这种机制每次查询的时候查询到的都是事务已经提交的数据,防止了脏读。
那什么是SCN呢?
SCN的全称是System Change Number,它在数据库中充当时钟的角色,是数据库内部使用的一个逻辑数据,Oracle数据库可以根据它的大小来判断事务发生的先后顺序。Oracle数据库是在SGA(Sytem Global Area)中完成SCN增量操作。当事务修改数据时,数据库会将新的SCN保存到与事务相对应的撤销段中(undo segment)中。
即使有了SCN机制,数据库能够知道事务的先后顺序,那数据库是怎么知道事务提交了还是没有提交?
每一个数据库块(block,Oracle数据库底层逻辑存储最小单元),都有一个数据块头(block header),里面包含了在数据库块上进行的事务活动(事务执行历史),里面记录了事务的状态(活跃 Active、提交 Commit、回滚 RollBack)。这些被记录的事务称为ITL(Interested Transaction List,我也不知道为什么要这么称呼,我的理解就是一组在这个数据库块上面执行过的事务以及它们的一些标识信息)。
不懂数据库块的可以大致翻一下前面一篇文章的介绍,这里就不再赘述了。
2.2 事务级别的读一致性
Transaction-Level Read Consistency
Oracle数据同样也可以对一个事务中的多条查询语句提供读一致性。在事务中的每条语句看到的都是同一节点(同一SCN)的已经提交的数据。
2.3 读一致性的数据存储
Oracle数据库是如何给每个不同SCN对应的事务看到之前已经提交的数据呢?这些数据又是放到那里呢?
要管理多版本读取一致性模型(multiversion read consistency model),数据库必须在同时查询和更新表时创建一组读取一致性数据。这里就涉及到了撤销段,撤销段(undo segment)是数据库的一个逻辑结构,用于管理extents(区),而extents是由多个数据库块(block)组成。
每当用户修改数据是,Oracle数据库都会生成一个撤销数据实体(undo entries),之后将这个实体写入到撤销段(undo segment)中。撤销段中包含已提交事务或者未提交事务所作更改之前,数据库中的旧数据。通过这种方式,同一个数据,在不同时间节点的不同版本数据可以存在于数据库中。这样数据库可以提供不同时间节点,不同版本的数据库快照视图,来保证读一致性。
2.4 锁机制
Oracle数据的某一条事务在修改数据时,其他事务不能在进行修改,实现这种操作就要使用到锁机制。
锁我的理解是它是一种机制,一种用于防止破坏性交互行为的机制,破坏性行为就是指并发修改某一个数据,修改后的结果是错误的,通过锁这种机制来避免错误的修改结果,保证数据库数据的一致性。
锁大致可以分为两类,一种是排他锁,另外一种是共享锁。多个资源在竞争锁时,只有一个资源能够获取到排他锁,多个资源可以获取到共享锁,只有当修改数据时,数据才会被锁定,正常情况下是锁定一行数据,而不是整张数据库表。当多个事务修改同一个数据时,通过锁可以防止共享数据被并发事务同时修改。需要注意的是select语句是不会被锁阻止的,它是被允许读取数据的,当然它读取的数据是撤销段所提供的数据,具体前文已经举例子进行了说明。
3. 事务隔离级别
Oracle数据库提供三种事务隔离级别分别是RC(read committed)、Serializable、read-only。
事务隔离级别标准:
Isolation Level | Dirty Read(脏读) | Nonrepeatable Read(不可重复读) | Phantom Read(幻读) |
---|---|---|---|
Read uncommitted | Possible(会发生) | Possible | Possible |
Read committed | Not possible(不会发生) | Possible | Possible |
Repeatable read | Not possible | Not possible | Possible |
Serializable | Not possible | Not possible | Not possible |
3.1 RC隔离级别
RC隔离级别是Oracle数据库默认的隔离级别,在RC隔离级别下可以保证每次查询到的数据都是事务已经提交的数据,不会读取到事务未提交的数据,保证数据的一致性。也就说当一个查询正在查询一个表中id为2的数据时,此时另外一个事务把该数据修改了,但是查询出的结果是修改之前的结果,不会查询到事务未提交的结果。
在RC隔离级别下,可能会出现更新丢失的问题。这是一个很诡异的现象。举个例子说一下:
假设某公司要给小张涨工资1k,现在有两个财务操作员A和B,它们进行了以下操作:
1.操作员A查询出了小张现有的工资,选择了薪水修改,并输入了要提薪的1000数据,但此时有事,临时走开了,没有提交(事务未提交)。
2.操作员B查询出小张现有的工资,选择了薪水修改,并输入了要提薪的1000数据,然后提交了修改。
3.操作员A忙完事情回来了,然后提交了修改。
小张高兴坏了,自己涨了两千的工资。
在这个例子中操作员A没有提交,但是在提交的时候要重新查询一下看一下薪水有没有被修改。这样不会造成操作员B的修改丢失。
比如可以通过乐观锁的形式,给表上添加版本列进行解决,版本列可以是日期,一个递增的版本数字等。这是一种比较好的方式,当然也存在其他方式。
3.2 Serializable隔离级别
Serializable隔离级别是Oracle数据库最高级的事务隔离级别,在这个级别下,数据库确保事务之间的操作不会相互干扰(行级锁定、多版本并发控制等),从而保证了数据的一致性和完整性。
在序列化事务隔离级别下,事务只能看到在事务开始时做的更改以及事务本身所做的更改。如果某个事务开始后,其他事务已经提交了对相同数据的修改,那么这个事务就会被视为尝试修改已经被其他事务更改的数据,此时Oracle数据库会抛出ORA-08177: Cannot serialize access for this transaction
异常。
3.3 Read-Only隔离级别
read-only事务隔离级别,是一个简单粗暴的事务隔离级别,它只允许事务进行读取数据,不允许修改数据,但是当用户是sys用户时将允许进行数据修改。
4. 总结
Oracle数据库实现读一致性需要事务、锁、MCVCC来共同完成,锁分为行级锁、事务锁等,事务隔离机制有RC、Serializable、Read-Only。 Oracle数据库通过需要从撤销段中重新构建数据(CR克隆)实现读取一致性。撤销段使用的是一种循环数据结构,事务所进行的删除、更新、插入操作的旧版本数据都存放在此处。当事务提交后撤销段所存储的这些数据就会被标记为可重用,供新的事务使用(新事务产生的数据会覆盖此处的旧数据),通过这种方式可以有效的管理存储空间,避免因撤销数据过度增长所带来的性能问题。