并发与多版本

并发与多版本


1.定义:

  • 并发控制(concurrency control):是数据库提供的函数集合,允许多个人同时访问和修改数据。

  • 多版本(multi-versioning):Oracle能同时物化多个版本的数据,提供数据读一致视图(read-consistent view)。它有一个很好的副作用,即写不会阻塞读。


2.事务隔离级别

  • ANSI/ISO SQL标准定义隔离级别所依据的3种现象:

    1> 脏读(dirty read):你能读取未提交的数据,也就是脏数据。

    2> 不可重复读(nonrepeatable read):如果你在T1时间读取某一行,在T2时间重新读取这一行时,这一行可能已经有所修改。也许它已经消失,有可能被更新了。

    3> 幻像读(phantom read):如果你在T1时间执行一个查询,而在T2时间再执行这个查询,此时可能已经向数据库中增加了另外的行,这会影响你的结果。与不可重复读的区别在于:在幻像读中,已经读取的数据不会改变,只是与以前相比,会有更多的数据满足你的查询条件。

  • ANSI/ISO SQL标准定义的4种隔离级别:

隔离级别

脏读

不可重复读

幻像读

READ UNCOMMITED

允许

允许

允许

REDA COMMITED


允许

允许

REPEATABLE READ



允许

SERIALIZABLE




  • Oracle明确地支持READ COMMITEDSERIALIZABLE隔离级别,并提供了另外一种隔离级别,即READ ONLY

  • READ UNCOMMITED

    1> READ UNCOMMITED隔离级别允许脏读。Oracle没有利用脏读,甚至不允许脏读。READ UNCOMMITED隔离级别的根本目标是提供一个基于标准的定义以支持非阻塞读。Oracle会默认的提供非阻塞读。

    2> Oracleundo段重新建立原数据,从而绕过未提交事务对数据所作的修改,它没有读修改后的值。

  • READ COMMITED

    1> READ COMMITED隔离级别是指,事务只能读取数据库中已经提交的数据。这里没有脏读,不过可能有不可重复读和幻像读。它是Oracle数据库的默认模式。

    2> 如果在一条语句中查询了多行,除了Oracle外,在几乎所有其他的数据库中,READ COMMITED隔离都可能“退化”得像脏读一样,这取决于具体的实现。

  • REPEATABLE READ

    1> REPEATABLE READ的目标是提供这样一个隔离级别,它不仅能给出一致的正确答案,还能避免丢失更新。

    2> 得到一致的答案

    1)大多数数据库(不包括Oracle)都通过使用低级的共享读锁来实现可重复读,而Oracle则采用更具并发性的多版本模型来提供读一致的答案。

    2)在一个采用共享读锁来提供可重复读的系统中,查询处理表中的行时,这些行都会锁定,导致两个副作用:1.数据的读取器会阻塞数据的写入器。不仅如此,数据的写入器还会阻塞数据读取器;2.数据的读取器和写入器可能而且经常相互死锁。

    3Oracle采用多版本,不会带来读阻塞写的现象,也不会导致死锁。

    3> 丢失更新:另一个可移植性问题

    1Oracle解决丢失更新的方法是悲观锁定和乐观锁定。

    2)在一个采用共享读锁的数据库中,应用认为REPEATABLE READ就意味着“丢失更新不可能发生”,等你把应用移植到没有使用共享读锁作为底层并发控制机制的数据库时,就会痛苦地发现与你预想的不一样。

  • SERIALIZABLE

    1> SERIALIZABLE事务在一个环境中操作时,就好像没有别的用户在修改数据库中的数据一样。原本通常在语句级得到的读一致性现在可以扩展到事务级。换句话说,Oracle使用回滚段按事务开始时数据的原样来重建数据,而不是按语句开始时的样子重建。

    2> 只要你试图更新某一行,而这一行自事务开始后已经修改,你就会得到这个错误:

    ORA-08117: can't serialize access for this transaction

    注:Oracle试图完全在行级得到这种隔离性,但是即使你想修改的行尚未被别人修改,也可能得到一个ORA-08117错误。发生ORA-08117错误的原因可能是:包含这一行的块上有其他行正在被修改。

    3> 如果使用SERIALIZABLE隔离级别,只要保证以下几点就能很有成效:

    1)一般没有其他人修改相同的数据;

    2)需要事务级读一致性;

    3)事务都很短(这有助于保证第一点)。

    4> 注意:SYS用户(或作为SYSDBA链接的用户)不能有READ ONLYSERIALIZABLE事务。

  • READ ONLY

    1> READ ONLY事务与SERIALIZABLE事务很相似,惟一的区别是READ ONLY事务不允许修改,因此不会遭遇ORA-08117错误。

    2> READ ONLY事务的目的是支持报告需求,即相对于某个时间点,报告的内容应该是一致的。在其他系统中为此要使用REPEATABLE READ,这就需要承受共享读锁的相关影响,在Oracle中则可以使用READ ONLY事务,就像对单语句一样,也使用了同样的多版本机制,根据需要从回滚段重新创建数据,并提供报告开始时数据的原样。

    3> READ ONLY事务中,可能会遇到ORA-1555: snapshot too old错误。原因:如果系统上有人正在修改你读取的数据,就会发生这种情况。对这个信息所做的修改(undo信息)将记录在回滚段中。但是回滚段以一种循环方式使用,这与重做日志非常相似。报告运行的时间越长,重建数据所需的undo信息就越有可能已经不在那里了。回滚段会回绕,你需要的那部分回滚段可能已经被另外某个事务占用了。此时,就会得到ORA-1555错误,只能从头再来。对于这个棘手的问题,惟一的解决方案就是为系统适当地确定回滚段的大小。


