控制锁的粒度

探讨了SQL Server中锁升级的机制及控制方法,包括锁升级的触发条件、跟踪标志1211的作用、ALTER TABLE SET LOCK_ESCALATION选项的应用,以及ALTER INDEX SET ALLOW_PAGE_LOCKS选项的影响。

作者:Kalen Delaney(SQL Server MVP)

日期:2009年5月3日

翻译:张洪举

原文:http://sqlblog.com/blogs/kalen_delaney/archive/2009/05/03/controlling-lock-granularity.aspx

SQL Server 7.0 的所有版本中,引擎可以锁定行、 页或整个表。 此外,即使通过获取行或页锁的方式启动了查询,如果获取了太多的锁,SQL Server 也可能会将其提升到一个表锁。 每个锁都需要使用内存,所以,当以一个表锁升级替换成千上万的细粒度锁,可以节省大量的资源。另一方面,一旦表以独占方式被锁定,则没有其他进程可以从中访问任何数据,从而大幅降低并发操作。

我们回到 SQL Server 6/6.5中,我们具有一些控制,像表的多少百分比被锁定才引发锁的粒度升级。但是,这些作为执行控制的配置选项在 SQL 7 中被移除掉了。所以,现在我们如何进行控制呢?

在下列情况下会发生锁升级(来自SQL Server 2008 内部,MS Press 2009):

  • 在一个对象或是对象的一个分区上单个语句持有的锁的数量超过了一个阈值,当前该阈值是 5000 个锁,但是在以后的 Service Pack 中有可能更改该值。 如果锁散布在同一语句中的多个对象上,则不会发生锁升级,例如,在一个索引上有 3000 个锁,在另一个索引中也具有 3000 个锁。
  • 如果 locks 配置选项设置为 0 ,当被锁使用的内存超过了非 AWE32 位)或正常( 64 位)可用内存的 40% ,将达到锁升级阈值。实际上,在这种情况下,锁的内存是根据需要动态分配的,因此 40% 值并不是一个常数。如果 locks 选项设置为非零值,在 SQL Server 启动时,为锁保留的内存是静态分配的。 SQL Server 使用的内存超过了为锁资源保留的锁内存的 40% 以上时,将达到锁升级阈值。

我所见过的大部分对升级的讨论都是要阻止锁升级,从而能够最大化地访问数据。一种方法是在实例上启用跟踪标志1211,这可以防止从在任何情况下发生锁升级。 这样做你需要非常小心,因为它会影响实例上的所有数据库中的表。 另一种方法是跟踪 SQL Server 表的使用,因为即使有一个被不同连接锁定的行,也不会发生锁升级。 所以,你可以将一个虚拟的行添加到表中,并开启一个事务进程,更新虚拟的行,然后将事务保持打开状态。虽然这可以防止任何人获得一个表锁,但此方法具有严重的副作用。即:只要该事务处于打开状态,日志不能截断过去这一点。 但是,如果您可以将其与表的更新操作同步,并确保只要更新一完成就关闭虚拟更新,效果会好一些。

SQL Server 2008 为我们提供了更多的控制功能,在 ALTER TABLE 中添加一个新选项:

ALTER TABLE <table_name>

SET (LOCK_ESCALATION = [TABLE | AUTO |DISABLE]);

默认升级是 TABLE,并且在 SQL 2008之前这是唯一的升级选择。如果将该选项设置为 AUTO,锁可以升级到表或是一个分区(如果表已分区)。 第三个选项是完全禁用此表的升级。 此选项更易于管理对另一个对单行锁定的事务跟踪,以及更细粒度地禁止整个实例上的升级。

但是,你希望相反的行为怎么办?你总是希望通过表锁来节省资源怎么办? 是这样的,锁提示可以请求为 TABLOCK TABLOCKX,但这些设置必须在每个查询中指定。 如果一些表一直想锁定在表级别怎么办?

