• 精选
  • 会员

三 SQL Server 锁机制 悲观锁 乐观锁 实测解析

2018年7月19日  来源: 作者: 提供人:anchi......
在使用SQL时,大都会遇到这样的问题,你Update一条记录时,需要通过Select来检索出其值或条件,然后在通过这个值来执行修改操作。
但当以上操作放到多线程中并发处理时会出现问题:某线程select了一条记录但还没来得及update时,另一个线程仍然可能会进来select到同一条记录。
 一般解决办法就是使用锁和事物的联合机制:
1. 把select放在事务中, 否则select完成, 锁就释放了
2. 要阻止另一个select , 则要手工加锁, select 默认是共享锁, select之间的共享锁是不冲突的, 所以, 如果只是共享锁, 即使锁没有释放, 另一个select一样可以下共享锁, 从而select出数据 
[sql] view plain copy
  1. BEGIN TRAN
  2. SELECT * FROM Table WITH(UPDLOCK)
  3. --或者 SELECT * FROM Table WITH(TABLOCKX, READPAST) 具体情况而定。
  4. UPDATE ....
  5. COMMIT TRAN
所有Select加 With (NoLock)解决阻塞死锁,在查询语句中使用 NOLOCK 和 READPAST 
处理一个数据库死锁的异常时候,其中一个建议就是使用 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
  1. ------------------A连接 Insert Lock-------------------
  2. BEGIN TRAN
  3. INSERT INTO dbo.UserInfo
  4. Name, Age, Mobile, AddTime, Type )
  5. VALUES ( 'eee'-- Name - varchar(50)
  6. 2, -- Age - int
  7. '555'-- Mobile - char(11)
  8. GETDATE(), -- AddTime - datetime
  9. -- Type - int
  10. )
  11. SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
  12. WHERE request_session_id=@@SPID
  13. GROUP BY resource_type,request_mode
  14. --ROLLBACK TRAN
  15. ------------------------B连接 Insert Lock------------------------
  16. INSERT INTO dbo.UserInfo
  17. Name, Age, Mobile, AddTime, Type )
  18. VALUES ( 'fff'-- Name - varchar(50)
  19. 2, -- Age - int
  20. '123'-- Mobile - char(11)
  21. GETDATE(), -- AddTime - datetime
  22. -- Type - int
  23. --可以执行插入
  24. SELECT * FROM dbo.UserInfo --需要等待解锁
  25. SELECT * FROM dbo.UserInfo WHERE Age=1 --需要等待解锁
  26. SELECT * FROM dbo.UserInfo WHERE Id=3 --可以执行查询(根据主键可以)
  27. SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
  28. SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Age=1 --可以执行查询
  29. UPDATE dbo.UserInfo SET Type=5 WHERE Name='fff' --需要等待解锁
  30. DELETE FROM dbo.UserInfo WHERE Name='fff' --需要等待解锁

更新锁:

结论:“表锁”锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select
[sql] view plain copy
  1. -----------------------A连接 Update Lock-----------------------
  2. BEGIN TRAN
  3. UPDATE dbo.UserInfo SET Name = 'eee' WHERE Age = 2
  4. SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
  5. WHERE request_session_id=@@SPID
  6. GROUP BY resource_type,request_mode
  7. --ROLLBACK TRAN
  8. ------------------------B连接 Update Lock------------------------
  9. INSERT INTO dbo.UserInfo
  10. Name, Age, Mobile, AddTime, Type )
  11. VALUES ( 'ppp'-- Name - varchar(50)
  12. 15, -- Age - int
  13. '666'-- Mobile - char(11)
  14. GETDATE(), -- AddTime - datetime
  15. -- Type - int
  16. --可以执行插入
  17. SELECT * FROM dbo.UserInfo --需要等待解锁
  18. SELECT * FROM dbo.UserInfo WHERE Name='ppp' --需要等待解锁
  19. SELECT * FROM dbo.UserInfo WHERE Id=3 --可以执行查询(根据主键可以)
  20. SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
  21. SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'ppp' --可以执行查询
  22. UPDATE dbo.UserInfo SET Age=8 WHERE Name='ccc' --需要等待解锁
  23. DELETE dbo.UserInfo WHERE Age = 5 --需要等待解锁

主键锁:

结论:“行锁+表锁” 锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select、Update、Delete
[sql] view plain copy
  1. ------------------------A连接 Key Lock--------------------
  2. BEGIN TRAN
  3. UPDATE dbo.UserInfo SET Name='hhh' WHERE Id=3 --以主键为条件
  4. SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
  5. WHERE request_session_id=@@SPID
  6. GROUP BY resource_type,request_mode
  7. --ROLLBACK TRAN
  8. ------------------------B连接 Key Lock----------------------
  9. INSERT INTO dbo.UserInfo
  10. Name, Age, Mobile, AddTime, Type )
  11. VALUES ( 'kkk'-- Name - varchar(50)
  12. 18, -- Age - int
  13. '234'-- Mobile - char(11)
  14. GETDATE(), -- AddTime - datetime
  15. -- Type - int
  16. --可以执行插入
  17. SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事务中,有更新字段但还没有提交,此时就会查处脏数据)
  18. SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以执行查询
  19. -----//全表查询及操作正在处理的行
  20. SELECT * FROM dbo.UserInfo --需要等待解锁
  21. SELECT * FROM dbo.UserInfo WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
  22. UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
  23. DELETE dbo.UserInfo WHERE Id=3 --需要等待解锁(根据主键,但与A连接操作相同行不可)
  24. -----//使用非主键为条件的操作
  25. SELECT * FROM dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
  26. UPDATE dbo.UserInfo SET Name='ooo' WHERE Name='aaa' --需要等待解锁(非主键不可)
  27. DELETE dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
  28. -----//使用主键为条件的操作
  29. SELECT * FROM dbo.UserInfo WHERE id=1 --可以执行查询(根据主键可以)
  30. UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以执行更新(根据主键可以)
  31. DELETE dbo.UserInfo WHERE Id=1 --可以执行删除(根据主键可以)

