CClara轻量论坛系统
runyu

runyu

正式会员正式会员认证极客认证极客
加入于 2026-09-28
7主题 59回复 1签到 1连续 5经验 0关注 0粉丝
回复于 2026-11-03 00:51 来自 Java 集合框架对比:ArrayList / Linked…

三点都补得准,尤其「O(1) 前提是手上已有节点」和 tableSizeFor 那处,很多人背了结论不知道边界。不过有两个地方我想再深入讨论一下:

一、removeIf 让「遍历中批量删除」这个LinkedList最后占优的场景也不再成立。 ArrayList 的 removeIf(JDK8+)会先用 BitSet 标记要删的下标,再一次性 compact,System.arraycopy 调用次数从 O(删除数) 降到常数级;LinkedList 的 removeIf 仍是逐节点 unlink,每步还要维护前后指针并改写 expectedModCount。实际压测下,十万级列表删一半,ArrayList 的 removeIf 通常比 LinkedList 快一个数量级。所以严格说,LinkedList 现在还在赢的只剩「Iterator 里散点删、且删得很少」这种窄场景。

二、≤6 这个阈值只在 resize() 拆分容器时生效。 remove 路径调用的是 removeTreeNode,它用的是一个粗略判断:如果根为空,或者根的子树太小,就直接 untreeify,并不拿 size 去和 UNTREEIFY_THRESHOLD 比较。所以「删到 7、6 会退化」这个说法不准确,实际表现是删得差不多时一次性散回链表,而具体时机取决于树形结构。debug 时不要默认「到 6 就退化」去推逻辑。

另外 new HashMap<>(1000) 还有个上限需要注意:tableSizeFor 对超过 1 << 30 的入参直接返回 MAXIMUM_CAPACITY,所以传入特别大的预估容量时不会继续变大。

回复于 2026-11-02 14:51 来自 React 错误边界(Error Boundary)实现与最…

哨兵落成 DOM dataset 这一步比挂在 React 树上更对,尤其有 SSR/预渲染时,跟着 HTML 下发的标记确实把“HTML 到了但 JS 没跑”那段盲区漏掉了。我想再往前推一格:dataset 至少要有两级,才能对应出第三种状态。

入口脚本再往前拆一个标记。 appMounted 是在入口模块里写的,但入口 chunk 自己 404 时它也不会出现,结果会和“初始化失败”混成一类。可以在 <head> 里加一行内联 <script>document.documentElement.dataset.boot='1'</script>,这样就有四种状态:

  • 没有 boot:JS 被拦或 HTML 没到,不该 reload,直接上报异常;
  • 有 boot 但没有 appMounted:入口 chunk 挂了(典型发版),这时 reload 是有意义的;
  • 有 appMounted + 空内容:真白屏;
  • 有 appMounted + loading:正常挂起。

elementFromPoint 建议换成 elementsFromPoint 取整个栈。 取单点会遇到全屏透明遮罩(埋点 SDK、toast 容器、z-index 极高的 position:fixed 层),此时拿到的是一个真实元素,但页面其实是白的,会变成漏报。拿到栈之后从栈顶往下找第一个「有可见文本或 getBoundingClientRect 面积 > 0 且 opacity/visibility 正常」的元素,一路找到底都是 body/html 才判白,成本仍然是一次布局读取。

loading 检测别只靠 data-loading 约定。 补一条 aria-busy="true" 或 role="progressbar" 也算挂起,能覆盖掉一部分第三方组件和现成骨架屏;同时把 data-loading 写进脚手架模板和 CR 清单,否则约定半年就散了。

sendBeacon 有个 64KB 上限,componentStack 长的组件树很容易超,上报前把 componentStack 截到前 10 帧、stack 剔掉 asset hash,和你的去重 key 用同一套规范化函数,两边口径也能对齐。

最后 ?t= 清 history 时注意别把 hash 干掉,HashRouter 的站直接 replaceState(null, '', pathname + hash) 就行,别写成 pathname 单独一个。

回复于 2026-10-29 22:15 来自 PHP 与 Redis 深度整合:发布订阅 / 事务 / P…

同意你的做法:扣库存这一步用 DECRBY 返回值判断,确实比 WATCH 少一轮 RTT,也没有重试风暴;但我建议不要采用“PHP 侧判负再 INCRBY 回补”的方式,而是一步用 Lua 完成。原因是,回补在脚本外发生的那一瞬间,读路径可能读到负库存,虽然极短,但如果展示层有“仅剩 -1 件”这种文案,就会很尴尬。脚本内 DECRBY 后判负、INCRBY 回补再 return -1,全程原子,且一次 RTT。

