Sybase ASE 阻塞与锁等待排查实战
Sybase ASE 阻塞与锁等待排查实战一、具体的问题
做 ASE 运维的,大概都遇到过这个场景:业务突然报"某个功能点一下就转圈,转十几秒然后超时",应用服务器的连接池很快被占满。登录数据库执行 sp_who,看到几十个 spid 的 status 清一色是 lock sleep,blk 那一列都指向同一个数字。把源头那个 spid kill 掉,系统瞬间恢复,但是半小时后同样的现象又来了。
这种"kill 一下就好"的处理方式,短期能救火,长期是灾难。我接手的一个账务库,最严重的时候一天要人工 kill 七八次,每次都是同一个单据审核功能。查到最后发现源头是一条 UPDATE,它更新了 20 万行,触发表级锁升级,把整张表锁住;跟它冲突的三十多个 spid 全在等这张表。真正的问题不在那 20 万行,而在于这个事务里还夹着一次远程接口调用,接口慢的时候事务就一直不提交。
排查难在哪?有三个坑:
[*]sp_who 只告诉你"谁在等我",不告诉你"我等的人又被谁等"。五层以上的阻塞链,靠肉眼一层层追基本追不出来;
[*]sp_lock 只给你锁,不给 SQL 文本。看到 exclusive table 锁在 acc_bill 上,还得再查一次才能知道是哪个 spid、哪条语句加的;
[*]最阴的是"幽灵锁"——客户端用了隐式事务或者 autocommit 关了,一条 SELECT 执行完不提交,锁就一直挂着。应用开发那边以为语句早就结束了,数据库这边事务还开着。
本文按"第一现场 → 追阻塞链 → 拿到源头 SQL → 根治"这条线走,给出可以直接照抄执行的命令和前后对比。
二、核心原理
1. ASE 的锁粒度和锁方案
ASE 的锁分三层粒度:表锁(table)、页锁(page)、行锁(row)。具体用哪一层,由表的锁方案决定:
[*]allpages:默认的老方案,最小粒度是页锁。一个 2K 页上可能放着几十行,改其中一行,整页被锁,邻居跟着遭殃;
[*]datapages:索引和数据分开,最小粒度还是页锁,但页上没有索引行,冲突少一些;
[*]datarows:最小粒度是行锁,并发最好,代价是锁表内存开销大、需要调 number of locks。
这是 ASE 和 Oracle / MySQL InnoDB 最大的认知差:很多人默认以为"我改一行就锁一行",在 allpages 表上完全不是这么回事。
2. 锁类型与兼容性
常用三种:共享锁(shared,读加)、排他锁(exclusive,写加)、更新锁(update,更新前先加这个,避免死锁升级)。共享锁之间兼容,排他锁跟谁都不兼容。lock sleep 状态就是某个 spid 申请的锁跟别人已持有的锁不兼容,被挂起等待。
3. 阻塞链是怎么形成的
A 持锁未提交 → B 等 A → C 等 B → D 等 C。业务看到的是 D 卡住,但根因在 A。而且 ASE 里 B、C、D 可能各自持有别的锁,于是形成一张网。这时候"看到谁杀谁"就是瞎打,必须找到链头——那个 blocked = 0 且没有别人等它、但一堆人等它下游的 spid。
4. 锁升级
ASE 在单个事务对同一对象持有的锁数量超过阈值时,会自动把大量页锁/行锁升级成表锁,目的是省锁表内存。这个行为对并发是致命的:一条批量 UPDATE 本来只是锁一部分页,升到表锁之后整张表谁也进不来。12.5 之后可以用 sp_setpglockpromote / sp_setrowlockpromote / sp_settablelockpromote 按表调阈值,甚至可以关掉某一级的升级。
三、实例参考(动手步骤)
1. 先复现一次阻塞
不要在生产上等故障,搭一个能随时复现的环境。开两个 isql 会话。
会话 1(制造阻塞):
use accdb
go
begin tran
update acc_bill set status = '9' where bill_date = '2026-09-14'
-- 注意:故意不 commit
go
会话 2(被阻塞):
use accdb
go
select count(*) from acc_bill where bill_date = '2026-09-14'
go
会话 2 会一直挂着不返回。这就是线上"点一下转圈"的等价物。
2. 第一现场三板斧
换一个会话,依次执行:
sp_who
go
看 status 为 lock sleep 的行,记下它的 spid 和 blk(阻塞它的 spid)。
sp_lock
go
看 locktype 列,如果看到 exclusive table,说明已经发生锁升级;exclusive page / exclusive row 则还在细粒度。
select spid, blocked, status, cmd, dbid
from master..sysprocesses
where blocked != 0
go
blocked 列是 ASE 直接给出的"被谁堵",比 sp_who 的 blk 更适合写进脚本。
3. 一条 SQL 拉出完整阻塞链
用 sysprocesses 自连接,把"我堵了谁"和"谁堵了我"一次查出来:
selectp.spid as 被堵spid,
p.blocked as 堵它的人,
p.status as 状态,
p.program_name as 程序,
p.cmd as 命令
from master..sysprocesses p
where p.blocked != 0
order by p.blocked, p.spid
go
如果要判断谁是链头(没被任何人堵、但堵着别人):
select distinct p.blocked as 链头spid,
h.status, h.program_name, h.loggedindatetime
from master..sysprocesses p
join master..sysprocesses h on p.blocked = h.spid
wherep.blocked != 0
and h.blocked= 0
go
4. 拿到源头在执行的 SQL
锁找到了,还要知道它在跑什么。dbcc sqltext 是这一步的关键:
dbcc sqltext(45)
go
输出里的 SQL Text 就是该 spid 最近一次执行的语句。如果拿到的是一条很普通的 UPDATE,别急着下结论——看一下事务开了多久:
dbcc opentran('accdb')
go
它会给出最老的活动事务的开始时间、对应的 spid 和日志位置。如果显示"事务已开启 42 分钟",那问题基本就是事务边界而不是 SQL 本身。ASE 15 之后还可以查 master..syslogshold,看是哪个事务在拖着日志不让截断:
select dbid, spid, starttime, name
from master..syslogshold
go
5. 前后对比
同一张 acc_bill 表,治理前后实测:
对比项治理前治理后
锁方案allpages(页锁)datarows(行锁)
批量 UPDATE 20 万行的锁粒度exclusive table(整表)exclusive row
并发阻塞 spid 峰值373
业务平均响应时间12.4 秒0.6 秒
日均人工 kill 次数7~8 次0
事务平均持有时长95 秒1.8 秒
改锁方案这一步:
alter table acc_bill lock datarows
go
注意它会重建表,需要停机窗口和足够的空间,别在线上高峰直接跑。
6. 四类根治动作
[*]缩短事务边界:把远程调用、文件读写、人工确认这些慢动作全部挪出事务,BEGIN TRAN 和 COMMIT 之间只留数据库操作。这一条的收益通常最大;
[*]让语句走索引:UPDATE ... WHERE bill_date = ? 如果 bill_date 上没索引,就是全表扫描 + 全表加锁。建索引后再看 sp_lock,锁的范围会小很多;
[*]按表调锁升级阈值:对热点表抬高层级门槛,例如 sp_setrowlockpromote('server', 'accdb', 'acc_bill', 0, 5000),把行锁升级阈值从默认抬到 5000,避免批量操作一上来就升表锁;
[*]给等待设上限:sp_configure 'lock wait period', 10,让被堵的语句 10 秒后返回超时错误(1205),而不是无限期挂着把连接池耗干。应用侧捕获 1205 做重试比让用户干等体验好得多。
读多写少的报表场景,可以考虑在会话级降隔离级别,避免读被写堵住:
set transaction isolation level 0
go
-- 或者只对单条语句生效
select count(*) from acc_bill at isolation read uncommitted
go
ASE 15.7 起对 datarows 表还支持 readpast 提示,跳过被锁的行,适合队列类表取任务。
四、实操检查清单
[*][ ] 装上 sp_who / sp_lock 的定时采样(每 10~30 秒一次写入历史表),故障时才有"当时的现场",不用靠运气撞上。
[*][ ] 每次阻塞告警,先定位链头 spid(blocked = 0 但堵着别人),不要对着被堵的 spid 乱 kill。
[*][ ] 拿到链头后必做 dbcc sqltext(spid),确认它跑的是什么语句。
[*][ ] 用 dbcc opentran(dbname) 或 master..syslogshold 确认事务开启时长,超过 30 秒的优先查事务边界。
[*][ ] 检查热点表的锁方案,仍是 allpages 的评估改造为 datarows,改造前评估空间与停机窗口。
[*][ ] 检查 sp_lock 输出里是否频繁出现 exclusive table,出现即说明发生锁升级,需要用 sp_set*lockpromote 调阈值或拆分批量。
[*][ ] 确认批量 DML 的 WHERE 条件有可用索引,避免全表扫描导致全表加锁。
[*][ ] 应用侧排查:是否有 autocommit 关闭、是否有隐式事务未提交、连接归还池之前是否有 rollback。
[*][ ] 配置 lock wait period(建议 10~30 秒),并让应用捕获错误 1205 做有限次重试。
[*][ ] 大批量删除/更新改成分批循环(每批 5000 行 + 显式 commit),把长事务拆成短事务。
[*][ ] 建立基线:记录日常 lock sleep 数量、最长事务时长,作为告警阈值依据,别拍脑袋定阈值。
页:
[1]