3.多版本读一致性的含义

  • 解释热表上超出期望的I/O

    1> 现象:生产环境中在一个大负载条件下,一个查询使用的 I/O 比你在测试或开发系统时观察到的 I/O 要多得多,而你无法解释这一现象。你查看查询执行的 I/O ,注意到它比你在开发系统中看到的 I/O 次数要多得多,多得简直不可想像。然后,你再在测试环境中恢复这个实例,却发现 I/O 又 降下来了。但是到了生产环境中,它又变得非常高(但是好像还有些变化:有时高,有时低,有时则处于中间)

    2> 原因:在你测试系统中,由于它是独立的,所以不必撤销事务修改。不过,在生产系统中,读一个给定的块时,可能必须撤销(回滚) 多个事务所做的修改,而且每个回滚都可能涉及 I/O 来获取 undo 信息并应用于系统。

    3> 例:

    create table t(x int);

    insert into t values(1);

    exec dbms_stats.gather_table_stats(user,'T');

    PL/SQL procedure successfully completed.

    select * from t;

    X

    ----------

    1

    alter session set isolation_level=serializable;

    Session altered.

    set autotrace on statistics

    select * from t;

    X

    ----------

    1

    Statistics

    ----------------------------------------------------------

    0 recursive calls

    0 db block gets

    7 consistent gets

    0 physical reads

    0 redo size

    415 bytes sent via SQL*Net to client

    420 bytes received via SQL*Net from client

    2 SQL*Net roundtrips to/from client

    0 sorts (memory)

    0 sorts (disk)

    1 rows processed

    在另一个会话中反复修改这个表

    begin

    for i in 1..10000

    loop

    update t set x = x + 1;

    commit;

    end loop;

    end;

    /

    再返回到前面的 SERIALIZABLE 会话,重新运行同样的查询:

    select * from t;

    X

    ----------

    1

    Statistics

    ----------------------------------------------------------

    0 recursive calls

    0 db block gets

    10002 consistent gets

    0 physical reads

    0 redo size

    415 bytes sent via SQL*Net to client

    420 bytes received via SQL*Net from client

    2 SQL*Net roundtrips to/from client

    0 sorts (memory)

    0 sorts (disk)

    1 rows processed

    分析:第一次只执行7I/O,而这一次执行了10002次,这是因为Oracle回滚了对该数据块的修改。在运行第二个查询时,Oracle知道查询获取和处理的所有块都必须针对事务开始的那个时刻。到达缓冲区缓存时,我们发现,缓存中的块“太新了”,另一个会话已经把这个块修改了10,000 次。查询无法看到这些修改,所以它开始查找 undo 信息,并撤销上一次所做的修改。它发现这个回滚块还是太新了,然后再对它做一次回滚。这个工作会复发进行,直至最后发现事务开始时的那个版本(即事务开始时数据库中的已提交块)。这才是我们可以使用的块,而且我们用的就是这个块。

    再次运行查询:

    select * from t;

    X

    ----------

    1

    Statistics

    ----------------------------------------------------------

    0 recursive calls

    0 db block gets

    7 consistent gets

    0 physical reads

    0 redo size

    415 bytes sent via SQL*Net to client

    420 bytes received via SQL*Net from client

    2 SQL*Net roundtrips to/from client

    0 sorts (memory)

    0 sorts (disk)

    1 rows processed

    可以看出,I/0再次下降到7,这是因为Oracle能把同一个块的多个版本保存在缓冲区缓存中。你撤销对这个块的修改时,也就把相应的版本留在缓存中了,这样以后执行查询时就可以直接查询。可以用以下查询查看这些块:

    select file#,block#,count(*)

    from v$bh

    group by file#,block#

    having count(*)>3

    order by 3

    /

    一般而言,你会发现在任何时间点上缓存中一个块的版本大约不超过6个。