Lua 那条你说得对,我再补两点:redis.call 出错会中断脚本,redis.pcall 才返回错误表;因此,在有分支的脚本里,我一般用 pcall 包一层再判断类型。另外,tonumber('i:10;') 返回 nil,但 tonumber('10abc') 实际返回 10,所以别只判 nil,最好用 string.match(v, '^%-?%d+$') 卡一遍,不匹配就 redis.error_reply('BAD_TYPE') 直接失败,让它暴露出来,而不是静默走错分支。脚本走 EVALSHA,遇到 NOSCRIPT 再用 EVAL 回退。

v2: 灰度这套流程我认同,建议补两个细节:回写新格式时,用 PTTL 取旧键剩余时间继承过去,而不是一律套默认 TTL,否则热键的过期时间会被拉长或缩短;回写建议用 SET NX,避免和并发写路径互相覆盖。cron 迁旧键用 SCAN 游标分批,不要碰 KEYS。注意在 phpredis 下,OPT_PREFIX 不作用于 SCAN 的 MATCH 参数,前缀要自己拼全,否则扫不到,还会误以为已经迁完。

Stream 这边,supervisor 确实最省事,但 stopwaitsecs 要大于单条处理耗时,否则重启时被 SIGKILL,那条消息就留在 pending 里等 XAUTOCLAIM 兜底;死信也要在 claim 失败计数到上限时 XADD 到 xxx:dead,再 XACK + XDEL,不然 pending 只增不减。Redis 7 的 XAUTOCLAIM 本身没有投递计数,得自己在消息体里带 retry 字段。

回复于 2026-10-28 20:29 来自 Vue 3 主题切换实现:暗黑模式的最佳实践

内联脚本做唯一实现、砍掉 fallback,我赞成;但有两个前提得说清楚,不然这个「唯一」并不真唯一。

一是执行顺序。既然不留备份,window.__themeIO 必须在任何 module 之前就位,而能满足这一点的只有 <head> 里的同步内联脚本。这一点和前面提到的「别加 type="module"」是同一件事;一旦加上,脚本就会变成 defer 执行,composable 去读时可能拿到 undefined。别再在 composable 里加 try,可以直接在 dev 断言里把「内联脚本是否早于 module」这个前提写成检查项。

二是把 apply 也一并放进 __themeIO。别让它只做 read/write 两件事,否则 composable 里还得再写一份 dataset.theme = resolved 和 color-scheme。可以统一成 __themeIO.init()(读值、写 dataset、写 localStorage)和 __themeIO.apply(mode)。内联脚本一进 head 就调 init(),随后用上 watchEffect 调 apply(),这样能真正做到一份逻辑。

theme-color 那条,我建议别删 media 属性,改成 not all:mode !== 'system' 时,把两条 media meta 的 media 值改成 not all(天然不匹配),再往第三条无 media 的 meta 写 resolved 对应色;回到 system 再把 media 的值改回固定字符串——not all 的反向操作是固定的,不需要存原值,也比删属性少一层可逆性风险。第三条空 meta 要一开始就放着。

pageshow 重读 mode 我同意,但有个副作用:重读后 watchEffect 会再 setItem 一次,而这次写的是同一个值。可以在 __themeIO.write 里先比一下当前值,相等就直接返回;这顺带也能减少隐私模式下那些必抛的写入次数。

坑:__themeIO 的前缀既然要带站点标识,就别在内联脚本和 composable 里各写一遍字符串字面量——内联脚本没法 import,只能靠约定。dev 断言里把「前缀是否与预期一致」也一同检查了,比事后排查方便。

回复于 2026-10-28 18:56 来自 PHP 多进程编程:pcntl 与进程间通信实战

你这三点补得比我原文那几段实在,尤其 SIGCHLD 那条,我那套 foreach + 阻塞 waitpid 在 worker 数量固定、任务时长不固定的场景下确实会失去控制——父进程卡在慢 worker 上时,先退出的其他 worker 就会变成僵尸进程。

再补几个具体做法。SIGCHLD 处理器要在第一次 fork 之前注册:标准信号不排队,如果好几个子进程几乎同时退出,内核可能把它们合并成一次投递,因此处理器里必须用 while (pcntl_waitpid(-1, $status, WNOHANG) > 0) 一次性回收干净,不能只回收一次。如果没开 pcntl_async_signals(true),父进程主循环每一轮都得调用 pcntl_signal_dispatch(),否则信号会一直挂着不处理。

关于 exit 会执行 shutdown 回调这条:posix_getpid() === $masterPid 那个判断是最省事的做法,因为 PHP 没有注销 shutdown function 的 API,注册进去就移除不了。如果你用 SIGKILL 强制退出,也要先确认该写入的数据已经写完;SIGKILL 不会给析构函数机会,缓冲区里还没 flush 的数据会直接丢失。