索引锁:

结论:“行锁+表锁” 锁定对该表的Select、Update、Delete操作,但不影响对该表的Insert操作也不影响以主键Id为条件的Select、Update、Delete,也不影响以索引列Name为条件的Update、Delete但不可以Select
[sql] view plain copy
  1. ------------------------A连接 Index Lock--------------------
  2. DROP INDEX dbo.UserInfo.Index_UserInfo_Name
  3. CREATE INDEX Index_UserInfo_Name ON dbo.UserInfo(Name)
  4. BEGIN TRAN
  5. UPDATE dbo.UserInfo SET age=66 WHERE Name='ddd' --使用name索引列为条件
  6. SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
  7. WHERE request_session_id=@@SPID
  8. GROUP BY resource_type,request_mode
  9. --ROLLBACK TRAN
  10. ----------------------B连接 Index Lock-------------------
  11. INSERT INTO dbo.UserInfo
  12. Name, Age, Mobile, AddTime, Type )
  13. VALUES ( 'iii'-- Name - varchar(50)
  14. 20, -- Age - int
  15. '235235235'-- Mobile - char(11)
  16. GETDATE(), -- AddTime - datetime
  17. 12 -- Type - int
  18. --可以执行插入
  19. SELECT * FROM dbo.UserInfo WITH(NOLOCK) --可以执行查询(在一个事物中,有更新字段但还没有提交,此时就会查处脏数据)
  20. SELECT * FROM dbo.UserInfo WITH(NOLOCK) WHERE Name = 'kkk' --可以执行查询
  21. -----//全表查询及操作正在处理的行
  22. SELECT * FROM dbo.UserInfo --需要等待解锁
  23. SELECT * FROM dbo.UserInfo WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
  24. UPDATE dbo.UserInfo SET Name='mmm' WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
  25. DELETE dbo.UserInfo WHERE Id=4 --需要等待解锁(根据主键,但与A连接操作相同行不可)
  26. -----//使用非主键非索引为条件的操作
  27. SELECT * FROM dbo.UserInfo WHERE Age=5 --需要等待解锁(非主键不可)
  28. UPDATE dbo.UserInfo SET Name='ooo' WHERE Age=5 --需要等待解锁(非主键不可)
  29. DELETE dbo.UserInfo WHERE Age=5 --需要等待解锁(非主键不可)
  30. -----//使用主键为条件的操作
  31. SELECT * FROM dbo.UserInfo WHERE Id=1 --可以执行更新(根据主键可以)
  32. UPDATE dbo.UserInfo SET Name='yyy' WHERE Id=1 --可以执行更新(根据主键可以)
  33. DELETE dbo.UserInfo WHERE Id=1 --可以执行删除(根据主键可以)
  34. -----//使用索引为条件的操作
  35. SELECT * FROM dbo.UserInfo WHERE Name='aaa' --需要等待解锁(非主键不可)
  36. UPDATE dbo.UserInfo SET Name='ooo' WHERE Name='aaa' --可以执行更新(根据索引可以)
  37. DELETE dbo.UserInfo WHERE Name='aaa' --可以执行删除(根据索引可以)

悲观锁(更新锁-人工手动设置上锁):

结论:可以理解为在使用版本控制软件的时候A迁出了一个文件,并且8i将这个87文件锁定,B就无法再迁出该文件了,直到A迁入解锁后才能被其他人迁出。
[sql] view plain copy
  1. ------------------------A连接 Update Lock(悲观锁)---------------------
  2. BEGIN TRAN
  3. SELECT * FROM dbo.UserInfo WITH(UPDLOCK) WHERE Id=2
  4. SELECT resource_type, request_mode,COUNT(*) FROM sys.dm_tran_locks
  5. WHERE request_session_id=@@SPID
  6. GROUP BY resource_type,request_mode
  7. --COMMIT TRAN
  8. --ROLLBACK TRAN
  9. ---------------------------B连接 Update Lock(悲观锁)-------------------------
  10. SELECT * FROM dbo.UserInfo --可以执行查询
  11. SELECT * FROM dbo.UserInfo WHERE id=2 --可以执行查询
  12. SELECT * FROM dbo.UserInfo WHERE Name='ooo' --可以执行查询
  13. UPDATE dbo.UserInfo SET Age=3 WHERE id=1 --可以执行更新(根据主键可以)
  14. UPDATE dbo.UserInfo SET Age=3 WHERE Name='ccc' --需要等待解锁(非主键不可)
  15. DELETE dbo.UserInfo WHERE id=1 --可以执行更新(根据主键可以)
  16. DELETE dbo.UserInfo WHERE name='ccc' --需要等待解锁(非主键不可)

乐观锁(人工通过逻辑在数据库中模拟锁)

结论:可以理解为同样在使用版本控制软件的时候A迁出了一个文件,B也可以迁出该文件,两个人都可以对此文件进行修改,其中一个人先进行提交的时候,版本并没有变化所以可以正常提交,另一个后提交的时候,发现版本增加不对称了,就提示冲突由用户来选择如何进行合并再重新进行提交。
[sql] view plain copy
  1. --------------------------A客户端连接 Lock(乐观锁)------------------------
  2. --DROP TABLE Coupon
  3. -----------------创建优惠券表-----------------
  4. CREATE TABLE Coupon
  5. (
  6. Id INT PRIMARY KEY IDENTITY(1,1),
  7. Number VARCHAR(50) NOT NULL,
  8. [UserVARCHAR(50),
  9. UseTime DATETIME,
  10. IsFlag BIT DEFAULT(0) NOT NULL,
  11. CreateTime DATETIME DEFAULT(GETDATE()) NOT NULL
  12. )
  13. INSERT INTO dbo.Coupon(Number) VALUES ( '10000001')
  14. INSERT INTO dbo.Coupon(Number) VALUES ( '10000002')
  15. INSERT INTO dbo.Coupon(Number) VALUES ( '10000003')
  16. INSERT INTO dbo.Coupon(Number) VALUES ( '10000004')
  17. INSERT INTO dbo.Coupon(Number) VALUES ( '10000005')
  18. INSERT INTO dbo.Coupon(Number) VALUES ( '10000006')
  19. --SELECT * FROM dbo.Coupon WITH(NOLOCK) --查询数据
  20. --UPDATE Coupon SET [User]=NULL, UseTime=NULL, IsFlag=0 --还原数据
  21. -----------------1、模拟高并发普通更新-----------------
  22. DECLARE @User VARCHAR(50) --模拟要使用优惠券的用户
  23. DECLARE @TempId INT --模拟抽选出来的要使用的优惠券
  24. SET @User='a'
  25. BEGIN TRAN
  26. SELECT @TempId=Id FROM dbo.Coupon WHERE IsFlag=0 --高并发时此语句有可能另外一个该事务已取出的Id
  27. --WAITFOR DELAY '00:00:05'    --改用此方式要开两个SQL Management客户端
  28. UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId
  29. COMMIT TRAN
  30. --ROLLBACK TRAN
  31. -----------------2、悲观锁解决方案-----------------
  32. DECLARE @User VARCHAR(50) --模拟要使用优惠券的用户
  33. DECLARE @TempId INT --模拟抽选出来的要使用的优惠券
  34. SET @User='a'
  35. BEGIN TRAN
  36. SELECT @TempId=Id FROM dbo.Coupon WITH(UPDLOCK) WHERE IsFlag=0 --高并发时此语句会锁定取出的Id数据行
  37. --WAITFOR DELAY '00:00:05'    --改用此方式要开两个SQL Management客户端
  38. UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId
  39. COMMIT TRAN
  40. --ROLLBACK TRAN
  41. -----------------3、乐观锁解决方案-----------------
  42. ALTER TABLE dbo.Coupon ADD RowVer ROWVERSION NOT NULL --增加数据行版本戳类型字段(微软新推荐数据字段,该字段每张表只能有一个,会在创建行或更新行时自动进行修改无需人为干涉,该字段不能建立索引及主键因为会频繁修改)
  43. DECLARE @User VARCHAR(50) --模拟要使用优惠券的用户
  44. DECLARE @TempId INT --模拟抽选出来的要使用的优惠券
  45. DECLARE @RowVer BINARY(8) --抽选出来的优惠券的版本(ROWVERSION数据类型存储大小为8字节)
  46. SET @User='a'
  47. BEGIN TRY
  48. BEGIN TRAN
  49. SELECT @TempId=Id, @RowVer=RowVer FROM dbo.Coupon WHERE IsFlag=0 --取出可用的Id及对应的版本戳
  50. --WAITFOR DELAY '00:00:05'    --改用此方式要开两个SQL Management客户端
  51. UPDATE dbo.Coupon SET IsFlag=1, [User]=@User, UseTime=GETDATE() WHERE Id=@TempId ANDRowVer=@RowVer
  52. IF(@@ROWCOUNT > 0)
  53. BEGIN
  54. PRINT('修改成功')
  55. COMMIT TRAN
  56. END
  57. ELSE
  58. BEGIN
  59. PRINT('该数据已被其他用户修改')
  60. ROLLBACK TRAN
  61. END
  62. END TRY
  63. BEGIN CATCH
  64. ROLLBACK TRAN
  65. END CATCH
  66. --------------------------B客户端连接 Lock(乐观锁)------------------------
  67. --此测试需要开两个SQL Management Studio客户端,在A客户端使用WAITFOR DELAY来模拟并发占用,在B客户端执行与A客户端相同的SQL脚本即可(注释掉WAITFOR),所以在此不放相同代码了。
在乐观锁和悲观锁之间进行选择的标准是:冲突的频率与严重性。如果冲突很少,或者冲突的后果不会很严重,那么通常情况下应该选择乐观锁,因为它能得到更好的并发性,而且更容易实现。但是,如果冲突的结果对于用户来说痛苦的,那么就需要使用悲观策略。
我认为如果同一张表的并发很高,但并发处理同一条数据的冲突几率很低,那就应该使用乐观锁,反之,如果同一张表的并发不高,但同时处理同一条数据的几率很高,就应该使用悲观锁。

如涉及版权,请著作权人与本网站联系,删除或支付费用事宜。

0000