4.写一致性

  • 一致读和当前读

    1> Oracle处理修改语句时会完成两类块获取。它会执行:

    1)一致读(Consistent read):“发现”要修改的行时,所完成的获取就是一致读。

    2)当前读(Current read):得到块来实际更新所要修改的行时,所完成的获取就是当前读。

    2> 考虑以下更新序列

    时间

    会话1

    会话2

    注释

    T1

    Update t set y=10 where y=5;


    这会更新与条件匹配的一行。

    T2


    Update t set x=x+1 where y=5;

    使用一致读,这会找到会话1修改的记录,但是无法更新这个记录,因为会话1已经将其阻塞。会话2将被阻塞,并等待这一行可用。

    T3

    Commit;


    这会解放会话2;会话2不再阻塞。它终于可以在包含这一行(会话1开始更新时y等于5的那一行)的块上完成当前读

    因此开始UPDATEY=5的记录不再是Y=5了。UPDATE的读一致部分指出:“你想更新这个记录,因为我们开始时Y=5”,但是根据块的当前版本,你会这样想:“噢,不行,我不能更新这一行,因为Y不再是5了,这可能不对。”如果我们此时只是跳过这个记录,并将其忽略,就会有一个不确定的更新。

    在这种情况下,Oracle会选择重启动更新。如果开始时Y=5的行现在包含值Y=10 , Oracle会悄悄地回滚更新,并重启动(假设使用READ COMMITTED隔离级别)。如果使用了SERIALIZABLE隔离级别,此时这个事务就会收到一个ORA-08177:can't serialize access错误。采用READ COMMITTED模式,事务回滚你的更新后,数据库会重启动更新(即修改更新相关的时间点),而且它并非重新更新数据,而是进入SELECT FOR UPDATE模式,并试图为你的会话锁住所有WHERE Y=5的行。一旦完成了这个锁定,它会对这些锁定的数据运行UPDATE,这样可以确保一次就能完成而不必(再次)重启动。

  • 查看重启动

    1> 例:

    首先创建测试表:

    SQL> create table t(x int,y int);

    Table created.

    SQL> insert into t values(1,1);

    1 row created.

    SQL> commit;

    Commit complete.

    为了观察重启动,创建一个BEFORE UPDAE FOR EACH ROW触发器:

    SQL> create or replace trigger t_bufer

    before update on t for each row

    begin

    dbms_output.put_line ('old.x=' || :old.x || ',old.y=' || :old.y);

    dbms_output.put_line( 'new.x=' || :new.x || ',new.y=' || :new.y);

    end;

    /

    Trigger created.

    更新一行:

    SQL> set serveroutput on

    SQL> update t set x = x+1;

    old.x=1,old.y=1

    new.x=2,new.y=1

    1 row updated.

    确实触发器每触发一次,都可以看到旧值和新值。要注意此时还没有提交,这一行仍被锁定,在另一个会话,执行以下更新将会被阻塞:

    SQL> set serveroutput on

    SQL> update t set x =x+1 where x > 0;

    此时回到第一个会话提交事务,在第二个会话就会得到以下输出:

    old.x=1,old.y=1

    new.x=2,new.y=1

    old.x=2,old.y=1

    new.x=3,new.y=1

    1 row updated.

    可以看到,行触发器看到这一行有两个版本。行触发器会触发两次:一次提供了行原来的版本以及我们想把原来这个版本修改成什么,另一次提供了最后实际更新的行。由于这是一个BEFORE FOR EACH ROW 触发器,Oracle 看到了记录的读一致版本,以及我们想对它做的修改。不过,Oracle 以当前模式获取块, 从而在 BEFORE FOR EACH ROW 触发器触发之后具体执行更新。它会等待触发器触发后再以当前模式得到块,因为触发器可能会修改:NEW 值。因此 Oracle 在触发器执行之前无法修改这个块。

    触发器触发后,Oracle 以当前模式获取这个块,并注意到用来查找这一行的 X 列已经修改过。由于使用了X来定位这条记录,而且X 已经修改,所以数据库决定重启动查询。注意,尽管X1更新到2,但这并不会使该行不满足条件(X>0) ;这条UPDATE 语句还是会更新这一行。而且,由于X 用于定位这一行, X 的一致读值(这里是1)不同于 X 的当前模式读值(2) ,所以在重启动查询时,触发器把值X=2(被另一个会话修改之后)看作是:OLD ,而把 X=3 看作是:NEW 值。

    注意:即使语句本身不一定导致重启动,触发器本身也可能导致发生重启动。例如下面使用WHERE Y>0来查找行:

    在第一个会话执行如下查询:

    SQL> update t set x = x+1 where y > 0;

    old.x=3,old.y=1

    new.x=4,new.y=1

    1 row updated.

    在第二个会话执行同样的查询,由于第一个未提交,故会被阻塞:

    SQL> update t set x = x+1 where y > 0;

    提交第一个会话,则第二个会话得到如下结果:

    old.x=3,old.y=1

    new.x=4,new.y=1

    old.x=4,old.y=1

    new.x=5,new.y=1

    1 row updated.

    说明:虽然我们搜索的是Y,且Y没有被修改,但仍导致了重启动,这是因为在触发器中引用:NEW.X:OLD.X时,会比较X的一致读值和当前读值,并发现二者不同。这就会带来一个重启动,即:WHERE子句中查找行所用的列集会与行触发器中引用的进行比较。行的一致读版本会与行的当前版本比较,只要有不同,就会重启动修改。如果从触发器将这一列的引用去掉后,就没有重启动了:

    SQL> create or replace trigger t_bufer

    before update on t for each row

    begin

    dbms_output.put_line ('fired');

    end;

    /

    SQL> update t set x = x+1;

    fired

    1 row updated.

    根据这些信息,可以进一步理解使用AFTER FOR EACH ROW触发器比使用BEFOR E FOR EACH ROW更高效。AFTER触发器不存在这些问题。

  • 为什么重启动对我们很重要?

    1> 如果在触发器中做任何非事务性的工作,就会受到重启动的影响。考虑以下影响:

    1)考虑一个触发器, 它维护着一些 PL/SQL 全局变量,如所处理的个数。重启动的语句回滚时, PL/SQL 变量的修改不会“回滚“。

    2)一般认为,UTL_开头的几乎所有函数(UTL_FILEUTL_HTTPUTL_SMTP )都会受到语句重启动的影响。语句重启动时,UTL_FILE 不会“取消“对所写文件的写操作。

    3)作为自治事务一部分的触发器肯定会受到影响。语句重启动并回滚时,自治事务无法回滚。

    2> 重启动影响性能

    例如:如果你开始一个很大的批更新,而且它处理了前 100,000 条记录后重启动了会怎么样?它会回滚前 100,000 行的修改,SELECT FOR UPDATE 模式重启动,在此之后再完成那 100,000 行的修改。

    你可能注意到了,放上那个简单的审计跟踪触发器后(即读取:NEW :OLD 值的触发器 ),即使除了增加了这些新触发器外什么也没有改变,但性能可能会突然差到你无法解释的地步。你可能在重启动过去从未用过的查询。或者你增加了一个小程序,它只更新某处的一行,却使过去只运行 1 个小时的批处理突然需要几个小时才能运行完,其原因只是重启动了过去从未发生过工作。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/17014649/viewspace-610485/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/17014649/viewspace-610485/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值