07 concurrency and Multi-version

本章提要
---------------------------------------------------------
对并发和锁的进一步补充
并发控制
事务的隔离级别
多版本控制读一致性的含义
写一致性
---------------------------------------------------------
开发多用户的数据库驱动的应用时, 最大的难题之一是, 一方面要力争最大的并发访问, 另一方面还要确保每个用户能以一致
方式读取和修改数据.

并发控制: 是数据库提供的函数集合, 允许多个人同时访问和修改数据.前一章曾经说过, 锁是管理共享数据库资源并发访问并
            防止并发数据库事务之间"相互干涉"的核心机制之一.
但是, oracle 对并发的支持部仅仅只是高效的锁定, 它还实现了一种多版本控制体系结构, 这种体系结构提供了一种受控但
高度并发的数据访问, 多版本控制是指, oracle能同时物化多个版本的数据, 这也是oracle提供数据读一致视图的机制(读一致
视图, 是指相对于某个时间点有一致的结果)所以:(只是读)
oracle 与其他数据库一个显著的不一样的地方是, 读不会被写阻塞.
默认情况下, oracle的读一致性多版本模型应用与语句级(statement level), 也就是说, 应用于每一个查询, 另外还可以应用与
事务级(transaction level), 这说明, 至少提交到数据库的每一条SQL语句都会看到数据库的一个读一致视图.

数据库中事务的基本作用是将数据库从一种一致状态转变为另一种一致状态.
sql 规定的现象:
1)脏读(dirty read): 别的session还没有commit的内容, 你也可以读取, 这肯定是不好的
2)不可重复读(nonrepeatable read): 意思是, 如果你在t1时刻读取某一行, 在t2时刻重新读取这一行时, 这一行可能已经有所修改,
    也许它已经消失.
3)幻像读(phantom read): 意思是, 你在t1时刻执行一个查询, 而在t2时刻再执行这个查询, 此时可能已经向数据库中增加了另外的行,
    这会影响你的结果, 与不可重复读的区别在于, 在幻象读中, 已经读取的数据不会改变, 只是与以前相比, 会有更多的数据满足
    你的查询条件.
通过以上3个现象, 个人感觉, 支持 不可重复读是好的, 其他两个都不怎么好...
通过以上sql规定的现象, 定义了以下的事务的隔离级别

read uncommited (脏读)
read committed 只有提交了才看的见 (oracle 支持)
repeatable read  可重复读, 可以用来防止丢失更新, 因为在读的时候其他数据库会加"共享锁".(其他数据库这样做,
                共享锁会防止其他会话修改我们已经读取的数据, 这肯定会降低并发性)
serializable 可串行化隔离级别 (oracle 支持), 提供了最高程度的隔离性.
下面详细介绍:

read committed:
oracle 提供的默认事务隔离级别是 read committed, 这很好, 与之相对应的是脏读, 脏读即没提交的事务的结果, 也可以被读取,
    脏读显然是不好的. read committed 是大多数据库采用的方法, 很少有采用其他方法的. 这种隔离级别可能存在不可重复读
    和幻象读, read committed 并不是很容易实现, 除了oracle意外,其他数据库都实现不好.

serializable (oracle支持)
serializable 事务(一个事务整体, 从开始到结束)在一个环境中操作时, 就好像没有别的用户在修改数据库一样, 我们读取所有行
    在重读取时都肯定完全一样, 所执行的查询在整个事务期间也总能返回相同的结果. 例如:
    select * from t;    // 时刻t1, 返回结果 1000 行
    begin dbms_lock.sleep(60*60*24); end;
    select * from t;    // 时刻t2, 相比于t1, 已经过去了1天, 甚至更长时间, 但是返回的结果也还是 1000 行
                        // 因为, t1 与 t2 这两个时刻虽然相隔很远, 但是都在一个事务中, 而根据 serializable特性
                        // 其他事务的修改对查询是不可见的.
    oracle中是这样实现serializable事务的, 原本通常在语句级得到的读一致性现在可以扩展到事务级.
    结果并非相对于语句开始的那个时间点一致, 而是在事务开始的那一刻就固定了, 换句话说, oracle 使用 undo 段按事务开始时
    数据的原样来重建数据, 而不是按语句开始时的样子重建. 这种隔离性是有代价的:
    那么, 如果你在 serializable 事务中修改了数据, 那么有可能得到 ora-08177错误, 如果你在使用 serializable 事务, 就
    不要指望它与其他事务一同更新同样的信息, 如果你确实要更新信息, 就应该使用select .. for update(这里说的是在
    serializable 事务隔离级别的情况下), 如果要使用 serializable 隔离级别, 要保证以下几点:
    1) 一般没有其他人修改相同的数据
    2) 需要事务读一致性
    3) 事务都很短(这有助于保证第一点)
    要记住, 如果隔离级别设置成 serializable, 事务开始之后, 你不会看到数据库中作出的任何修改, 直到提交为止.(正是因为
    你看不到其他事务的修改,而你在你自己事务中修改了某个数据, 而别的事务也修改了这个数据,那肯定会返回错误, 即便修改
    操作时其他session先, 你这个session后)
    
