在SQL Server等关系型数据库的使用过程中,长事务引发的锁表问题是常见的性能瓶颈。当某个事务长时间持有表的锁资源不释放时,其他查询或写入操作会被阻塞,严重时会导致业务接口超时、系统响应缓慢。很多开发者会尝试使用NOLOCK查询提示或者调整事务隔离级别来缓解这个问题,这两种方式确实能在一定程度上减少锁冲突,但各自有不同的适用场景和注意事项。

长事务锁表的产生原因
数据库的事务遵循ACID特性,为了保证数据一致性,不同隔离级别下事务会对操作的数据加对应的锁。如果事务中包含耗时操作,比如复杂的多表关联查询、大量数据的批量更新,或者事务开启后没有及时提交或回滚,就会导致锁持有时间过长,形成长事务锁表。比如下面这段未及时提交的事务代码就可能引发问题:
BEGIN TRANSACTION; -- 开启事务后执行耗时更新操作 UPDATE user_table SET status = 1 WHERE age > 30; -- 这里如果忘记提交,事务会一直持有锁 -- COMMIT TRANSACTION;
使用NOLOCK提示规避锁表
NOLOCK是SQL Server中的查询提示,作用是让查询语句在读取数据时不申请共享锁,同时忽略其他事务施加的排他锁,直接读取数据的最新版本(包括未提交的数据)。这种方式能避免查询被其他事务的锁阻塞,从而减少锁冲突的概率。
使用NOLOCK提示的语法很简单,在查询的表名后加上WITH(NOLOCK)即可:
-- 使用NOLOCK提示查询用户表,不会被其他事务的锁阻塞 SELECT id, name, age FROM user_table WITH(NOLOCK) WHERE status = 0;
不过NOLOCK提示有明显的风险,因为它允许读取未提交的数据,也就是会产生脏读。如果其他事务后续回滚了未提交的修改,那么当前查询得到的结果就是错误的。因此NOLOCK只适合对数据一致性要求不高的场景,比如后台统计类查询,不适合核心业务的一致性校验场景。
通过设置隔离级别避免长事务锁表
事务隔离级别是数据库控制事务之间数据可见性的机制,不同的隔离级别加锁策略不同,合理设置隔离级别也能减少锁冲突。SQL Server支持四种标准隔离级别和一种快照隔离级别:
| 隔离级别 | 加锁策略 | 是否避免锁表 | 数据一致性保障 | |
|---|---|---|---|---|
| READ UNCOMMITTED | 读操作不加共享锁,读未提交数据 | 是 | 最低,存在脏读、不可重复读、幻读 | |
| READ COMMITTED | 读操作加共享锁,读完立即释放 | 部分场景可避免 | 避免脏读,存在不可重复读、幻读 | |
| REPEATABLE READ | 读操作加共享锁,事务结束才释放 | 否,更容易产生锁冲突 | 避免脏读、不可重复读,存在幻读 | |
| SERIALIZABLE | 范围锁,完全串行化执行 | 否,锁冲突概率最高 | 最高,无并发问题 | |
| SNAPSHOT | 基于行版本控制,读操作不加锁 | 是 | 避免脏读、不可重复读、幻读,读已提交数据 |
如果想要避免长事务锁表,同时又要保证读取的是已提交的数据,优先选择SNAPSHOT隔离级别,它的原理是为每个事务维护数据的行版本,读操作不需要加锁,不会阻塞写操作,也不会被写操作阻塞。设置SNAPSHOT隔离级别的步骤如下:
-- 首先开启数据库的快照隔离支持 ALTER DATABASE test_db SET ALLOW_SNAPSHOT_ISOLATION ON; -- 然后设置当前会话的隔离级别为SNAPSHOT SET TRANSACTION ISOLATION LEVEL SNAPSHOT; -- 执行查询,此时不会加锁,也不会读未提交数据 SELECT id, name FROM user_table WHERE age < 25;
READ UNCOMMITTED隔离级别的效果和NOLOCK提示一致,也会产生脏读,使用方式和NOLOCK类似:
-- 设置会话隔离级别为READ UNCOMMITTED,等价于所有查询加NOLOCK SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT id, name FROM user_table WHERE status = 1;
两种方式的对比与最佳实践
- NOLOCK提示是语句级别的,只针对加了提示的查询生效,灵活性高,但风险也明确,仅适合临时统计、日志查询等对数据一致性要求低的场景,不能滥用在核心业务查询中。
- 隔离级别是会话或事务级别的,设置后对当前会话或事务的所有查询生效,SNAPSHOT隔离级别能兼顾无锁和数据一致性,适合对并发要求高、又要保证数据正确的业务场景,但需要数据库开启对应支持,且会占用一定的存储空间维护行版本。
除了使用这两种方式,避免长事务锁表更根本的做法是优化事务设计:尽量缩短事务的执行时间,不要在事务中执行耗时操作;及时提交或回滚事务,避免事务长时间挂起;对于大批量数据操作,可以分批次执行,减少单次事务的锁持有时间。如果必须使用较高隔离级别,也要尽量缩小事务的操作范围,减少锁的影响面。