但当以上操作放到多线程中并发处理时会出现问题:某线程select了一条记录但还没来得及update时,另一个线程仍然可能会进来select到同一条记录。
一般解决办法就是使用锁和事物的联合机制:
1. 把select放在事务中, 否则select完成, 锁就释放了
2. 要阻止另一个select , 则要手工加锁, select 默认是共享锁, select之间的共享锁是不冲突的, 所以, 如果只是共享锁, 即使锁没有释放, 另一个select一样可以下共享锁, 从而select出数据
[sql] view plain copy
- BEGIN TRAN
- SELECT * FROM Table WITH(UPDLOCK)
- --或者 SELECT * FROM Table WITH(TABLOCKX, READPAST) 具体情况而定。
- UPDATE ....
- COMMIT TRAN
处理一个数据库死锁的异常时候,其中一个建议就是使用 NOLOCK 或者 READPAST 。有关 NOLOCK 和 READPAST的一些技术知识点:
对于非银行等严格要求事务的行业,搜索记录中出现或者不出现某条记录,都是在可容忍范围内,所以碰到死锁,应该首先考虑,我们业务逻辑是否能容忍出现或者不出现某些记录,而不是寻求对双方都加锁条件下如何解锁的问题。
NOLOCK 和 READPAST 都是处理查询、插入、删除等操作时候,如何应对锁住的数据记录。但是这时候一定要注意NOLOCK 和 READPAST的局限性,确认你的业务逻辑可以容忍这些记录的出现或者不出现:
简单来说:
1.NOLOCK 可能把没有提交事务的数据也显示出来
2.READPAST 会把被锁住的行不显示出来
不使用 NOLOCK 和 READPAST ,在 Select 操作时候则有可能报错误:事务(进程 ID **)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品。
SELECT * FROM Table WITH(NOLOCK)
SELECT * FROM Table WITH(READPAST)
实际开始动手用代码说话吧!
SQLServer2012在查询分析器里面开两个连接
插入锁:
结论:“表锁”锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select,所以Select如果不想等待就要在Select后加With(Nolock),但这样会产生脏数据就是其他事务已更新但并没有提交的数据,如果该事务进行了RollBack则取出的数据就是错误的,所以好自己权衡利弊,一般情况下90%以上的Select都允许脏读,只有账户金额相关的不允许。[sql] view plain copy
- ------------------A连接 Insert Lock-------------------
- BEGIN TRAN
- INSERT INTO dbo.UserInfo
- ( Name, Age, Mobile, AddTime, Type )
- VALUES ( 'eee', -- Name - varchar(50)
- 2, -- Age - int
- '555', -- Mobile - char(11)
- GETDATE(), -- AddTime - datetime
- 0 -- Type - int
- )
- SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
- WHERE request_session_id=@@SPID
- GROUP BY resource_type,request_mode
- --ROLLBACK TRAN
- ------------------------B连接 Insert Lock------------------------
- INSERT INTO dbo.UserInfo
- ( Name, Age, Mobile, AddTime, Type )
- VALUES ( 'fff', -- Name - varchar(50)
- 2, -- Age - int
- '123', -- Mobile - char(11)
- GETDATE(), -- AddTime - datetime
- 1 -- Type - int
- ) --可以执行插入
- SELECT * FROM dbo.UserInfo --需要等待解锁
- SELECT * FROM dbo.UserInfo WHERE Age=1 --需要等待解锁
- SELECT * FROM dbo.UserInfo WHERE Id=3 --可以执行查询(根据主键可以)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Age=1 --可以执行查询
- UPDATE dbo.UserInfo SET Type=5 WHERE Name='fff' --需要等待解锁
- DELETE FROM dbo.UserInfo WHERE Name='fff' --需要等待解锁
更新锁:
结论:“表锁”锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select[sql] view plain copy
- -----------------------A连接 Update Lock-----------------------
- BEGIN TRAN
- UPDATE dbo.UserInfo SET Name = 'eee' WHERE Age = 2
- SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
- WHERE request_session_id=@@SPID
- GROUP BY resource_type,request_mode
- --ROLLBACK TRAN
- ------------------------B连接 Update Lock------------------------
- INSERT INTO dbo.UserInfo
- ( Name, Age, Mobile, AddTime, Type )
- VALUES ( 'ppp', -- Name - varchar(50)
- 15, -- Age - int
- '666', -- Mobile - char(11)
- GETDATE(), -- AddTime - datetime
- 9 -- Type - int
- ) --可以执行插入
- SELECT * FROM dbo.UserInfo --需要等待解锁
- SELECT * FROM dbo.UserInfo WHERE Name='ppp' --需要等待解锁
- SELECT * FROM dbo.UserInfo WHERE Id=3 --可以执行查询(根据主键可以)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'ppp' --可以执行查询
- UPDATE dbo.UserInfo SET Age=8 WHERE Name='ccc' --需要等待解锁
- DELETE dbo.UserInfo WHERE Age = 5 --需要等待解锁
主键锁:
结论:“行锁+表锁” 锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select、Update、Delete[sql] view plain copy
- ------------------------A连接 Key Lock--------------------
- BEGIN TRAN
- UPDATE dbo.UserInfo SET Name='hhh' WHERE Id=3 --以主键为条件
- SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
- WHERE request_session_id=@@SPID
- GROUP BY resource_type,request_mode
- --ROLLBACK TRAN
- ------------------------B连接 Key Lock----------------------
- INSERT INTO dbo.UserInfo
- ( Name, Age, Mobile, AddTime, Type )
- VALUES ( 'kkk', -- Name - varchar(50)
- 18, -- Age - int
- '234', -- Mobile - char(11)
- GETDATE(), -- AddTime - datetime
- 7 -- Type - int
- ) --可以执行插入
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以执行查询
- -----//全表查询及操作正在处理的行
- SELECT * FROM dbo.UserInfo --需要等待解锁
- SELECT * FROM dbo.UserInfo WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
- UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
- DELETE dbo.UserInfo WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
- -----//使用非主键为条件的操作
- SELECT * FROM dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
- UPDATE dbo.UserInfo SET Name='ooo' WHERE Name='aaa' --需要等待解锁(非主键不可)
- DELETE dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
- -----//使用主键为条件的操作
- SELECT * FROM dbo.UserInfo WHERE id=1 --可以执行查询(根据主键可以)
- UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以执行更新(根据主键可以)
- DELETE dbo.UserInfo WHERE Id=1 --可以执行删除(根据主键可以)
索引锁:
结论:“行锁+表锁” 锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select、Update、Delete,也不影响以索引列Name为条件的Update、Delete但不可以Select[sql] view plain copy
- ------------------------A连接 Index Lock--------------------
- DROP INDEX dbo.UserInfo.Index_UserInfo_Name
- CREATE INDEX Index_UserInfo_Name ON dbo.UserInfo(Name)
- BEGIN TRAN
- UPDATE dbo.UserInfo SET age=66 WHERE Name='ddd' --使用name索引列为条件
- SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
- WHERE request_session_id=@@SPID
- GROUP BY resource_type,request_mode
- --ROLLBACK TRAN
- ----------------------B连接 Index Lock-------------------
- INSERT INTO dbo.UserInfo
- ( Name, Age, Mobile, AddTime, Type )
- VALUES ( 'iii', -- Name - varchar(50)
- 20, -- Age - int
- '235235235', -- Mobile - char(11)
- GETDATE(), -- AddTime - datetime
- 12 -- Type - int
- ) --可以执行插入
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事物中,有更新字段但还没有提交,此时就会查处脏数据)
- SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以执行查询
- -----//全表查询及操作正在处理的行
- SELECT * FROM dbo.UserInfo --需要等待解锁
- SELECT * FROM dbo.UserInfo WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
- UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
- DELETE dbo.UserInfo WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
- -----//使用非主键非索引为条件的操作
- SELECT * FROM dbo.UserInfo WHERE Age=5 --需要等待解锁(非主键不可)
- UPDATE dbo.UserInfo SET Name='ooo' WHERE Age=5 --需要等待解锁(非主键不可)
- DELETE dbo.UserInfo WHERE Age=5 --需要等待解锁(非主键不可)
- -----//使用主键为条件的操作
- SELECT * FROM dbo.UserInfo WHERE Id=1 --可以执行更新(根据主键可以)
- UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以执行更新(根据主键可以)
- DELETE dbo.UserInfo WHERE Id=1 --可以执行删除(根据主键可以)
- -----//使用索引为条件的操作
- SELECT * FROM dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
- UPDATE dbo.UserInfo SET Name='ooo' WHERE Name='aaa' --可以执行更新(根据索引可以)
- DELETE dbo.UserInfo WHERE Name='aaa' --可以执行删除(根据索引可以)
悲观锁(更新锁-人工手动设置上锁):
结论:可以理解为在使用版本控制软件的时候A迁出了一个文件,并且8i将这个87文件锁定,B就无法再迁出该文件了,直到A迁入解锁后才能被其他人迁出。[sql] view plain copy
- ------------------------A连接 Update Lock(悲观锁)---------------------
- BEGIN TRAN
- SELECT * FROM dbo.UserInfo WITH(UPDLOCK) WHERE Id=2
- SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
- WHERE request_session_id=@@SPID
- GROUP BY resource_type,request_mode
- --COMMIT TRAN
- --ROLLBACK TRAN
- ---------------------------B连接 Update Lock(悲观锁)-------------------------
- SELECT * FROM dbo.UserInfo --可以执行查询
- SELECT * FROM dbo.UserInfo WHERE id=2 --可以执行查询
- SELECT * FROM dbo.UserInfo WHERE Name='ooo' --可以执行查询
- UPDATE dbo.UserInfo SET Age=3 WHERE id=1 --可以执行更新(根据主键可以)
- UPDATE dbo.UserInfo SET Age=3 WHERE Name='ccc' --需要等待解锁(非主键不可)
- DELETE dbo.UserInfo WHERE id=1 --可以执行更新(根据主键可以)
- DELETE dbo.UserInfo WHERE name='ccc' --需要等待解锁(非主键不可)
乐观锁(人工通过逻辑在数据库中模拟锁)
结论:可以理解为同样在使用版本控制软件的时候A迁出了一个文件,B也可以迁出该文件,两个人都可以对此文件进行修改,其中一个人先进行提交的时候,版本并没有变化所以可以正常提交,另一个后提交的时候,发现版本增加不对称了,就提示冲突由用户来选择如何进行合并再重新进行提交。[sql] view plain copy
- --------------------------A客户端连接 Lock(乐观锁)------------------------
- --DROP TABLE Coupon
- -----------------创建优惠券表-----------------
- CREATE TABLE Coupon
- (
- Id INT PRIMARY KEY IDENTITY(1,1),
- Number VARCHAR(50) NOT NULL,
- [User] VARCHAR(50),
- UseTime DATETIME,
- IsFlag BIT DEFAULT(0) NOT NULL,
- CreateTime DATETIME DEFAULT(GETDATE()) NOT NULL
- )
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000001')
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000002')
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000003')
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000004')
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000005')
- INSERT INTO dbo.Coupon(Number) VALUES ( '10000006')
- --SELECT * FROM dbo.Coupon WITH(NOLOCK) --查询数据
- --UPDATE Coupon SET [User]=NULL, UseTime=NULL, IsFlag=0 --还原数据
- -----------------1、模拟高并发普通更新-----------------
- DECLARE @User VARCHAR(50) --模拟要使用优惠券的用户
- DECLARE @TempId INT --模拟抽选出来的要使用的优惠券
- SET @User='a'
- BEGIN TRAN
- SELECT @TempId=Id FROM dbo.Coupon WHERE IsFlag=0 --高并发时此语句有可能另外一个该事务已取出的Id
- --WAITFOR DELAY '00:00:05' --改用此方式要开两个SQL Management客户端
- UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId
- COMMIT TRAN
- --ROLLBACK TRAN
- -----------------2、悲观锁解决方案-----------------
- DECLARE @User VARCHAR(50) --模拟要使用优惠券的用户
- DECLARE @TempId INT --模拟抽选出来的要使用的优惠券
- SET @User='a'
- BEGIN TRAN
- SELECT @TempId=Id FROM dbo.Coupon WITH(UPDLOCK) WHERE IsFlag=0 --高并发时此语句会锁定取出的Id数据行
- --WAITFOR DELAY '00:00:05' --改用此方式要开两个SQL Management客户端
- UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId
- COMMIT TRAN
- --ROLLBACK TRAN
- -----------------3、乐观锁解决方案-----------------
- ALTER TABLE dbo.Coupon ADD RowVer ROWVERSION NOT NULL --增加数据行版本戳类型字段(微软新推荐数据字段,该字段每张表只能有一个,会在创建行或更新行时自动进行修改无需人为干涉,该字段不能建立索引及主键因为会频繁修改)
- DECLARE @User VARCHAR(50) --模拟要使用优惠券的用户
- DECLARE @TempId INT --模拟抽选出来的要使用的优惠券
- DECLARE @RowVer BINARY(8) --抽选出来的优惠券的版本(ROWVERSION数据类型存储大小为8字节)
- SET @User='a'
- BEGIN TRY
- BEGIN TRAN
- SELECT @TempId=Id, @RowVer=RowVer FROM dbo.Coupon WHERE IsFlag=0 --取出可用的Id及对应的版本戳
- --WAITFOR DELAY '00:00:05' --改用此方式要开两个SQL Management客户端
- UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId ANDRowVer=@RowVer
- IF(@@ROWCOUNT > 0)
- BEGIN
- PRINT('修改成功')
- COMMIT TRAN
- END
- ELSE
- BEGIN
- PRINT('该数据已被其他用户修改')
- ROLLBACK TRAN
- END
- END TRY
- BEGIN CATCH
- ROLLBACK TRAN
- END CATCH
- --------------------------B客户端连接 Lock(乐观锁)------------------------
- --此测试需要开两个SQL Management Studio客户端,在A客户端使用WAITFOR DELAY来模拟并发占用,在B客户端执行与A客户端相同的SQL脚本即可(注释掉WAITFOR),所以在此不放相同代码了。
我认为如果同一张表的并发很高,但并发处理同一条数据的冲突几率很低,那就应该使用乐观锁,反之,如果同一张表的并发不高,但同时处理同一条数据的几率很高,就应该使用悲观锁。