SQL Server ALTER INDEX 中提供了一个选项,用于禁止 ROW PAGE 锁(在 SQL 7 2000中可以使用系统过程 sp_indexoption 来做这些事情)。如果索引是聚集索引,这意味着对于表,ROW Page 锁是被禁止的。但是,如果表是一个堆,则不能使用此选项。

ALTER INDEX <index_name> ON <table_name>

SET (ALLOW_PAGE_LOCKS = OFF);

此外请注意如果将 ALLOW_PAGE_LOCKS 设置为 OFF,则不能 REORGANIZE (进行碎片整理) 索引。

我通常在我的内部和调试课程中提及此 ALTER INDEX 选项,并且还有一次提到此选项是客户不恰当地关闭了 PAGE ROW 锁,同时启动了许多莫名其妙的表锁。当你检查 sys.indexes (有名为 allow_row_locks allow_page_locks的列,可能的值仅为 0 1)时,该行为不再是莫名其妙的。 (对于 SQL Server 2005 之前的版本,可以使用 INDEXPROPERTY 函数来检查这些选项的状态) 这就是几年前我最后一次在遇到有人使用此选项来关闭精细粒度的锁,并且我正考虑从课程中删除对它的提及。

然而,上周在我的奥斯陆课上,一个学生询问为什么她具有 CTE 的查询在任何时候都启用表锁。我不相信它与 CTE 有关,但建议她等待一下,我们将在课程的第四天进行锁的讨论。 她付出了很多关注,星期五的早上她回来对我说不是 CTE 的问题,而是有人关闭了 ALLOW_PAGE_LOCKS ALLOW_ROW_LOCKS 选项

所以,在以后的课程中我想我会继续提及此选项。

