当不同的事务,对同一资源进行一些相关的操作的时候,彼此对公共数据的更改,会影响到其他事务对数据的操作,这样就导致了数据的不一致性。前两篇博客,介绍了一些事务的锁,这里侧重总结一下事务的隔离级别。
一、准备工作:
1、创建银行账号表:
CREATE TABLE dbo.BankAccount
(
BankAccountId CHAR(16) NOT NULL, -- 银行账号
UserName NVARCHAR(32) NOT NULL, -- 用户
Balance DECIMAL(19, 2) NOT NULL, -- 余额
LastUpdate SMALLDATETIME NOT NULL
)
GO
2、准备数据:
INSERT INTO dbo.BankAccount
VALUES ('9555500100071120', N'张三', 10000.00, GETDATE()) -- 北京分行账号
INSERT INTO dbo.BankAccount
VALUES ('9555507551227787', N'张三', 20000.00, GETDATE()) -- 深圳分行账号
GO
二、应用场景
假设张三在招商银行开设了两个账号,一个是招商银行北京分行,一个是招商银行深圳分行,两个账号的余额分别是10,000和20,000。
1、脏读: 一个事务读到另外一个事务还没有提交的数据,我们称之为脏读。
情景:张三在网上做了一笔交易,交易额100,买方小王通过银行汇款100到张三的北京分行的账号(见下面左图),柜台操作人员向张三账号存入100(事务一),然后系统些操作日志(假设需要10秒,WAITFOR DELAY '00:00:10')正在此时张三在ATM查了一下账号上余额(事务二),发现已经是10100,于是回去准备发货,但是事务一在写操作日志时超时,这是事务回滚,存款交易被取消,钱退给了小王,这样张三查到的账号余额事实上是事务一还没有提交的数据,导致张三错误的认为已经收到交易款项。
begin tran
update BankAccount
set Balance=Balance+100
where BankAccountId='9555500100071120'
select Balance from BankAccount
where BankAccountId='9555500100071120'
waitfor delay '00:00:10' --系统此时可能在做写日志等事情,导致延时。
rollback --事务回滚,导致其他线程对Balance的脏读()
select Balance
from BankAccount
where BankAccountId='9555500100071120'
另一个事务代码:
Begin tran
set tran isolation level read uncommitted --降低隔离级别后,容易导致脏读
select Balance
from BankAccount
where BankAccountId='9555500100071120'
commit
如才采用read uncommitted 隔离级别,会导致读数据的事务读到还没有执行完的事务中的数据,如果正在执行的其他事务是修改操作,那么Read cuncommitted级别的事务不会等待事务执行完,就读取数据。所以导致脏读。
解决方法:
设置读数据的事务的隔离级别为set tran isolation level read committed 。
Begin tran
set tran isolation level read committed --默认的事务隔离级别,避免脏读
select Balance
from BankAccount
where BankAccountId='9555500100071120'
commit
read committed隔离级别,指定要读数据的事务必须等待事务一执行完后,才可以执行。
2、不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同。
情景:张三先后两次查询某一账号的余额,在两次查询期间,小王完成了银行转账,导致两次的查询结果不同。
事务一:
begin tran
select Balance
from BankAccount
where BankAccountId='9555500100071120'
waitfor delay '00:00:10' --系统延时,模拟不可重复读
select Balance
from BankAccount
where BankAccountId='9555500100071120'
commit
begin tran
事务二:
begin tran
update BankAccount ---修改数据,导致另一个事务在一个事务中不可重复读
set Balance=Balance+100
where BankAccountId='9555500100071120'
commit
select Balance
from BankAccount
where BankAccountId='9555500100071120'
解决方法:
把事务一隔离级别调整到repeatable read。
begin tran
set tran isolation level repeatable read --设置事务的隔离级别为可重复读,其他事务需要等到这个事务执行完为止。解决读到的数据不一致的问题。
select Balance
from BankAccount
where BankAccountId='9555500100071120'
waitfor delay '00:00:10' --系统延时,模拟不可重复读
select Balance
from BankAccount
where BankAccountId='9555500100071120'
commit
事务二会一直等到事务一执行完毕再返回结果。
3、幻象读:一个事务先后读取一个范围的记录,但两次读取的纪录数不同。
情景:张三妻子先后两次查询张三招商银行所有账号的总余额,而在此期间张三在广州招商银行分行成功开设了一个账号,并存入5000,导致张三妻子两次查询的总余额不同,在此期间张三原有两个账号的余额均未发生改变
事务一:
begin tran
insert into BankAccount
values( '9555500100071000','N张三',5000.00, GETDATE())
select balance from BankAccount
where BankAccountId= '9555500100071000'
commit
事务二:
begin tran
set tran isloation level repeatable read
select SUM(balance) as Balance
from BankAccount
where UserName='N张三'
waitfor delay '00:00:05' --此处的延时期间,其他事务进行了操作,导致本事务两次读出的数据库不一致,称为幻读。
select SUM(balance) as Balance
from BankAccount
where UserName='balance'
commit
解决方法:
把事务隔离级别调整到serializable。set tran isloation level serializable
begin tran
set tran isloation level serializable --解决事务幻读的问题,其他事务会等待该事务执行完后才执行。
select SUM(balance) as Balance
from BankAccount
where UserName='N张三'
waitfor delay '00:00:05' --此处的延时,导致其他事务进行了操作,导致本事务两次读出的数据库不一致,称为幻读。
select SUM(balance) as Balance
from BankAccount
where UserName='balance'
commit
重复上面的动作会发现事务二会一直等到事务一执行完毕再返回结果。
三、总结:
在并发性的系统中,要控制好事务的隔离性。事务的隔离级别是通过数据库的锁控制的。
SQL默认的隔离级别是READ COMMITTED。
set tran isloation level serializable语句可以手动修改事务隔离级别。
参考链接:http://hi.baidu.com/liupras/item/ecf9003525f1644b3175a1e5