设为首页 收藏本站
查看: 1885|回复: 0

[经验分享] 转SQL Server 的事务和锁

[复制链接]

尚未签到

发表于 2015-6-30 09:17:25 | 显示全部楼层 |阅读模式
  最近在项目中进行压力测试遇到了数据库的死锁问题,简言之,如下的代码在 SERIALIZABLE 隔离级别造成了死锁:



?

1
2
3
4
5
6
7
8
9
10
11
12
13
14


SELECT @findCount=COUNT(id) FROM MyTable
WHERE [fk_related_id]=@Argument

IF (@findCount > 0)
BEGIN
    ROLLBACK TRANSACTION
    RETURN ERROR_CODE
END

INSERT INTO MyTable ([fk_related_id],…)
VALUES (@Argument,…)

COMMIT TRANSACTION
RETURN SUCCESS_CODE
  
  在搞清楚这个问题的过程中做了不少的实验,与各位共享。这一篇是开篇,主要说明的是 SQL Server 的四种(其实还有别的)经典的事务隔离级别,以及在不同的隔离级别下锁的使用手段,以及所带来的不同的数据一致性。
SQL Server 中锁的种类(Schema操作就暂时不涉及了)
锁类型描述
(Shared Lock)用于只读操作数据锁定
(Update Lock)用于数据的更新,在数据真正的需要更新的时候会申请升级为X锁。
X(Exclusive Lock)独占锁,用于数据的更改。
Key-Range Lock(稍后讨论)仅仅在 Serializable 隔离级别保护数据,以避免任何有可能使得本事务第二次读取信息产生错误的数据插入操作
各个事务隔离级别下锁的使用
  SQL Server 中有四种事务隔离级别,具体的大家去参建 MSDN。下面列出在不同的事务隔离级别下这些锁是如何使用的:
隔离级别读数据锁状态写数据锁状态锁持有时间
Read Uncommitted不获得任何锁不获得任何锁
Read Committed数据获得S锁对于 INSERT、DELETE、UPDATE的执行,获得X锁;对于UPDATE的标记,获得U锁;读完即释放,并不持有至事务结束。
Repeatable Read数据获得S锁对于 INSERT、DELETE、UPDATE的执行,获得X锁;对于UPDATE的标记,获得U锁;持有至事务结束
Serializable数据获得S锁,同时获得Key-Range锁。对于 INSERT、DELETE、UPDATE的执行,获得X锁;对于UPDATE的标记,获得U锁,同时获得Key-Range锁。持有至事务结束
  我们可以利用这些知识形象说明各个隔离级别下的数据一致性:
Read Uncommitted 级别
  (1)脏读
DSC0000.png
  (2)更新丢失
DSC0001.png
  (3)不可重复读
DSC0002.png
  (4)幻读
DSC0003.png
Read Committed 级别
  (1)脏读
DSC0004.png
  (2)更新丢失
DSC0005.png
  (3)不可重复读
DSC0006.png
  (4)幻读
DSC0007.png
Repeatable Read 级别
  (1)脏读
DSC0008.png
  (2)更新丢失
DSC0009.png
  (3)不可重复读
DSC00010.png
  (4)幻读
DSC00011.png
Serializable 级别
  (1)脏读
DSC00012.png
  (2)更新丢失
DSC00013.png
  (3)不可重复读
DSC00014.png
  (4)幻读
DSC00015.png
  我们从上图可以比较直观的看到以下的结论

脏读更新丢失不可重复读幻读
Read Uncommitted可能可能可能可能
Read Committed不可能可能可能可能
Repeatable Read不可能不可能不可能可能
Serializable不可能不可能不可能不可能
  这一篇到此为止,下一篇详细介绍 Key-Range Lock 并分析开篇提到的死锁问题。
  
  
  在这篇随笔中,我们的主要关注点在 Key-Range Lock。Key-Range Lock有 S-S、S-U、I-N、X-X几种情况。我们一个一个来说,力求明白。遗憾的是,这里可能会比较冗长,那么死锁分析只好依次顺延了。
Range S-S锁的获取规则
  MSDN 对 Range 锁的规则有部分描述,但是言简意赅,以下我们会将各种情况分解开来,理清MSDN中涉及的或者未涉及的规则,这些规则适用于SQL Server 2000/2005/2008/2008 R2。关于MSDN的描述,请参见:http://technet.microsoft.com/zh-cn/library/ms191272(en-us,SQL.110).aspx。
  在描述规则之前需要声明的是,我们的聚集索引就建立在 WHERE 字句之上,这很重要,否则是不会获得 Range 锁的,也就达不到 SERIALIZABLE 的要求了;另外,为了讨论简便,以下的 SQL 全部省略 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE 的声明。
  我们假设有以下的表:



?

1
2
3
4
5
6
7
8
9
10
11
12
13
14


