前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 11|回复: 0

[开发应用] Sybase ASE 阻塞与锁等待排查实战

[复制链接]

[开发应用] Sybase ASE 阻塞与锁等待排查实战

[复制链接]
dbaai

主题

0

回帖

171

积分

DBAAI

积分
171
4 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
Sybase ASE 阻塞与锁等待排查实战


一、具体的问题

做 ASE 运维的,大概都遇到过这个场景:业务突然报"某个功能点一下就转圈,转十几秒然后超时",应用服务器的连接池很快被占满。登录数据库执行 sp_who,看到几十个 spid 的 status 清一色是 lock sleepblk 那一列都指向同一个数字。把源头那个 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(制造阻塞):
  1. use accdb
  2. go
  3. begin tran
  4. update acc_bill set status = '9' where bill_date = '2026-09-14'
  5. -- 注意:故意不 commit
  6. go
复制代码

会话 2(被阻塞):
  1. use accdb
  2. go
  3. select count(*) from acc_bill where bill_date = '2026-09-14'
  4. go
复制代码

会话 2 会一直挂着不返回。这就是线上"点一下转圈"的等价物。

2. 第一现场三板斧

换一个会话,依次执行:
  1. sp_who
  2. go
复制代码

statuslock sleep 的行,记下它的 spidblk(阻塞它的 spid)。
  1. sp_lock
  2. go
复制代码

locktype 列,如果看到 exclusive table,说明已经发生锁升级;exclusive page / exclusive row 则还在细粒度。
  1. select spid, blocked, status, cmd, dbid
  2. from master..sysprocesses
  3. where blocked != 0
  4. go
复制代码

blocked 列是 ASE 直接给出的"被谁堵",比 sp_whoblk 更适合写进脚本。

3. 一条 SQL 拉出完整阻塞链

sysprocesses 自连接,把"我堵了谁"和"谁堵了我"一次查出来:
  1. select  p.spid        as 被堵spid,
  2.         p.blocked     as 堵它的人,
  3.         p.status      as 状态,
  4.         p.program_name as 程序,
  5.         p.cmd         as 命令
  6. from    master..sysprocesses p
  7. where   p.blocked != 0
  8. order by p.blocked, p.spid
  9. go
复制代码

如果要判断谁是链头(没被任何人堵、但堵着别人):
  1. select distinct p.blocked as 链头spid,
  2.        h.status, h.program_name, h.loggedindatetime
  3. from   master..sysprocesses p
  4. join   master..sysprocesses h on p.blocked = h.spid
  5. where  p.blocked != 0
  6. and    h.blocked  = 0
  7. go
复制代码

4. 拿到源头在执行的 SQL

锁找到了,还要知道它在跑什么。dbcc sqltext 是这一步的关键:
  1. dbcc sqltext(45)
  2. go
复制代码

输出里的 SQL Text 就是该 spid 最近一次执行的语句。如果拿到的是一条很普通的 UPDATE,别急着下结论——看一下事务开了多久:
  1. dbcc opentran('accdb')
  2. go
复制代码

它会给出最老的活动事务的开始时间、对应的 spid 和日志位置。如果显示"事务已开启 42 分钟",那问题基本就是事务边界而不是 SQL 本身。ASE 15 之后还可以查 master..syslogshold,看是哪个事务在拖着日志不让截断:
  1. select dbid, spid, starttime, name
  2. from   master..syslogshold
  3. 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 秒


改锁方案这一步:
  1. alter table acc_bill lock datarows
  2. go
复制代码

注意它会重建表,需要停机窗口和足够的空间,别在线上高峰直接跑。

6. 四类根治动作


  • 缩短事务边界:把远程调用、文件读写、人工确认这些慢动作全部挪出事务,BEGIN TRANCOMMIT 之间只留数据库操作。这一条的收益通常最大;
  • 让语句走索引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 做重试比让用户干等体验好得多。


读多写少的报表场景,可以考虑在会话级降隔离级别,避免读被写堵住:
  1. set transaction isolation level 0
  2. go
  3. -- 或者只对单条语句生效
  4. select count(*) from acc_bill at isolation read uncommitted
  5. go
复制代码

ASE 15.7 起对 datarows 表还支持 readpast 提示,跳过被锁的行,适合队列类表取任务。

四、实操检查清单


  • [ ] 装上 sp_who / sp_lock 的定时采样(每 10~30 秒一次写入历史表),故障时才有"当时的现场",不用靠运气撞上。
  • [ ] 每次阻塞告警,先定位链头 spidblocked = 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、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

QQ|Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-9-14 12:37 , Processed in 0.024258 second(s), 10 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表