一:背景1。讲故事 在有关SQLSERVER的各种参考资料中,经常会看到如下四种事务隔离级别。READUNCOMMITTEDREADCOMMITTEDSERIALIZABLEREPEATABLEREAD 随之而来的是大量的文字解释,还会附带各种脏读,幻读,不可重复读常常会把初学者弄得晕头转向,其实事务的本质就是隔离,落地就需要锁机制,理解这四种隔离方式的花式加锁,应该就可以入门了,那如何可视化的观察锁过程呢?这里借助SQLProfile工具。二:四种事务隔离方式1。测试数据准备 还是用上一篇创建的post表,脚本如下:CREATETABLEpost(idINTIDENTITY,contentchar(4000))GOINSERTINTOdbo。postVALUES(aaa)INSERTINTOdbo。postVALUES(bbb)INSERTINTOdbo。postVALUES(ccc);INSERTINTOdbo。postVALUES(ddd);INSERTINTOdbo。postVALUES(eee);INSERTINTOdbo。postVALUES(fff); 有了测试数据之后,我们按照隔离级别高低的顺序来观察吧。2。SERIALIZABLE事务 事务串行化其实很好理解,如果要在C中找对应那就是ReaderWriterLock,读写事务是完全排斥的,接下来把SQLSERVER的隔离级别调整为SERIALIZABLE。SETTRANISOLATIONLEVELSERIALIZABLEGOBEGINTRANSELECTFROMdbo。postWHEREid3COMMIT 打开profile,选择lock:Acquired,lock:Released,SQL:StmtStarting选项,开启观察。 从图中可以清楚的看到,SQLSERVER直接对post附加了S锁,在COMMIT之后才真正的释放,在S锁期间,Insert和Update引发的X锁是进不来的,所以就会存在相互阻塞的情况,也许这就是串行化的由来吧。 sqlserver是一个支持多用户并发的数据库程序,如果锁粒度这么粗,必定给并发带来非常大的负面影响,不过文章开头的那三个指标脏读,幻读,不可重复读肯定都是不会出现的。2。REPEATABLEREAD事务 什么叫可重复读呢?简而言之就是同一个select查询执行二次,不会出现记录修改的情况,在真实场景中两次select查询期间,可能会有其他事务修改了记录,如果当前是REPEATABLEREAD模式,这是被禁止的,接下来的问题是如何落地实现呢?我们来看看SQLSERVER是如何做到的,参考sql如下:SETTRANISOLATIONLEVELREPEATABLEREADGOBEGINTRANSELECTFROMdbo。postWHEREid3COMMIT 这个图可能有些朋友看不懂,我稍微解释一下吧,数据库由数据页Page组成,数据页由记录RID组成,有了这个基础就好理解了,SQLSERVER会在事务期间把1:489:0也就是id3这个记录全程附加S锁,直到事务提交才释放S锁,在事务期间任何对它修改的X锁都无法对其变更,从而实现事务期间的可重复读功能,如果大家不明白可以再琢磨琢磨。 这里有一个细节需要大家注意一下,可重复读的场景下会出现幻读的情况,幻读就是两次查询出的结果集可能会不一样,比如第一次是3条记录,第二次变成了5条记录,为了方便理解我来简单演示一下。会话1SETTRANISOLATIONLEVELREPEATABLEREADGOBEGINTRANSELECTFROMdbo。postWHEREid3WAITFORDELAY00:00:05SELECTFROMdbo。postWHEREid3COMMIT会话2 在会话1执行的5s期间执行会话2语句。BEGINTRANINSERTINTOdbo。post(content)VALUES(gggggg)COMMIT 稍等片刻之后,会发现多了一个记录7,截图如下: 3。READCOMMITTED 提交读是目前SQLSERVER默认的隔离级别,它是以不会出现脏读为唯一目标,何为脏读,简而言之就是读取到了别的事务未提交的修改数据,这个数据有可能会被其他事务在后续回滚掉,如果真的被其他事务回滚了,那你读到了这样的数据就是错误的数据,可能会给你的系统带来非常隐蔽的bug,为了说明这个现象,我们用两个会话来测试一下帮助大家理解。会话1 在这个会话中,将id3的记录修改成zzzzzBEGINTRANUPDATEdbo。postSETcontentzzzzzWHEREid3WAITFORDELAY00:00:05ROLLBACK会话2 这个会话中,重复执行sql查询。BEGINTRANSELECTFROMdbo。postWITH(NOLOCK)WHEREid3脏读啦WAITFORDELAY00:00:05SELECTFROMdbo。postWITH(NOLOCK)WHEREid3正确的数据COMMIT 为了实现脏读这里加了nolock关键词,从图中明显的看到,获取的zzzzz数据是错误的,在一些和钱打交道的系统中是被严厉禁止的。 有了这些基础再理解可提交读可能会容易些,是不是很好奇SQLSERVER是如何实现的呢?参考sql如下:SETTRANISOLATIONLEVELREADCOMMITTEDGOBEGINTRANSELECTFROMdbo。postWHEREid3COMMIT 从加锁流程看,SQLSERVER会逐一扫描数据页附加IS锁,扫完马上就释放,不像前面那样保持到COMMIT之后,如果找到记录所在的Page时,会对下面的所有记录附加S锁,这个时候X锁就进不来了,这就是它的实现原理,大家可以把刚才的脏读的sql中的nolock去掉试试看,两次读取结果都是一样的。4。READUNCOMMITTED 本质上来说READUNCOMMITTED和nolock的效果是一样的,会引发脏读现象,主要是因为READUNCOMMITTED根本就不会对表记录使用任何锁,参考sql如下:SETTRANISOLATIONLEVELREADUNCOMMITTEDGOBEGINTRANSELECTFROMdbo。postWHEREid3COMMIT 接下来观察sqlprofile的输出。 可以看到READUNCOMMITTED只会对表和堆表结构这种架构附加锁,不会对表中记录附加任何锁,也就会引发脏读现象。三:总结 其实SQLSERVER还有带版本的SNAPSHOT隔离级别,在真实场景中往往会给TempDB造成很大的压力,这里就不介绍了。 相信通过Profile观察到的加锁动态过程,会让大家有更深入的理解。