CREATE TABLE [dbo].[MyTable](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [index_column] [int] NOT NULL,
    [data] [int] NOT NULL,
    CONSTRAINT [PK_MyTable] PRIMARY KEY NONCLUSTERED
    (
        [id] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]

CREATE UNIQUE CLUSTERED INDEX [IX_MyTable] ON [dbo].[MyTable]
(
    [index_column] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
  并假设我们有如下的数据:



?

1
2
3
4
5
6
7
8
9
10


INSERT INTO [MyTable] ([index_column],[data]) VALUES (1, 1)
INSERT INTO [MyTable] ([index_column],[data]) VALUES (2, 2)
INSERT INTO [MyTable] ([index_column],[data]) VALUES (3, 3)
INSERT INTO [MyTable] ([index_column],[data]) VALUES (4, 4)
INSERT INTO [MyTable] ([index_column],[data]) VALUES (5, 5)
INSERT INTO [MyTable] ([index_column],[data]) VALUES (15, 6)
INSERT INTO [MyTable] ([index_column],[data]) VALUES (16, 7)
INSERT INTO [MyTable] ([index_column],[data]) VALUES (18, 8)
INSERT INTO [MyTable] ([index_column],[data]) VALUES (25, 9)
INSERT INTO [MyTable] ([index_column],[data]) VALUES (30, 10)
  那么这张表看起来应该是这样的(我另外还将Index的Hash值以及row所在的数据页Dump出来了,以便咱们做实验)。
idindex_columndataindex hashrow page
111(8194443284a0)78
222(61a06abd401c)78
333(98ec012aa510)78
444(a0c936a3c965)78
555(59855d342c69)78
6156(f1de2a205d4a)78
7167(f07ed88b2b23)78
8188(e9069d930a93)78
9259(b81181109ebc)78
103010(8034b699f2c9)78
对于WHERE子句中的条件命中现有记录的情况
  规则一:如果 WHERE 子句使用的是“相等”条件,例如“WHERE [index_column]=6”,并且该索引是唯一索引,则该索引不会获得Key-Range S-S锁,仅仅是Key上获得普通S锁;
  假设我们执行
  SELECT [data] FROM [MyTable] WHERE [index_column]=1
  那么我们使用 sp_lock 得到锁的情况:
DSC00016.png
  可以发现第一个索引上获得了S锁,但并不是 Range S-S 锁。
  规则二:如果 WHERE 子句使用的是“范围”条件,例如“>、=1 AND [index_column]=20 AND [index_column]=2 AND [index_column]=10 AND [index_column]=30 AND [index_column]=6 AND [index_column]30 AND [index_column]=20 AND [index_column]、 0)
BEGIN
    ROLLBACK TRANSACTION
    RETURN ERROR_CODE
END

INSERT INTO MyTable ([fk_related_id],…)
VALUES (@Argument,…)

COMMIT TRANSACTION
RETURN SUCCESS_CODE[/td][/tr][/table]  在这个例子中,表 MyTable 的列 fk_related_id 是一个唯一索引(非聚集),事务隔离级别为 SERIALIZABLE。不同的存储过程执行会传入不同的 @Argument,表面看来,这不会有任何的问题,但是由于“下一个”数据的锁定,在稍高水平的并发上,就出现了大约 80% 的失败情况,这些失败都来源于死锁。我们挑选了其中的一次:
  我们试图以每秒钟 15 个的压力在 @Argument 属于 [1, 1000] 的范围内进行存储过程调用。在这个过程中,有一个 @Argument 为 115 的记录首先成功的插入了进去!
idfk_related_iddata
1115
  接下来有一个 @Argument 为 74 的记录获得了机会,我们假设它的 Session Id 为 A。它执行了 SELECT 语句:
idfk_related_iddata
1115 (A 获得了Range S-S Lock)
  接下来有一个 @Argument 为 4 的记录获得了机会,我们假设它的 Session Id 为 B。它执行了 SELECT 语句:
idfk_related_iddata
115 (A 、B获得了Range S-S Lock)
  接下来,Session A 执行到了 INSERT 语句,那么 Range S-S 锁会试图进行一个转换测试(Range I-N 锁),但这显然是行不通的,因为 Session B 也获得了 Range S-S Lock,因此 Session A 陷入了等待;
  而 Session B 也执行到了 INSERT 语句,相同的,它也陷入了等待;这样,Session A 等待 Session B 放弃 Range 锁,Session B 等待 Session A 放弃锁,这是一个死锁了。
  而更糟糕的事情是,凡是 @Argument 小于 115 的记录,他都会试图令下一个记录获得新的 Range S-S 锁,从而进入无限的等待中,至少,1-115 号记录死锁,并且最终 114 个需要放弃,1个成功。这就是为什么 SERIALIZABLE 隔离级别不但会发生死锁,而且在某些时候,是大面积死锁。
  总之:在 SERIALIZABLE 隔离级别下,只要有类似同一索引为条件先读后写的状况的,在较大并发下发生死锁的概率很高,而且如果碰巧既有的记录索引按照排序规则在非常靠后的位置,则很可能发生大面积死锁。
  那么如何解决这个问题呢,呃,降低隔离级别当然是一个方法,例如,如果你能接受幻读,那么 REPEATABLE READ 是一个不错的选择。但是我突然在某篇博客中看到了使用 SELECT WITH UPDLOCK 的方法。事实上,这种东西让死锁更容易了。
  例如,一个存储过程 SELECT B,而后 SELECT A;而另外的存储过程先 SELECT A,再 SELECT B,那么由于顺序不同,排他锁仅仅是 Read 的情况就可能发生死锁了。
  那么为什么 REPEATABLE READ 会好得多呢?因为 REPEATABLE READ 紧紧锁定现有记录,而不会使用 Range 锁。我们仍然以上述存储过程为例,这样,只有两个被锁定的行数据在同一个页上(因为默认情况下使用页级锁),或者说挨得足够近,才有可能死锁,并且这个死锁仅仅限于这个数据页上的记录而不会影响其他记录,因此死锁的概率大大降低了。
  我们实际测试中,在相同的测试条件下,并发提高到 100 的情况下时才有不到 0.1% 的死锁失败几率。当然我们付出了允许幻读的代价。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-81826-1-1.html 上篇帖子: sqlserver2005安装错误:性能监视器计数器要求:SQL Server 2005 中为安装程序增加计数器注册表项值.... 下篇帖子: 使用 SQLD 在线设计 Microsoft SQL Server 数据库实例
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表