两个服务往同一个库里写数据,第一个服务先插入了id为1的数据但没有提交事务,第二个服务后插入id为2的数据并且提交了事务,这时候第三个服务去查询,查到了第二个服务id为2的数据。这时候根据第三个服务的业务逻辑,认为id<=2的数据都已经处理完了,下次就从id>2开始处理了,这样就漏掉了id为1的数据!
使用sql脚本复现了这种情况,下面提供测试脚本:
--建测试表
CREATE TABLE transactions(
[id] [int] IDENTITY(1,1) NOT NULL,
[insert_datetime] [datetime] NOT NULL
CONSTRAINT [pk_transactions] PRIMARY KEY CLUSTERED
(
[id] ASC
)) ON [PRIMARY]
GO
--模拟第一个服务的插入
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
Begin Transaction
Insert into transactions(insert_datetime) values(GETDATE())
--Commit Transaction
--模拟第二个服务的插入
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
Begin Transaction
Insert into transactions(insert_datetime) values(GETDATE())
Commit Transaction
--第三个服务,负责查询
--READ UNCOMMITTED非常不安全,这里仅作参照
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
SELECT * FROM transactions
GO
--READ COMMITTED是一般的数据访问层的默认隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
SELECT * FROM transactions
GO
--REPEATABLE READ提高的这个级别以上就能避免这个问题
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
SELECT * FROM transactions
GO
测试流程:
数据库工具为三个服务开启三个独立的窗口,这样它们就运行在不同的事务里。先执行第一个服务的插入,此时第一个服务没有Commit,再执行第二个服务的插入,第二个服务先Commit了。这个时候第三个服务来查询得到结果如图:
可以看到第一句查询把未提交的数据也查出来了,第二句是安全又效率的,但根据应用层的业务逻辑就把id为1的数据跳过了。第三句会一直等待第一个服务解锁(显示“正在执行查询…”)。当我们把第一个服务Commit,第三个服务的第三句查询也就能查出来了。