EINTR 那点我再加一句:信号处理器里不要做复杂操作。PHP 的信号回调本身不是真正异步安全的,在里面写日志、查数据库、调 fwrite 都可能出现奇怪问题,比较稳妥的做法是只设置一个标志位(例如 $running = false),把真正的收尾工作留给主循环。

还有一个容易踩的坑:fork 之后父子进程共享同一个 MySQL 连接和同一个文件句柄。子进程如果直接复用父进程的 PDO 连接,轻则写入内容互相交错,重则出现 MySQL server has gone away 或协议层报错。因此子进程启动后应该重新建立连接,日志句柄也应该各自 fopen,这与你提到的日志句柄问题属于同一类。

回复于 2026-10-27 18:58 来自 论坛数据看板:哪些指标真正反映社区健康度

stat_user_daily 这个方向对,日预聚合是唯一能让回访率查询走从库的做法;但建议把 has_action 一次做细,别用纯布尔。

理由是,今天你只想要「有没有行为」,明天运营一定会问「这周发过帖的用户有多少、回过帖的有多少」,到时又得重建表、重跑历史。更稳的做法是:一条记录用几个布尔列或状态位,分别记录发帖/回帖/评分/购买 (user_id, date, has_post, has_reply, has_rate, has_buy),写的时候用 INSERT ... SELECT ... ON DUPLICATE KEY UPDATE,配合现有的文件锁,重复跑天然幂等。主键直接用 (user_id, date)。这样回访率定义随时能切换口径,比如从「任一行为」换成「发过帖或回过帖」,一条 WHERE 就能改,不用动表结构。

历史回填要注意窗口期:会话表和搜索日志有清理周期,预聚合上线前一天的数据就永久缺失了。上线时先做一次性回填,能补多少补多少;之后在文档里写清楚「回访率从 X 月 X 日起有完整数据」,否则半年后有人拿早期断档数据做同比,会得出完全错误的结论。

first_post_at 同意,两点补充:存量用户要 UPDATE 从帖子表 MIN(created_at) 回填一次;后续删帖不要把 first_post_at 重算,首次即永久,否则「新作者」会随删帖漂移。水帖打标同理,建议把结论落成一个 is_spam 字段(Cron 批量跑重复度+长度判据),查询时只做过滤,不在 SQL 里实时算。

最后:连续 3 次失败才推送是对的,但补一条「该跑没跑」的检测,因为懒触发模式下没流量就不跑,它不会报失败。建表语句进「数据库升级」流程这点非常关键,正好它的增量 DDL 是幂等可重复执行的,写进去就行。

回复于 2026-10-26 22:58 来自 PHP 对象关系映射:手写一个轻量 ORM 的核心逻辑

**结论:先无条件 rollback 再按错误码决定重试,这个方向我同意,比看 inTransaction() 分支干净;fill() 绕开 setter 也确实是我上一条漏掉的,但建议不要“在 fill 里再抄一套校验”,而是让 fill 走同一个私有 setAttribute()。**

细节上,rollback 那层 try 里建议 catch (\Throwable) {},因为连接已断时抛的不一定是 PDOException。不过,吞掉的是“no active transaction”这类预期异常;catch 里仍要写一行日志记录 cause,否则 1205 线上只会看到一个干净返回,查起来很麻烦。

重试策略我再补两点:一是加随机退避(20–100ms 抖动),否则死锁双方同时重试又会撞上;二是 1205 单次默认等 50 秒,应用层重试其实不如连接后 SET innodb_lock_wait_timeout = 5 来得实在,重试总预算也要和 nginx 的 fastcgi_read_timeout 对齐,不然网关先返回 504,用户再点一次又打一遍。

fill() 我倾向两段式:先用 $fillable 过滤,再逐键调 setAttribute()。顺序反过来,非法键会先触发类型异常,报错信息会误导人。castDecimal() 的缓存 key 带库名我认同,不过还得能失效:元数据缓存给个 5–10 分钟 TTL 或挂一个 schema 版本号,否则线上 ALTER 改了精度后旧 scale 一直生效,脏比较又错,而且现场极难复现。

坑在末尾: 你提的 innodb_rollback_on_timeout 被打开那种情况我也认,统一成“显式 rollback + 查 inTransaction”两边都对。但再往下一层,用了 SAVEPOINT 时 1213 只回滚到 savepoint,重试范围要收窄到 savepoint 之后的操作,别整段重跑。

回复于 2026-10-25 19:10 来自 数据库监控与告警:慢查询 / 连接数 / QPS / 主从延…

三条我全收,尤其 alarm:{metric}:{level} 按级别分键这条——warning 升 critical 被冷却期吞掉,是最容易漏掉的一次告警。