read only (只oracle 提供)
    另外, oracle 还提供了 read only 级别的事务隔离, 它的特性与 serializable 完全一样, 只是它的事务本身不准许修改.
    因为不准许修改, 所以这种模式不会得到 ora-08177 错误. 不过在 read only 事务隔离机制中, 可能会遇到 ora-1555错误,
    snapshot too old, 如果系统上有人正在修改你读取的数据, 就会发生这种错误, 对这个信息所做的修改(undo信息)将记录在
    undo段中, 但是undo段是循环方式使用的, 所以可能会被覆盖. 如果出现这个错误, 那只能重头再来. 对于这个错误, 只能
    适当调整 undo 段的大小.

多版本控制读一致性的含义
    一种会失败的常用数据仓库技术:
    1) 它们使用一个触发器维护源表中的一个 last_update 列.
    2) 最初要填充数据仓库表时, 它们要记住当前的时间, 为此会选择源系统上的 sysdate, 例如早上 9:00
    3) 然后它们从事务系统中拉出所有行, 这是一个完整的 select * from table查询, 可以得到最初填充的数据仓库
    4) 要刷新这个数据仓库, 它们要再次记住现在时间, 例如, 假设已经过去了1个小时, 现在源系统上的时间为10:00, 然后拉出
        自上午9:00(也就是第一次拉出数据之前的那个时刻)以来修改过的所有记录, 并把这些修改合并到数据仓库中.
    这个例子的问题时, 在拉出的那个时间点上, 假设上午 9:00 至少有一个打开的未提交的事务, 因为拉出数据时我们看不到对这一行做
    的修改, 而只能看到它的最后一个已提交的版本, 所以在上午9:00第一次拉数据期间我们读不到这一行的新版本, 上午10:00刷新
    我们也拉不出这行, 因为上午10:00的刷新只会拉出自那天早上上午9:00以后修改的记录.现在我们稍作修改,就可以实现这个逻辑:
    select nvl(min(to_date(start_time, ‘mm/dd/rr hh24:mi:ss‘)), sysdate)
      from v$transaction;    -- 这个视图能查到当前活动的事务(即已经开始,并未提交的事务, start_time这个例子中就定义为9:00)
    解释热表上超出期望的 I/O:
    你查看查询执行的I/O时(生产环境下), 注意到它比你在开发系统中看到的I/O次数多很多. 而你无法解释这种现象, 测试说明原因:
    在你测试系统中, 由于它是独立的, 所以不必撤销事务修改, 不过在生产系统中, 读一个给定的块时, 可能必须撤销(回滚)多个事务
    所做的修改, 而且每个回滚都可能涉及I/O来获取undo信息并应用与系统.
    可能只是要查询一个表, 但是这个表上发生了多个并发修改, 因此你看到oracle正在读undo段, 参考下例:

create table t(x int);

insert into t values(1);

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

select * from t;
-- 我们使用 serializable 事务级别, 这样, 无论在会话中运行多少次查询,
-- 得到事务开始时一样的结果
alter session set isolation_level = serializable;

set autotrace on statistics
select * from t;
-- 注意结果返回的 statistic: consistent gets 是 15

-- another session
begin
    for i in 1.. 10000
    loop
        update t set x = x+1;
        commit;
    end loop;
end;
/
-- 返回之前的 session, 重新运行
select * from t;
-- 现在得到了很高的 consistent gets 100000 以上

7-1

