1.2.3 SQL Server的并发

关系型数据库为了保证数据一致性和并发性的协调,通过事务隔离级别来实现具体控制。最早的事务机制就是由James Gray博士提出并落地的,他的著作Transaction Processing: Concepts and Techniques正是描述了事务的控制,他于1998年获得图灵奖。他也是SQL Server 7.0的奠基人。众所周知,SQL Server的原始版本是与Sybase公司合作开发的,而真正奠定现在SQL Server内核基础的版本,就是James Gray领导开发的7.0版本,从8.0版本开始改用发行年命名规则,比如后续的版本为SQL Server 2000、SQL Server 2005等。

关于事务的ACID四属性,有数据库基础的读者肯定都已经知道了。1.1节介绍MySQL时也提到过,此处不再赘述。

这里我们重点说一下Lock和Latch的主要区别。

Lock是逻辑概念,比如表锁、行锁、X锁、S锁、IX锁、IS锁。

Latch是物理概念,它是实际存在的内存地址,用于处理页的隔离操作。

Mutex比Latch更轻(字节少),Mutex时常会使用自旋锁。

在SQL Server中很少会使用自旋锁,因为自旋会导致产生CPU开销。SQL Server只有在很短的时间内,为了锁住某个对象名称,才会使用自旋锁;而其他开源数据库在这一点上缺乏规避的能力,比如MySQL经常会遇到大量自旋导致CPU跑高的情况,这与其本身并发调度方式设计有很大的关系。

关于SQL Server的并发,有两个常见误区。

误区一:SQL Server的写阻塞读。

下面是Oracle培训班里的一个经典案例,为了展示Oracle的多版本读特性。

Session #1 执行一条update id=1 语句(Oracle默认隐式事务)。

Session #2 执行一条select id=1 语句,因为读取了回滚段的最后提交版本数据,可以返回结果,所以Oracle写不阻塞读。

同样的方法,在SQL Server的测试中,我们看到Session #2会被阻塞。

Session #1 需要手动启动显式事务。

Session #2 查询id=2的行,会被阻塞。

这种读被写阻塞的锁,我们称之为“悲观锁”,如图1-24所示。不被阻塞的锁,我们称之为“乐观锁”。

图1-24 在悲观锁情况下被阻塞

通过查询锁可以看到,确实是Session #1持有了id=2的X行锁,Session #2拿不到S锁,如图1-25所示。

图1-25 悲观锁——锁等待

这里的行锁是符合X锁和S锁不兼容的隔离级别要求的,所以和Oracle最大的区别在于,SQL Server默认没有使用回滚段。为什么SQL Server默认不使用回滚段呢?这时就要考虑SQL Server是否有“回滚段”了。请参考图1-26。

图1-26 SQL Server Redo与Undo

SQL Server没有单独的Undo空间,而是把Undo的信息放在了日志文件里。在SQL Server的白皮书里,官方介绍过SQL Server的日志和恢复(Logging & Recovery)三个阶段:Analysis、Redo、Undo。

所以,如果想要像Oracle一样维护数据行的多版本,也需要一个专门的空间来支持。在SQL Server中,这个空间就是tempdb,当启用了Read-Committed Snapshot时,这个快照(Snapshot)就会在tempdb中维护多版本的数据,实现写不阻塞读。

误区二:SQL Server的并发不行。

我们时常能够听到关于SQL Server的并发不行的传言。经过和一些工程师的交流了解到,他们所抨击的正是SQL Server锁的实现方式。

在Oracle中,事务锁是存储在文件头部的;在MySQL中,事务锁是存储在THD的结构体里的;在PGSQL中,事务锁是存储在Tuple内的。这几种方式有一个天然的好处,就是锁的内存开销非常小。

而SQL Server的锁被维护在内存系统表中,每一个lock对象虽然是逻辑的,但物理内存成本是比较高的。为了应对突发性的对大量行锁的请求(row lock count>5000),SQL Server会直接进行锁升级(Lock Escalation),以实现节省内存+大批优先。当然,锁升级可能会导致同一时刻其他Session的请求被阻塞。

举例来说:SPID 100进程,需要更新10 000行数据,在正常情况下应该申请10 000个行锁。因为锁升级,现在变成了1个表锁。

同一时刻,SPID 101进程,需要更新1行数据,和SPID 100进程有重复,原本会因为行锁被阻塞,而此时会因为表锁被阻塞。

SPID 102进程,需要更新1行数据,和SPID 100进程无重复,原本不会被阻塞,而此时会被阻塞。

从本质上说,如果表没有大量并发,锁升级并不会太影响并发度。而真正的热点表,也不应该有大量行锁;否则,类似于SPID 101进程的情况,也会因为SPID 100进程执行慢而导致等待的会话累积变多。

那么,SQL Server把锁放在内存中有没有好处呢?

当然是有好处的,最大的好处就是SQL Server的锁跟踪非常方便,可以通过sp_lock、sys.dm_tran_locks等系统视图进行实时查询。

而类似于MySQL的锁跟踪会比较麻烦,尤其是MDL,需要打开Performance Schema专门的跟踪器(Counter)才可以跟踪到。

关于锁升级的一些跟踪标记(Trace Flag),请查看微软官方手册。如果系统对并发性能很敏感,则可以弱化锁升级的行为。