You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

通知计数触发器异常:批量标记已读时未读数递减结果不符预期

计数异常的核心原因

这个问题的本质是触发器内的计数器更新写法不符合PostgreSQL的MVCC快照规则,具体表现为:

  • 你当前的计数器更新逻辑基于“先查当前值、算出差值、再覆盖回写”的非原子模式,对查询时的数据快照依赖极强。但PostgreSQL执行单条批量UPDATE语句时,整条语句会基于启动那一刻生成的全局数据快照,同语句内所有行级触发器,不管是BEFORE还是AFTER类型,都无法读到同语句内其他触发器对计数器表写入的中间值。
  • 对应你批量标记600条通知已读的场景:600次触发器触发计数器更新时,大部分操作读到的unread值都是语句启动前的初始值2000,计算后都会把unread设置成1999,不存在逐行累加递减的效果。只有少部分触发操作因为行锁等待重新读取了最新的计数器值,最后累计下来只减少了100,就出现了初始2000、最后1900的异常结果。
    你之前把触发器从AFTER改成BEFORE没有效果,就是因为两种行级触发器在同一个批量SQL的执行上下文中,遵循完全相同的快照可见性规则,从根本上解决不了读到过期值的问题。
修正方案

直接把计数器的更新逻辑改成无嵌套子查询的原子自增/自减写法,不要在触发器里嵌套查询计数器表的值:

CREATE OR REPLACE FUNCTION notifications.notify_seen_upd()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
  begin 
      if new.seen = true and old.seen = false then 
        -- 直接基于当前行的unread值做减法,不需要嵌套子查询读值
        update notifications.notifications_counter 
            set unread = unread - 1
            where notifications_counter.user_id = new.user_id;
      end if;
      if new.seen = false and old.seen = true then
        -- 直接基于当前行的unread值做加法,不需要嵌套子查询读值
        update notifications.notifications_counter 
            set unread = unread + 1
            where notifications_counter.user_id = new.user_id;
      end if;
  return new;        
  end;
$function$;

这种写法下,所有针对计数器表同一行的更新操作,会通过行锁串行化执行,每次拿到行锁后,都会基于当前行的最新值直接计算更新,完全不受语句初始快照的限制,600条通知触发的递减操作会准确累计,最终得到正确的unread值。

可选性能优化(针对大批量更新场景)

如果经常会一次更新成百上千条通知的seen状态,行级触发器会触发成百上千次对计数器表的单行更新,性能开销很高。可以将行级触发器调整为语句级触发器,在整条UPDATE语句执行完成后,一次性统计本次更新中从seen=false变为seen=true的行数,按用户维度批量更新计数器值,减少重复的行锁开销。

内容的提问来源于stack exchange,提问作者Mzia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.26 22:24:04