数据库事宜与隔离事例
大家都知道,数据 库 事 务 的四大特性 ACID ( Atomic, Consistency, Isolation, Durability ), 这 里主要考 虑 一致性和隔离性。 为 了提高事 务 的 处 理效率,通常并 发 的 执 行多个事 务 , 这 就是数据 库 中非常重要的 ‘ 并 发 控制 ’ 。 简单 说 ,并 发 的 执 行事 务 ,会有以下 问题 :
- 写 丢 失(Write Lost) :比如事 务 A 将x 的 值 更新 为 10 ,然后事 务 A 将y 的 值 更新 为 20 , 这时 A 重新 读 取x 发现 自己更新 过 的数据似乎不 见 了。
- 脏读 (Dirty Read) :比如事 务 A 的未提交( 还 依然 缓 存)的数据被事 务 B 读 走,如果事 务 A 失 败 回 滚 ,会 导 致事 务 B 所 读 取的的数据是 错误 的;
- 不可重复 读 (Non-repeatable Read) :比如事 务 A 中两 处读 取数据 total 的 值 。在第一 读 的 时 候, total 是 100 ,然后事 务 B 就把 total 的数据改成 200 ,事 务 A 再 读 一次, 结 果就 发现 , total 竟然就 变 成 200 了,造成事 务 A 数据混乱。
- 幻象 (Phantom Read) :和 Non-Repeatable Read 相似,也是同一个事 务 中多次 读 不一致的 问题 。但是 Non-Repeatable Read 的不一致是因 为 他所要取的数据集被改 变 了(比如 total 的数据),但是 Phantom Read 所要 读 的数据的不一致却不是他所要 读 的数据集改 变 ,而是他的条件数据集改 变 。比如 Select account.id where account.name="ppgogo*", 第一次 读 去了 6 个符合条件的 id ,第二次 读 取的 时 候,由于事 务 b 把一个 帐 号的名字由 "dd" 改 成 "ppgogo1" , 结 果取出来了 7 个数据。
这 四种 问题 呢,有些是可以忍的,有些解决后会大大滴降低并 发 性。所以, 为 了适 应 不同的需求,SQL'92 , 规 定了4 种隔离 级别 ,隔离 级别 越高,数据的一致性越好,数据 库 的并 发 性越低。
- Serializable (全序列化, 级别 3 )提供 严 格的事 务 隔离。它要求事 务 序列化 执 行,事 务 只能一个接着一个地 执 行,但不能并 发执 行。如果 仅仅 通 过 “ 行 级锁 ” 是无法 实现 事 务 序列化的,必 须 通 过 其他机制保 证 新插入的数据不会被 刚执 行 查询 操作的事 务访问 到。
- Repeatable Read ( 可重复 读 取, 级别 2 )禁止不可重复 读 取和 脏读 取,但是有 时 可能出 现 幻影数据。 这 可以通 过 “ 共享 读锁 ” 和“ 排他写 锁 ” 实现 。 读 取数据的事 务 将会禁止写事 务 (但允 许读 事 务 ),写事 务则 禁止任何其他事 务 。
- Read Committed (授 权读 取, 级别 1 ):允 许 不可重复 读 取,但不允 许脏读 取。 这 可以通 过 “ 瞬 间 共享 读锁 ” 和“ 排他写 锁 ” 实现 。 读 取数据的事 务 允 许 其他事 务继续访问该 行数据,但是未提交的写事 务 将会禁止其他事 务访问该 行。
- Read Uncommitted (未授 权读 取, 级别 0 ):允 许脏读 取,但不允 许 更新 丢 失。如果一个事 务 已 经 开始写数据, 则 另外一个数据 则 不允 许 同 时进 行写操作,但允 许 其他事 务读 此行数据。 该 隔离 级别 可以通 过 “ 排他写 锁 ” 实现 。
l 本示例文档演示 SQL SERVER , ORACLE 下不同事务隔离级别的区别,以及两种数据库本身的特点
l 为了模拟并发环境, SQL SERVER 在 SMO 程序中打开两个查询窗口即可。 oracle 可以用两个 sql *plus 程序连接到相同数据库来模拟
l SQL SERVER 、 ORACLE 中两个并发用户用事务 1 ,事务 2 简称。
l 所有测试例子,都以最初测试表脚本运行后状态为基准。
l 在下列例子中, set transaction isolation level 语句会改变会话的隔离级别,直到会话结束。故测试完毕需要改回默认级别。
l 最后,但并不是最不重要。以下的演示和相关解释,都是基于易于理解的原则来的,实际的情况可能更复杂,但对开发人员来说,理解如此程度的简化模型已经足够了。
测试表脚本:
SQL SERVER
CREATE TABLE [Customer](
[CustID] [int] NOT NULL,
[Fname] [nvarchar](20),
[Lname] [nvarchar](20),
[Address] [nvarchar](50),
[City] [nvarchar](20),
[State] [nchar](2) DEFAULT ('CA'),
[Zip] [nchar](5) NOT NULL,
[Phone] [nchar](10)
)
insert into customer values(1, 'Gary', 'Mckee', '111 Main', 'Palm Springs', 'CA', 94312, 7605551212)
insert into customer values(2, 'Tom', 'Smith', '609 Geogia', 'Fresno' 'JP', 33045, 5105551212)
insert into customer values(3, 'Jams', 'bond', 'ST Geogie 21', 'Washington', 'NY', 20331, 4405551864)
ORACLE
CREATE TABLE Customer(
CustID int NOT NULL,
Fname nvarchar2( 20),
Lname nvarchar2( 20),
Address nvarchar2( 50),
City nvarchar2( 20),
State nchar ( 2) DEFAULT 'CA' ,
Zip nchar ( 5) NOT NULL,
Phone nchar ( 10)
);
insert into customer values ( 1, 'Gary' , 'Mckee' , '111 Main' , 'Palm Springs' , 'CA' , 94312, 7605551212);
insert into customer values ( 2, 'Tom' , 'Smith' , '609 Geogia' , 'Fresno' , 'JP' , 33045, 5105551212);
insert into customer values ( 3, 'Jams' , 'bond' , 'ST Geogie 21' , 'Washington' , 'NY' , 20331, 4405551864);
1 。 Sqlserver 与 oracle 单条语句处理对比
SQL SERVER 单条语句默认自动提交,即单条语句自动作为一个事务处理;而 oracle 的原则是尽量延后提交,除非遇到显式提交命令或者 DDL 语句。
SQL SERVER
打开事务 1:
运行: select * from customer
可以看到表有 3 条记录
运行: insert into customer values(4, ' Hello ' , ' world ' , ' paradise road 01 ' , ' heaven ' , ' XY ' , 00001, 1234564321)
转到事务 2 :
运行: select * from customer
可以看到事务 1 中刚插入的 custid 为 4 的记录。
ORACLE
打开事务 1 ,运行:
select * from customer;
可以看到表有 3 条记录,运行:
insert into customer values(4, ' Hello ' , ' world ' , ' paradise road 01 ' , ' heaven ' , ' XY ' , 00001, 1234564321);
转到事务 2 ,运行:
select * from customer;
能看到的还是 3 条记录,事务 1 中刚插入的一条记录未自动提交,看不到。
转到事务 1 ,运行:
commit;
转到事务 2 ,运行:
select * from customer;
现在能看到 4 条记录了。
2. 丢失更新
Sqlserver 完全兼容 ANSI 92 标准定义的 4 个隔离级别。它的默认隔离级别是提交读( read committed ),在该级别下,可能会有丢失更新的问题。 Oracle 的默认情形也一样。故不再重复。
SQL SERVER
打开事务 1 运行:
set transaction isolation level read committed
begin tran
select * from customer -- 看到 3 条记录
现在切换到事务 2 ,此时事务 1 还未结束。在事务 2 中运行:
set transaction isolation level read committed
begin tran
select * from customer -- 看到 3 条记录,和事务 1 中相同
现在假设事务 1 事务继续运行,修改数据并提交:
update customer set state = 'TK' where CustID = 3
commit
回到事务 2 ,事务 2 根据先前查询到的结果修改数据:
update customer set Zip = 99999 where state = 'NY'
commit
结果因为事务 1 已经修改了事务 2 的 where 条件数据,事务 2 未成功修改数据(其实准确的说应该算是幻象读引起的更新失败。不过若满足条件的记录数多的话,事务 2 的 update 可能更新比预期的数量少的记录数,也可算“丢失”了部分本应完成的更新。个人认为只要明白实际上发生了什么即可,不必过分追究字眼)。丢失更新还可能有别的情形,比如事务 2 也是
update customer set state = 'KO' where CustID = 3
两个事务都结束后,事务 2 的结果反映到数据库中,但事务 1 的更新丢失了,事务 2 也不知道自己覆盖了事务 1 的更新。
3 .脏读演示
sqlserver 的默认隔离级别是提交读( read committed ),当手工将其改为未提交读时,事务可以读取其它事务没提交的数据; oracle 由于自身特殊实现机制,可以理解为自身基础的隔离级别就是可重复读(与 ANSI 标准还是有区别的,后面例子会说明)。
SQL SERVER
打开事务 1 ,运行:
begin tran
select * from customer
update customer set state = 'TN' where CustID = 3
转到事务 2 ,运行:
set transaction isolation level read uncommitted
begin tran
select * from customer
此时看到的数据是事务 1 已经更新但还未提交的( 3 号记录 state 值 TN )。而如果事务 1 发觉数据处理有误,转到事务 1 ,进行回滚:
Rollback
此时事务 2 如根据刚读取的数据进一步处理,会造成错误。它读