暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【技术文章】ORA-01555快照过旧

云贝教育 2022-10-19
389

原理:如果我们有一个长时查询,比如需要执行10分钟,从9点整开始执行查询,到9点10分返回结果。


9点整的数据如下:

    select value from table where name=’a’

    name

    value

    a

    1

    如果这十分钟内有人修改了数据(不管有没有提交)

    name

    value

    a

    2


    但是我们上述的select查询结果仍然要是1,因为要保持一致性读。一致性读的意思是,执行查询的时刻数据是1,查询结束后,返回结果也要是1。

    那么数据库是怎么让我们能得到原始的结果呢?(即使有人在9:05修改了数据并提交)原因是我们使用undo段“缓存”历史数据(这里的缓存其实是写入磁盘的意思,而不是在内存中缓存),当9:05分,执行update后,会将原始数据写入undo段(段都在磁盘上)中,执行查询时,去undo段上找最新的数据,找到了value=2,然后再看一下,value=2的时间是否是对应9点整的,不是的话,找第二新的数据,……重复这段过程,最后找到了9点整的value=1数据。

    undo段也是有大小的,可记录的数据有限,也就是说,你重复找的次数不会超过某个值,假设针对某一行数据,undo段只缓存8个块的内容,一个块是默认8K,每一行假设是1K的数据,当你在9:00到9:10重复更新该行65次,undo段回滚查询不到第1次的数据了(因为只有8*8k=64K的”缓存“,“缓存”满之后,又执行了更新,就会把最早的数据覆盖),就会报错:快照过旧,因为找不到当时的数据,无法满足一致性读要求,查询失败。

    一般情况下,如果我们采用手动undo管理,undo段大小固定,则查询时会重用undo段,新的“缓存”会覆盖旧的。如果我们采用自动undo管理,则undo段快满时会自动扩展当前大小(是否在低负载的时候自动收缩未知),也就是说旧的“缓存”不会被清理,新的undo一直写入,按道理说,不会出现快照过旧错误,但是undo信息不可能从数据库第一次dml操作后,就一直记录,如果真的这么做,那么undo文件将比数据文件大几百上千倍。对于数据库层面,采用自动undo管理,通过参数undo_retention控制undo信息存留时间,这个值一般是900秒=15分钟。

    所以,如果一个sql语句查询超过15分钟,极有可能会出现快照过旧,当然可供分配undo段太小,即使是自动undo管理,可能都等不到15分钟,undo段用完了,开始进行覆盖写,马上就会出现快照过旧。

    (我们rollback回到原始数据,也是去undo段上找,然后回滚,所以undo段也叫回滚段,回滚delete,就是从undo段找原始数据做insert.回滚insert,就是找到原始数据,做delete,所以insert生成的undo信息最少,delete最多)。

    查看当前undo管理模式

    Auto表示自动undo管理。

    查询每个undo留存时间

    900单位是秒

    查看当前undo所在表空间

    查看表空间详细信息

    说明undo表空间对应的物理文件是有限制的

    表示undo表空间对应的数据文件可自动扩展

    查看是否发生过快照过旧错误以及对应的sql语句(该表字段参考链接里有)
      SELECT *
      FROM v$undostat u
      WHERE 1=1
      and u.ssolderrcnt > 0


      重现:接下来重现一下快照过旧错误

      我的测试环境是自动undo管理,undo_retention是15分钟,由于采用undo段自动扩展,且undo表空间大小为2147483645/1024/1024/1024=2G大小。

      正常情况下出现快照过旧有点难,我只能创建一个很小的undo段,且不支持自动扩展,然后循环修改数据,让undo段用完之后,开始覆盖最老的undo信息,为了维持一致性读,去循环找历史undo信息,找不到就报错。测试脚本如下,仅供参考。

        SQL> show parameters UNDO_
        NAME TYPE VALUE
        ------------------------------------ ----------- ------------------------------
        temp_undo_enabled boolean FALSE--用于临时表的UNDO写入临时表空间中,而不是UNDO表空间
        undo_management string AUTO
        undo_retention integer 900
        undo_tablespace string APPS_UNDO_TEST
          1.
          create undo tablespace apps_undo_test
          datafile '/usr/local/oracle19c/oradata/ORCL/APPS_UNDO_TEST_TABLESPACE.dbf'--此路径可能和你们测试环境不一致,请用dba_data_files查询应该放置在哪
          size 100k--实测最小100多k
          autoextend off;--禁止扩展,即使我的undo_retention是900秒,在非自动扩展的undo表空间,undo_retention设置失效


          2.
          show parameters UNDO_TABLESPACE--事先记录下当前的表空间是什么,方便实验结束恢复
          alter system set undo_tablespace =apps_undo_test;--设置为较小的undo表空间


          3.
          --查看undo数据文件信息
          SELECT SUM(bytes) / 1024 / 1024 || 'm'
          FROM dba_data_files
          WHERE tablespace_name = 'APPS_UNDO_TEST';




          4.
          创建索引,使得update的时候产生更多的undo,更好模拟快照过旧
          Create table dba_objects_test as select * from dba_objects;
          Create index dba_objects_test_n1 on dba_objects_test(object_id);
          CREATE OR REPLACE FUNCTION sleep_time(p_time IN NUMBER) RETURN NUMBER IS
          BEGIN


          dbms_lock.sleep(p_time);
          RETURN p_time;
          END;


          一个窗口下执行查询
          DECLARE
          CURSOR a_cur IS
          SELECT *
          FROM dba_objects_test t
          WHERE 1 = 1
          -- AND ROWID = 'AAAWVDAAFAAAAaTAAA'--这个注释去掉或者不去都会产生快照过旧错误,测试的时候请自行替换自己系统的ROWID
          AND sleep_time(30) = 30;--模拟长时间运行的SQL,这个游标30秒后返回数据,但取出来的数据还是30秒前的,所以假如你们测试系统不能自己创建undo表空间并切换表空间,可以尝试把这个 AND sleep_time(30) = 30 改成 AND sleep_time(1000) =1000
          i NUMBER := 0;
          BEGIN
          FOR a_rec IN a_cur
          LOOP
          NULL;
          END LOOP;
          END;


          --另一个窗口下执行修改
          DECLARE
          g_total_undo_size NUMBER := 0;


          CURSOR a_cur IS
          SELECT ROWID FROM dba_objects_test;


          PROCEDURE print_undo_size IS
          l_undo_size NUMBER;
          BEGIN
          BEGIN
          SELECT t.used_ublk * 8 --返回KB
          INTO l_undo_size
          FROM v$session     s
          ,v$transaction t
          WHERE s.saddr = t.ses_addr
          AND s.sid = userenv('SID');
          EXCEPTION
          WHEN OTHERS THEN
          l_undo_size := 0;
          END;
          g_total_undo_size := g_total_undo_size + l_undo_size;
          dbms_output.put_line(l_undo_size);
          EXCEPTION


          WHEN OTHERS THEN
          dbms_output.put_line(SQLERRM);
          END;


          BEGIN
          FOR a_rec IN a_cur
          LOOP
          COMMIT; --如果去掉这个commit,将会报错ora-01552或ORA-30036因为为了保证能够回滚,原有undo段是不能被覆盖的,而我们undo只有1m,又不能自动扩展
          --如果不能保证undo段全部被覆盖,那么不一定能产生快照过旧错误
          BEGIN
          --打印此事务产生的undo
          --  print_undo_size;--commit之后肯定是0,或者自行修改,获取所有的事务的undo
          UPDATE dba_objects_test
          SET object_id = dbms_random.value
          WHERE ROWID = a_rec.rowid;
          print_undo_size;


          EXCEPTION
          WHEN OTHERS THEN
          print_undo_size;
          dbms_output.put_line(SQLERRM);


          RETURN;
          END;


          END LOOP;
          dbms_output.put_line(g_total_undo_size);
          END;


          5.
          实验完毕恢复修改
          alter system set undo_tablespace =UNDOTBS1;--恢复原始的undo表空间,原始的表空间是什么请事先在command 中执行,show parameters UNDO_TABLESPACE,如果 恢复不了,按上面的步骤再创建一个自定义的undo表空间。

          不过写之前要意识到,不仅select语句会出现快照过旧,update+where,delete +where,还有insert+where,都是先查再增删改,因此都会出现快照过旧错误。


          还有一种触发ora-01555的情况可以自行搜索“延迟块清除”,因为比较少见,所以不做讨论。

          解决办法:

          1.最直接的方法,检查可用的undo段够不够用,是自动管理还是手动管理,是否支持扩展,增加数据文件max_size需要dba协助操作。

          2.增大undo_retention值,alter system set UNDO_RETENTION=9000;--150分钟或者设置更大的值,可以参考v$undosta表信息(https://docs.oracle.com/cd/B19306_01/server.102/b14237/dynviews_2174.htm#REFRN30295)

          3.优化你的查询,由于查询时间太长(一般是报表程序),导致该时间内,查询开始时刻undo信息已被覆盖,才会出现快照过旧错误。同时检查该时刻哪些程序在进行大量dml操作。所有的dml都是共用一个undo段,因此你批量改表B,也会导致查表A的时候发生这个错误。

          4.使用v$undostat,可以找到期间内运行时间最长的 SQL 语句的 SQL 标识符和标识期间内并发执行的最高事务数


          疑问点:

          这个一致性读到底是读的内存的undo block,还是磁盘上的undo segment。我做上面的试验是基于磁盘的undo segment的假设。所以即使没有刷新buffer cache,也能出现快照过旧,后续有时间通过dump段头信息继续分析。

          下图附上update的过程:1创建改变向量(undo vector,table vector),合并成改变record->2刷日志缓冲区(内存)->3修改undo块(磁盘)->4修改数据块(磁盘)

          参考链接

          https://docs.oracle.com/cd/B19306_01/server.102/b14237/initparams222.htm#REFRN10225
          https://docs.oracle.com/cd/B19306_01/server.102/b14237/dynviews_2174.htm#REFRN30295
          https://docs.oracle.com/cd/A57673_01/DOC/server/doc/SCN73/ch4.htm
          https://docs.oracle.com/cd/A57673_01/DOC/server/doc/SAD73/toc.htm#org
          http://www.dba-oracle.com/t_ora_01555_snapshot_old.htm


          文章转载自云贝教育,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

          评论