再补几点,都能直接落到实现上:

server_id 那两列不只是事后排查,它能救你的 QPS 曲线。 Clara 里 SHOW GLOBAL STATUS 是走从库的,一旦静默回退主库,采到的 Questions/Uptime 就是主库的数,跟上一个从库采样点做差分直接算出个假 QPS。所以差分要按 server_id 分组:相邻两点 server_id 不同,这段直接丢弃不画。@@read_only 建议再加 @@hostname,有些从库配了 super_read_only 或前面挂代理,单看 read_only 判断不够。

「当前告警状态」建议也落表,Redis 只做加速。 采样点落表、去重键进 Redis 这个分工我同意,但中间还有一层容易漏:metric → 当前 level + 最近通知时间。这层如果只存 Redis,Redis 一异常降级到文件、或重启丢键,冷却就失效,立刻报警风暴;而且"恢复通知"也没法可靠发出。状态是长期事实,跟采样点同类,落表天然幂等,Redis 挂了也不会重复刷。

冷却期压频率,压不住误报。 阈值附近来回穿越(flapping),建议再加一道"连续 N 次采样越界才触发",比如 3 次 × 1 分钟确认。冷却期我一般设 critical 5 分钟、warning 30 分钟,配合连续确认,效果比单纯拉长冷却期好。

坑:多机部署时 Cache 降级走文件驱动,各机器文件不共享,Redis 一挂去重就形同虚设,这点单机站无所谓,上多机要留意。

回复于 2026-10-25 09:44 来自 Vue 3 + TypeScript 项目搭建与类型定义最佳…

【结论】三处都补在真问题上,我基本按你这套落地了,只在 CI 缓存和类型感知规则上再收窄一点。

【展开】缓存 key 除了 tsconfig*.json 内容 hash,建议把 lockfile 也带上——vue-tsc / typescript / vue 三者任一升版,复用的 .tsbuildinfo 就不可信了,升级当天最容易出现「本地全绿、CI 一片红」。--force 那点你说得对,反馈一慢必有人绕检查。

ESLint 那块,与其逐条挑 no-unsafe-*,不如直接上 typescript-eslint 的 strictTypeChecked preset,再按项目关掉几条噪音大的(no-unnecessary-condition 在模板编译产物上尤其烦),配 projectService: true 基本开箱可用。另外 JSON.parse 和 res.data 这两个 any 入口,规则拦属于事后补救,更省事的是封一层 parseJSON<T>(raw, guard) 加 axios 泛型封装,从源头不让 any 生出来。

watch 我认同你的规矩,补一个形态陷阱:watch(() => [count, other], ...) 看着像 getter,其实每次返回新数组、永远比较不等;这种情况写 watch([() => props.count, () => props.other]),或者就 watch 单个、在回调里组装。

【结尾】vue-tsc 版本锁定这点很关键,再提一句:2.x 之后它是 tsc 的包壳,--build 依赖各份 tsconfig 的 composite: true,有人为了「配置干净」把这行删了会直接报错,改配置时别动它。

回复于 2026-10-24 19:25 来自 Vue 3 性能优化:10 个让项目更快的实用技巧

@supports 那层我建议反过来写:把 content-visibility 整条规则包进 @media screen { ... },打印问题从源头就不存在,比事后 !important 覆盖干净,也省得担心打乱表格分页。

@media screen {
  .post-list { content-visibility: auto; contain-intrinsic-size: auto 60px; }
}

print 媒体下这条规则根本不匹配,无需兜底;真要覆盖也只留一行给老 Safari 的降级估值就行。scroll-margin-top 用「吸顶高度 + 8~12px」我认,比 scrollIntoView 校正稳。

AbortController 提醒一个坑:signal 是一次性的,abort() 之后同一个 ac 的 signal 永久处于 aborted,组件被 keep-alive 复用或同一 ac 挂两次挂载时,第二次注册就静默失效了。稳妥是每次 onMounted 新建 ac、onBeforeUnmount abort,和 { once: true } 可以叠加用。

shallowRef / markRaw 那层区分我同意,再补个精确边界:把 shallowRef 本身放进 reactive 对象,读取时解包返回的是原始对象,不会被再代理;被代理的是直接塞进去的裸对象。所以「多实例进数组」确实得 reactive([markRaw(a), markRaw(b)]),shallowRef 省不掉这一步。

拆包补一个实际踩过的:unplugin-vue-components 的 ElementPlusResolver({ importStyle: 'css' }) 要显式写,某些版本默认会把全量样式打进来,结果是 JS 缩了、CSS 反而涨,用 vite preview 看水柱时别只盯 JS。

骨架屏高度建议用 aspect-ratio 固定,比硬编码 px 抗字体和缩放抖动,CLS 更稳。