以上的 I/O 是从哪里来的呢? 这是因为 oracle 回滚了对该数据库块的修改(因为 serializable), 在运行第二个查询时, oracle知道
    查询获取和处理的所有块都必须针对事务开始的那个时刻, 到达缓冲区缓存时, 我们发现, 缓存中的块"太新了", 另一个会话已经把
    这个块修改了10000次, 查询无法看到这些修改, 所以它开始查找Undo信息, 并撤销上一次所做的修改. 它发现这个混滚块还是太新了,
    然后再对它做一次回滚, 这个工作会反复进行, 直至最后发现食物开始时的那个版本. 那么, 是不是只有使用serializable隔离级别时
    才会遇到这个问题呢? 不, 绝对不是, 可以考虑一个运行5分钟的查询, 在查询运行的这5分钟期间, 它从缓冲区缓存获取块, 每次从
    缓冲区缓存获取一个块时, 都会完成这样一个检查:"这个块是不是太新了? 如果是, 就将其回滚", 另外,要记住,查询运行时间越长,它
    需要的块在此期间被修改的可能性就越大.
    现在, 数据库希望进行这个检查(也就是说,查看块时不是"太新",并相应的回滚修改), 正是由于这个原因, 缓冲区缓存实际上可能在内存中
    包含同一个块的多个版本, 通过这种方式, 很有可能你需要的版本就在缓存中, 已经准备好, 正等着你用, 而无需使用 undo 信息进行物化,
    请看以下查询:
    select file#, block#, count(*)
    from v$bh
    group by file#, block#
    having count(*) > 3
    order by 3
    /
    可以使用这个查询看这些块,一般而言, 你会发现在任何时间点上缓存中一个块的版本大约不超过 6 个, 但是这些版本可以由需要它们的
    任何查询使用.
写一致性
    到此为止, 我们已经了解了读一致性: oracle 可以使用 undo 信息来提供非阻塞的查询和一致(正确)的读, 查询时, oracle会从缓冲区
    缓存中读出块, 它能保证这个块版本足够"旧", 能够被该查询看到.
    我们不能修改块的老版本, 修改一行时, 必须修改该块的当前版本.
    一致读 和 当前读
    oracle 处理修改语句时会完成两类块获取, 它会执行以下两部
    一致读(consistent read): "发现"要修改的行时, 锁完成的获取就是一致读.
    当前读(current read): 得到块来实际更新所要修改的行时, 锁完成的获取就是当前读.
    在一个正常查询中, 我们会遇到查询模式获取(一致读), 但是如果要修改, 更新模式(当前读)会获取得到现在的表块, 也就是包含待修改
    行的块, 得到一个undo段块来开始事务, 以及一个Undo块, (undo segment中, 磁盘文件), 既然存在当前模式获取, 这就说明发生了某种
    修改, 在oracle用新信息修改一个块之前, 它必须得到这个块的当前副本.
    那么读一致性对修改有什么影响呢? 参考下边例子:
    比如:你要执行一条 update t set x = x+1 where y = 5; 我们知道, 当你只是查询的时候, where y = 5部分(读一致模式来获取), 如果
    update 语句从开始到结束用5分钟来进行处理, 而有人在此期间向表中增加了另外1条 y=5的记录, 那么update看不到这个心增加的记录,
    因为一致读看不到新记录的. 这在预料之中, 也是正常的, 但问题是, 如果两个会话按顺序执行下面语句会发生什么情况?
    update t set y = 10 where y = 5;
    update t set x = x+1 where y = 5;
   
    因为开始 update y=5时, 已经将y 修改成10了, update的一致读部分指出"你想更新这个记录, 因为我们开始时 y = 5", 但是根据块当前
    版本(只能更新当前版本), 你会想"哦,这样不行, 我不能更新这一行(后面一个update), 因为 y 已经不是 5 了", 那么这个时候, oracle
    怎么办? 如果只是忽略, 显然不好, 弄的最后更新了没有都不清不楚, 在这种情况下, oracle会在当前session(最后面的session)
    选择重启动更新, 如果开始 y=5 的行现在包含值 y=10, oracle会悄悄地回滚更新(仅回滚更新, 不会混滚事务的任何其他不分),
    并重启动(假设使用的是 read commited隔离级别), 如果你使用了 serializable 隔离级别, 此时这个事务就会收到一个
    ORA-08177: can‘t serialize access for this transaction错误,使用 read committed 模式, 事务回滚你的更新后, 数据
    库会重启动更新(也就是说, 修改更新相关时间点), 而且它并非重新更新数据, 而是
    进入 select for update 模式, 并试图为你的会话锁住所有 where y=5的行, 一旦完成了这个锁定, 它会对这些锁定的数据运行update,
    这样可以确保这一次能完成而不必(再次)重启动.
    但是再想想, 如果重启动更新, 并进入 select for update 模式(与 update一样, 同样读一致获取)和读当前块获取, 开始select for update
    时 y=5 的一行等到你想得到它的当前版本准备修改时发现y=11, 会发生什么? select for update 一样也要重新启动, 再循环一次.
    个人理解, 对回滚更新表示怀疑, 请参考下例
    查看完后面的例子后, 知道是在最后的当前session下回滚执行了前边的Update操作, 但是这看上去好像跟个人理解的一样

