共计 3588 个字符,预计需要花费 9 分钟才能阅读完成。
这期内容当中丸趣 TV 小编将会给大家带来有关 SQL Server 中怎么实现一个自旋锁,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。
为什么我们需要自旋锁?
用闩锁同步多个线程间数据结构访问,在每个共享数据结构前都放置一个闩锁没有意义的。闩锁与此紧密关联:当你不能获得闩锁(因为其他人已经有一个不兼容的闩锁拿到),查询就会强制等待,并进入挂起(SUSPENDED)状态。查询在挂起状态等待直到可以拿到闩锁,然后就会进入可执行(RUNNABLE)状态。对于查询执行只要没有可用的 CPU,查询就一直在可执行(RUNNABLE)状态。一旦 CPU 有空闲,查询会进入运行(RUNNING)状态,最后成功获取到闩锁,用它来保护访问的共享数据结构。下图展示了 SQLOS 对协调线程调度实现的状态机。
因为太多关联的闩锁,对“忙碌”数据结构使用闩锁保护没有意义。因此 SQL Server 实现所谓
自旋锁(Spinlocks)。自旋锁就像一个闩锁,存储引擎使用的一个轻量级同步对象,用来同步对共享数据结构线程访问。和闩锁的主要区别是你积极等待自旋锁——不离开 CPU。在自旋锁上的“等待”总会发生在运行(RUNNING)状态的 CPU。在你闭合循环里旋转直到获得自旋锁。这就是所谓的忙碌等待(busy wait)。自旋锁的最大优点是当查询在自旋锁上等待时,不会涉及到上下文切换。另一方面忙碌等待浪费 CPU 周期,其他查询也许能对它们更有效的使用。
为了避免太多的 CPU 周期浪费,SQL Server 2008 R2 及后续版本实现所谓的指数补偿机制(exponential backoff mechanism),那里在 CPU 上一些时间的休眠后,线程停止旋转。在线程进入休眠期间,增加了尝试获得自旋锁的超时。这个行为可以降低对 CPU 性能的影响。
(补充说明:Spinlock 中文可以称为自旋锁。它是一个轻量级的,用户态的同步对象,和 critical section 类似,但是粒度比前者小多了。它主要用来保护某些特定的内存对象的多线程并发访问。Spinlock 是排他性的。一次只能一个线程拥有。
Spinlock 的设计目标是非常快和高效率。Spinlock 内部如何工作呢?它首先试图获得某个对象的锁,如果目标被其它线程占有,就在那里轮询(spin)一定时间。如果还得不到锁,就 sleep 一小会,然后继续 spin。反复这个过程直到得到对象的占有权。)
自旋锁与故障排除对自旋锁故障排除的主要 DMV 是 sys.dm_os_spinlock_stats。这个 DMV 里返回的每一行都代表 SQL Server 里的一个自旋锁。SQL Server 2014 实现了 262 个不同自旋锁。我们来详细看下这个 DMV 里的各个列:
name:自旋锁名称 collision:当尝试访问保护的数据结构时,被自旋锁阻塞的线程次数 spins:在循环里尝试获得自旋锁的自旋锁线程次数 spins_per_collision:旋转和碰撞之间的比率 sleep_time:因为退避线程休眠时间 backoffs:为了其他线程在 CPU 上继续,线程退避次数在这个 DMV 里最重要的列是 backoffs,对于特定的自旋锁类型,这列告诉你退避发生频率。高频率的退避会屈服于 CPU 消耗引起 SQL Server 里的自旋锁竞争(Spinlock Contention)。我就见过一个 32 核的 SQL Server 服务器,CPU 运行在 100% 而不进行任何工作——典型的自旋锁竞争症状。
对自旋锁问题进行故障排除你可以使用扩展事件提供的 sqlos.spinlock_backoff。当退避(backoff)发生时,就会触发这个扩展事件。如果你捕获了这个事件,你还要保证你使用非常好的选择性谓语,因为在 SQL Server 里退避会经常发生。一个好的谓语可以是特定的自旋锁类型,通过刚才提到的 DMV 你已经看到。下列代码给你展示了如何创建这样的扩展事件会话。
-- Retrieve the type value for the LOCK_HASH spinlock. -- That value is used by the next XEvent session SELECT * FROM sys.dm_xe_map_values WHERE name = spinlock_types AND map_value = LOCK_HASH GO -- Tracks the spinlock_backoff event CREATE EVENT SESSION SpinlockContention ON SERVER ADD EVENT sqlos.spinlock_backoff( ACTION ( package0.callstack ) WHERE ( [type] = 129 -- Value from the previous query )) ADD TARGET package0.histogram ( SET source = package0.callstack , source_type = 1 ) GO
从代码里可以看到,这里我在调用堆栈(callstack)上使用了直方图(histogram)目标来 bucktize。因此对于特定的自旋锁,你可以可能到 SQL Serve 里生成的最高退避(backoffs)代码路径。你甚至可以通过启用 3656 跟踪标记(trace flag)来标识调用堆栈。这里你可以看到来自这个扩展会话的输出:
sqldk.dll!XeSosPkg::spinlock_backoff::Publish+0x138sqldk.dll!SpinlockBase::Sleep+0xc5sqlmin.dll!Spinlock 129,7,1 ::SpinToAcquireWithExponentialBackoff+0x169sqlmin.dll!lck_lockInternal+0x841sqlmin.dll!XactWorkspaceImp::GetSharedDBLockFromLockManager+0x18dsqlmin.dll!XactWorkspaceImp::GetDBLockLocal+0x15bsqlmin.dll!XactWorkspaceImp::GetDBLock+0x5asqlmin.dll!lockdb+0x4a sqlmin.dll!DBMgr::OpenDB+0x1ecsqlmin.dll!sqlusedb+0xebsqllang.dll!usedb+0xb3sqllang.dll!LoginUseDbHelper::UseByMDDatabaseId+0x93sqllang.dll!LoginUseDbHelper::FDetermineSessionDb+0x3e1sqllang.dll!FRedoLoginImpl+0xa1bsqllang.dll!FRedoLogin+0x1c1sqllang.dll!process_request+0x3ecsqllang.dll!process_commands+0x4a3sqldk.dll!SOS_Task::Param::Execute+0x21esqldk.dll!SOS_Scheduler::RunTask+0xa8sqldk.dll!SOS_Scheduler::ProcessTasks+0x279sqldk.dll!SchedulerManager::WorkerEntryPoint+0x24csqldk.dll!SystemThread::RunWorker+0x8fsqldk.dll!SystemThreadDispatcher::ProcessWorker+0x3absqldk.dll!SchedulerManager::ThreadEntryPoint+0x226
使用提供调用堆栈,不难找出自旋锁竞争发生的地方。在那个指定的笤俑堆栈里竞争发生在 LOCK_HASH 自旋锁类型里,它是保护锁管理器的哈希表。每次在锁管理器里加锁或解锁被执行时,自旋锁必须在对应的哈希桶里获得。如你所见,在调用堆栈里,当从 XactWorkspacelmp 类调用 GetSharedDBLockFromLockManager 函数时,自旋锁被获得。这表示当竞争到数据库时,共享数据库锁被尝试获取。最后在用很高的退避(backoffs)的 LOCK_HASH 自旋锁里,这屈服于自旋锁竞争。
上述就是丸趣 TV 小编为大家分享的 SQL Server 中怎么实现一个自旋锁了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注丸趣 TV 行业资讯频道。