<think> 首先,用户的问题是关于“降低锁粒度的方法”。我需要基于提供的引用[^1],它讨论了锁粒度的概念,但用户真正的需求是降低锁粒度的方法。引用中提到了锁粒度是指在多线程编程中选择合适的锁范围来平衡线程安全和性能,以及使用synchronized、Lock接口和Concurrent包。 我的回答必须: - 使用中文。 - 结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 - 对于数学表达式,行内的用$...$,独立公式用$$...$$并单独成段。 - LaTeX语法正确。 - 生成相关问题。 - 在引用段落末尾添加引用标识[^1]。 从系统指令看,用户引用了[^1],内容是线程篇:锁粒度是什么。我应该基于这个引用,但用户的需求是降低锁粒度的方法。 关键点是: - 降低锁粒度是为了提高并发性能,通过减少锁的范围或使用更细粒度的锁。 - 方法包括: 1. 使用更细粒度的锁对象:例如,为每个独立资源或数据段使用单独的锁,而不是一个全局锁。 2. 减小锁的范围:只在必要的代码块上加锁,使用synchronized块而不是方法。 3. 使用读写锁:如ReentrantReadWriteLock,允许并发读取。 4. 利用并发集合:如Java中的ConcurrentHashMap,它使用分段锁。 5. 无锁编程:使用原子类(如AtomicInteger)或CAS操作。 6. 锁分离:将锁分为读锁和写锁。 现在,结构化回答: 1. **引言**:解释降低锁粒度的概念和目的。 2. **方法**:列出并详细说明降低锁粒度的几种方法,参考引用[^1]。 3. **示例**:给出代码示例(如Java),但用户问题不是特定于语言,所以保持通用。 4. **结论**:强调平衡线程安全和性能。 5. **生成相关问题**:在末尾添加相关问题。 引用处理:在回答中引用[^1]的段落末尾添加[^1]。 最终输出格式: - 回答部分。 - §§相关问题§§部分。 生成相关问题: 1. 如何避免锁竞争? 2. 锁粒度过小会带来什么问题? 3. Java中Concurrent包的锁机制有哪些? 4. 无锁编程的实现方式? 5. 如何选择合适的锁粒度? </think> ### 降低锁粒度的方法 在多线程编程中,**锁粒度(Lock Granularity)** 是指锁的作用范围大小。降低锁粒度旨在缩小锁的保护范围,以减少锁竞争、提高并发性能,同时保持线程安全[^1]。核心思想是避免不必要的锁冲突,通过优化锁的设计来平衡性能与安全性。以下是降低锁粒度的主要方法,基于Java等语言的常见实践(其他语言原理类似): #### 1. **使用更细粒度的锁对象** 将全局锁替换为多个独立锁,每个锁保护一小部分数据。例如: - 为每个资源或数据段创建单独的锁对象,而不是使用单一锁保护整个数据集。 - 代码示例(Java): ```java // 高锁粒度(不推荐):全局锁保护所有数据 private final Object globalLock = new Object(); public void updateAll() { synchronized (globalLock) { // 更新所有数据 } } // 低锁粒度(推荐):为每个数据项单独加锁 private final Map<String, Object> dataLocks = new HashMap<>(); public void updateItem(String key) { synchronized (dataLocks.get(key)) { // 仅锁定特定键 // 更新单个数据项 } } ``` 优势:减少线程等待时间,提升吞吐量。 #### 2. **减小锁的范围(锁细化)** 仅在必要代码块上加锁,避免在方法级别加锁: - 使用`synchronized`块代替`synchronized`方法,或使用`ReentrantLock`的`lock()`/`unlock()`精确控制临界区。 - 示例(Java): ```java // 高锁粒度:方法级锁(整个方法被锁定) public synchronized void process() { // 非临界区代码(无需锁保护) // ... // 临界区代码(需要锁) } // 低锁粒度:块级锁(仅锁定关键部分) public void process() { // 非临界区代码(无锁) // ... synchronized (this) { // 或指定锁对象 // 临界区代码 } } ``` 优势:减少锁持有时间,提高并发度。 #### 3. **使用读写锁(ReadWriteLock)** 区分读操作和写操作,允许多个读线程并发访问: - 读锁(共享锁)允许多线程并发读取,写锁(独占锁)保证写操作互斥。 - 示例(Java的`ReentrantReadWriteLock`): ```java private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); public void readData() { rwLock.readLock().lock(); // 获取读锁(可并发) try { // 读取数据 } finally { rwLock.readLock().unlock(); } } public void writeData() { rwLock.writeLock().lock(); // 获取写锁(互斥) try { // 写入数据 } finally { rwLock.writeLock().unlock(); } } ``` 优势:在读多写少场景中显著提升性能[^1]。 #### 4. **利用并发容器(Concurrent Collections)** 使用线程安全的容器替代手动加锁,这些容器内部实现了细粒度锁: - Java的`ConcurrentHashMap`使用分段锁(Segment Locks),每个段独立加锁。 - 示例: ```java ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); map.put("key", "value"); // 自动处理细粒度锁 ``` 优势:简化代码,避免显式锁管理。 #### 5. **采用无锁编程(Lock-Free Techniques)** 完全避免锁,使用原子操作或CAS(Compare-And-Swap): - 原子类(如`AtomicInteger`)通过硬件指令保证线程安全。 - 示例(Java): ```java private AtomicInteger counter = new AtomicInteger(0); public void increment() { counter.incrementAndGet(); // 无锁操作 } ``` 优势:消除锁开销,适用于高并发计数或状态更新。 #### 关键注意事项 - **平衡性能与安全**:锁粒度过小可能导致锁数量过多,增加内存和上下文切换开销。建议通过性能测试(如JMH)优化。 - **避免死锁**:确保锁的获取顺序一致。 - **工具支持**:使用JVM分析工具(如VisualVM)检测锁竞争。 降低锁粒度是多线程优化的核心策略,能显著提升系统响应速度[^1]。实际应用中,需结合场景权衡锁的精细度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值