/*
 * test 1
 */
-- init
create table tttt(
    x int,
    y int
)
/

insert into ttt values(1, 5);
-- session 1

select * from ttt;    -- result x=1, y=5

-- 上锁
update ttt
set y = 10
where y = 5;

-- session 2
select * from ttt;    -- result x=1, y=5, read committed 隔离级别

-- 阻塞
update ttt
set x = x+1
where y = 5;

-- 接下来
-- 1) session1 commit; 当前块的值为 x=1, y=10
-- 2) session2 的阻塞打开, 继续执行之前的update语句, 如果不满足执行条件
--        比如, y值已经变更, 那么表中肯定没有之前y值的行了, 所以执行结果
--        必然是 0 rows updated.
-- 综上, 以上的过程是, 上锁, 阻塞, 解开, 执行 等过程,没看到回滚 和重新启动 

7-p

演示查看重启动

drop table t;

create table t ( x int, y int );

insert into t values ( 1, 1 );

commit;

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;
 /

set serveroutput on

update t set x = x+1;

/* result session 1
    old.x = 1, old.y = 1
    new.x = 2, new.y = 1
 */

-- recently, we do not commit, so the line is locked now.

-- another session
set serveroutput on
update t set x = x+1 where x > 0;
-- 这时会立刻阻塞, 然后我们回到session1, commit session1 我们会得到以下:
-- 在 session2 中的结果
/* result session 2
    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行在 session2, 但是这个触发器被执行了 2 次.
-- 1次是原来的版本, 执行一次, 一次是后边的版本执行一次, oracle 会确认
-- read-consistent version of the record and modifications we would like to
-- have made to it.

7-2

为什么重启动对我们很重要?
    想想看, 如果你有一个触发器会做一些非事务性的事情, 这可能就是一个相当严重的问题, 例如, 考虑这样一个触发器, 它要发出一个更新(电子邮件)
    电子邮件正文是 "这是数据库以前的样子,它已经修改成现在的这个样子", 用户就会收到两个电子邮件.所以,考虑以下影响:
    1) 考虑一个触发器, 它维护着一些PL/SQL全局变量, 如所处理行的个数, 重启动的语句回滚时, 对PL/SQL变量的修改不会"回滚"
    2) 一般认为, 以 UTL_开头的几乎所有函数都会受到语句重启动的影响
    3) 作为自治事务一部分的触发器肯定会受到影响, 语句重启动并回滚时, 自治事务无法回滚.
    所有这些后果都要小心处理, 要想到对于每一行触发器可能会触发多次.

07 concurrency and Multi-version,布布扣,bubuko.com

时间: 2024-07-30 04:35:52

07 concurrency and Multi-version的相关文章

MySQL 温故而知新--Innodb存储引擎中的锁

近期碰到非常多锁问题.所以攻克了后,细致再去阅读了关于锁的书籍,整理例如以下:1,锁的种类 Innodb存储引擎实现了例如以下2种标准的行级锁: ? 共享锁(S lock),同意事务读取一行数据. ?  排它锁(X lock).同意事务删除或者更新一行数据. 当一个事务获取了行r的共享锁.那么另外一个事务也能够马上获取行r的共享锁,由于读取并未改变行r的数据.这样的情况就是锁兼容. 可是假设有事务想获得行r的排它锁,则它必须等待事务释放行r上的共享锁-这样的情况就是锁不兼容.二者兼容性例如以下表

Hekaton的神话与误解

最近这段时间,我花了很多时间来更好的理解Hekaton——SQL Sever 2014里的全新内存表技术.我看了很多文章,了解了Haktaon的各种内部数据存储结构(主要是哈希索引和Bw-tree).另外我也看了不少关于这方面的讲座. 但不止一次,有很多的误报,神话和误解出现,人们对Hektaton的认识发生了错误.从大家对Hekaton的提问就可以看出,我们需要整理Hekaton的知识,向大家重新传达它的相关知识,让大家更好的理解Hekaton,在Hekaton合适的场景来更好的使用它. 下面

SQL Server 内存中OLTP内部机制概述(二)

----------------------------我是分割线------------------------------- 本文翻译自微软白皮书<SQL Server In-Memory OLTP Internals Overview>:http://technet.microsoft.com/en-us/library/dn720242.aspx 译者水平有限,如有翻译不当之处,欢迎指正. ----------------------------我是分割线---------------

Mysql技术内幕——表&amp;索引算法和锁

表 4.1.innodb存储引擎表类型 innodb表类似oracle的IOT表(索引聚集表-indexorganized table),在innodb表中每张表都会有一个主键,如果在创建表时没有显示的定义主键则innodb如按照如下方式选择或者创建主键.首先表中是否有唯一非空索引(unique not null),如果有则该列即为主键.不符合上述条件,innodb存储引擎会自动创建一个6字节大小的指针,rowid(). 4.2.innodb逻辑存储结构 innodb的逻辑存储单元由大到小分别是

终极事务处理(XTP,Hekaton)——万能大招?

在SQL Server 2014里,微软引入了终极事务处理(Extreme Transaction Processing),即大家熟知的Hekaton.我在网上围观了一些文档,写这篇文章,希望可以让大家更好的理解Hekaton,它的局限性,还有它惊艳的全新内存数据库技术.这篇文章会通过下面几个方面来讲解Hekaton: 概况 可扩展性(Scalability) 局限性(Limitations) 1.概况 让我们从XTP的简洁概况开始.像XTP这样的内存数据库技术首要目标非常明确:尽可能高效的使用

MySQL锁的常见误区

今天给大家分享的内容是MySQL锁的常见误区.MySQL的锁包括两种lock和latch.latch的面向对象是线程,主要用来管理数据库临界资源的并发访问,锁的时间非常短,也不会产生死锁.不需要人工干预,所以这里我们不再做介绍.而lock则是面向事务的,操作的对象是数据库的表.页及行,用来管理并发线程对共享资源的访问,会产生死锁.因为我们现在数据库使用的是innodb存储引擎.所以今天主要给大家介绍的是innodb的lock的常见几个误区. 在介绍之前,我们需要再了解lock的几个概念: 行锁:

MySQL 马哥视频教程学习笔记

1.        关系型数据库 关系:由行和列组成的二维表 表:至少要有列,可以没有行. 列:是实体的属性. 数据模型:层次模型.网状模型.关系模型.非关系模型. DBMS:DataBase Mangenent System   数据库管理系统. 2.        数据库必须要有缓存  存放索引. 存储引擎:并发性好很好, 读,写: 读锁:共享锁 写锁:独占锁 减少锁粒度: 表锁:对整个表进行锁 页锁:对页和块进行锁 行锁:对行进行锁 数据库锁:对表进行加锁. 锁管理器: 数据库:mysql

蚂蚁技术专家:一篇文章带你学习分布式事务

小蚂蚁说: 分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,特别是在这几年越来越火的微服务架构中,几乎可以说是无法避免,本文就围绕分布式事务各方面与大家进行介绍. 一. 事务 1.1 什么是事务 数据库事务(简称:事务,Transaction)是指数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成. 事务拥有以下四个特性,习惯上被称为ACID特性: 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要

InnoDB高并发原理

一.并发控制 为啥要进行并发控制? 并发的任务对同一个临界资源进行操作,如果不采取措施,可能导致不一致,故必须进行并发控制(Concurrency Control). 技术上,通常如何进行并发控制? 通过并发控制保证数据一致性的常见手段有: 锁(Locking) 数据多版本(Multi Versioning) 二.锁 如何使用普通锁保证一致性? 普通锁,被使用最多: (1)操作数据前,锁住,实施互斥,不允许其他的并发任务操作: (2)操作完成后,释放锁,让其他任务执行: 如此这般,来保证一致性.