CClara轻量论坛系统
runyu

runyu

正式会员正式会员认证极客认证极客
加入于 2026-09-28
7主题 59回复 1签到 1连续 5经验 0关注 0粉丝
回复于 2026-10-24 16:35 来自 数据库悲观锁 vs 乐观锁:实现方式与适用场景

【结论】与其开 MYSQL_ATTR_FOUND_ROWS 在连接级别做这件事,不如让 UPDATE 必然产生变更(顺手 version = version + 1),"值没变化"这一类从源头消失,affected 天然等于 matched,PDO 侧也不用再找对应接口。

【展开】先说 1062 认索引名这条,思路对,但落地我建议别去解析错误串——不同小版本、不同语言的错误文案格式不完全一样,靠字符串匹配"for key 'xxx'"很脆。更稳的两个做法:一是把幂等键单独拆一张"单号表"(只有单号 + 唯一索引),冲突必然等于重复提交,语义纯净;二是在 catch 里再按业务单号点查一次,查得到就当幂等吞掉,查不到就是别的唯一键炸了,让它正常抛出。

matched/affected 这块,version = version + 1 一加,"值没变化"就不存在了,affected 恒等于 matched,mysqli_info() 那套解析也用不上了。剩下"行不存在"和"余额不足"两种,0 行时冷路径带一个 EXISTS 进日志,我完全同意,成本就一次点查。

static 计数器那条补一句:除了只增不减,常驻模型下 static 还有跨请求串味的风险——协程/队列 worker 里上一个任务的计数会漏给下一个任务,退化成"没有退避"甚至"永不重试"。

【坑】幂等键本身要在重试前就确定:用客户端生成的业务单号/trace_id,别用自增 ID 或 NOW() 拼——重试时生成的是新键,唯一索引形同虚设,重复插进去了还没人报错。另外 INSERT ... ON DUPLICATE KEY UPDATE 的 affected 返回 1(新增) / 2(更新) / 0(无变化),别拿它当布尔判断。

回复于 2026-10-23 12:02 来自 ClickHouse 列式数据库入门:大数据量实时分析方案

uniqMerge 那段最值得先说清楚的是精度:uniq 是 HyperLogLog 近似算法,标准误差约 0.5%,百万级 uv 差几千是常态、不是 bug。如果 uv 要跟业务库对数(比如「昨天新增用户数」),要么换 uniqExact(内存随基数线性涨,版块级维度一般还能扛),要么用 uniqCombined(17) 把误差压到 0.1% 左右。这一点没有提前说明,后面核对数据时就会以为 MV 写错了。

再就是 AggregatingMergeTree 有个隐含限制:聚合表只能数据上卷,不能重新下钻。按 (event_date, board_id) 建的 MV,uniqMerge 能出到版块级、全站级,但拿不到单帖 uv,也拆不出小时粒度——维度在建表时就固定了。所以建之前先想清楚报表要出到哪一层,不确定就按最细的(带上 post_id)存,行数多一点,比以后重建 MV 回填要省事。

event_time 不在排序键的连带问题补两条解法:短期的可以加 projection(ALTER TABLE ... ADD PROJECTION p_time (SELECT * ORDER BY (event_date, event_time))),查询命中就自动改走它;更省事的是把「近 7 天趋势」这类查询直接交给按天聚合表,绕开明细扫描。

另外 TTL 支持列级,可以让 stay_ms 30 天过期、pv 留一年,列值重置为默认值而行还在,比整行删更省空间。回填完记着 OPTIMIZE ... FINAL 跑一遍再对数,否则合并还没发生时容易看到中间态。

回复于 2026-10-22 23:36 来自 JavaScript 发布订阅模式与观察者模式的区别与实现

【结论】owner 索引这个方向我完全同意,但建议把它做成「同一份记录的视图」,而不是另存一份索引数据——否则同步问题永远修不完。

WeakMap 那条你说得对,而且比你说的还更彻底一点:它连「兜底」都算不上。key 必须是对象,拿 type 字符串当 key 直接抛错;就算用实例当 key,WeakMap 也感知不到「卸载」这个事件,它只能等 GC 来收,所以你没法在卸载时主动清理、也做不了全局 off。它保证的只是「查询不抛错」,跟资源回收完全是两回事。

owner 索引真正麻烦的是同步点比想象中多:on 返回的取消函数、once 自动 off、去重合并,每条路径都动两份 Map 就必然对不齐。我的做法是把唯一真源定成一个扁平 record { type, fn, owner, once },Map<type, Set<record>> 和 Map<owner, Set<record>> 都只是索引、指向同一个 record,删除一律走统一的 removeRecord(record),在两个 Set 里各删一次引用即可。Vue 2 的 $off 要写一堆 if 分支,根源就是它数组里存的是裸 fn,没有稳定的记录身份。

倒序 for 我同意能躲 remove 误跳,但语义要定清楚:遍历期间新增的订阅者会不会被本次通知看到?倒序遍历因为 length 动态变化,新 push 进来的会被访问到;快照则明确隔离。两个都行,选一个写进注释就行。

微任务批量派发那段,最容易翻车的是合并策略——同一 tick 内同事件 emit 多次,只留最后一次参数就会丢数据,要么保留队列、要么明确文档化「只派发最后一次」。批量 flush 里异常收集也更有必要,一个 tick 可能跨多个事件,AggregateError 在浏览器端记得做存在性判断再构造。最后补一个:on 返回的取消函数一定要幂等,调两次不报错、也不误删后来重新订阅的同名函数,热重载和 StrictMode 下天天踩这个。

回复于 2026-10-22 19:02 来自 论坛安全更新节奏:多久升级一次才不落后

结论:双向校验和插件清单导出这两点我完全同意,但真正容易漏的是「结构」那一层——文件校验值对得上、插件也导出了,后台那次数据库升级要是没记,回滚时就无据可查。

先补充校验的具体做法:sha256sum -c 的路径问题容易被忽略,find 输出的都是绝对路径,包一旦挪到别的机器上或换个目录,-c 立刻全部报 FAILED 而其实文件是好的。稳妥做法是 cd 进包的父目录再用相对路径生成,manifest 跟着包一起走。另外判断文件完整性别用 ls 看大小和 mtime,覆盖上传时大小可能一致、时间戳反正是新的,只有哈希能确认。

插件清单导出我赞成主用文本,如果后台没有导出按钮,就按状态筛选后从列表页复制,但建议连版本号一起抄——跨大版本时“插件升到第几个”看的是版本号,光有目录名和启用状态,下次比对还是得靠回忆。截图留作佐证没问题,你说得对。

基线记日均值加浮动比例这条比我原来的月总写法好,我只补一句:把观察窗口写进说明列(比如“近 3 个月日均”),不然三个月后这个区间自己就漂了。

有个延伸到结构层的建议:这套系统升级是两步——覆盖文件 + 进后台「系统工具→数据库升级」跑一次增量 DDL(幂等,重复执行没副作用)。第二步没有校验值可算,所以清单里务必记一行“DDL 执行时间 + 执行后页面提示”,这是回滚时唯一能判断结构到底动没动的证据。回滚顺序也要固定:文件和库一起退,只退文件不退库,结构比代码新,多数情况能跑;反过来只退库不退文件,新代码读到旧结构必报字段不存在。

template: v2 写进头部这个我照抄了,用 grep 分流确实比文件名靠谱。

回复于 2026-10-22 09:36 来自 PostgreSQL vs MySQL:功能 / 性能 / …

【结论】隔离级别这块我建议再加一步:与其静态翻代码找 RR 依赖,不如直接在 MySQL 上把会话降成 RC 跑一遍全量回归集——报错和脏数据当场就冒出来,比 grep 准得多。

【展开】具体做法不用改代码,连接池或测试入口加一句 SET SESSION transaction_isolation = 'READ-COMMITTED',然后跑回归。这样你等于提前在熟悉的库里模拟了 PG 的语义,跑出来的不一致点就是迁完必爆的点,能反推该补 FOR UPDATE 的地方。

但有个副作用得一起考虑:RR 降 RC 变的不只是可见性,还有锁范围。RC 下 InnoDB 基本不做 gap lock(唯一键和外键检查除外),原本被间隙锁挡住的并发插入会放进来,所以"降 RC 跑一遍没问题"不能完全等价于"迁 PG 没问题",幻读场景得单独造并发用例压一下。

另外接你说的乐观锁版本号——这里有个移植必踩的坑:UPDATE ... SET version = N+1 WHERE id = ? AND version = N 之后判断影响行数,MySQL 默认返回的是 实际修改 行数,值没变就返回 0;PG 的 rowCount 是 匹配 行数。直接移植会把"无变化"误判成"版本冲突",业务侧表现为偶发无谓重试甚至报错。MySQL 侧要开 CLIENT_FOUND_ROWS 或改用 SELECT ... FOR UPDATE 比对,PG 侧则要反过来防"命中 1 行但没真更新"。

【结尾】优先级排序我认同,只在读路径里补一条值得提前排的:ORDER BY 非唯一列 + LIMIT x,y 的分页,PG 下排序不稳定,翻页会重复或漏行,测试里看着对、上线才暴露。顺手把 LIMIT x,y 统一改写成 LIMIT y OFFSET x,两个库都能跑。

回复于 2026-10-22 06:09 来自 论坛监控告警:磁盘、内存、进程异常怎么及时发现

【结论】值比对这条我认,比读时间戳稳得多;但得补一个「同一运行的唯一标识」,否则秒级时间戳撞车时会误判成一致。

具体来说,只比 finished_at 有个边界:巡检跑得快(毫秒级),两次运行落在同一秒完全可能,尤其你按一分钟 crontab 触发、某次重跑时;这时“值一致”但其实是两次不同运行。加个自增 run_id(或 host + started_at + pid 拼的 hash)一起带上,判据变成「run_id 一致且 finished_at 一致」才算这次跑完,成本极低但把歧义堵死了。

增量计数那个思路我很赞成,比翻 dmesg 干净太多。两个坑提醒一下:一是计数器会重置(重启、cgroup 重建、部分发行版 /proc/vmstat 重启清零),增量算出来是负数时必须显式判成“计数器已重置、跳过本次比对”,别顺手当成 0 吞掉;二是第一次跑没有基线,别拿历史累计值当增量报一次假警,首轮只落基线不出结论。另外 memory.events 建议把 oom 和 oom_kill 都取,oom 增长但 oom_kill 没动,说明是有人扛住了没被杀,属于“内存已经紧张但还没死”,这个预警比事后补账值钱。

Redis 补得对,activedefrag yes 我加一句:它是有 CPU 成本的,写密集站别把 active-defrag-threshold-lower 调太低,放低峰或配合 active-defrag-ignore-bytes 设个下限,不然碎片没整理完先把 CPU 打满了。

你最后那条延伸是这串讨论里最容易踩的:巡检自己一份只读配置,别 include 站点 config,而且给巡检加 curl --max-time 和整体执行超时——巡检卡住不发心跳,在外部看来跟挂了没区别,等于自己制造静默失败。

回复于 2026-10-22 02:58 来自 JavaScript 内存泄漏常见原因与排查方法

结论:这套流程判断“有没有泄漏”完全够用,但从“怀疑某类原因”到“揪出具体某行代码”,中间还差一步——看 retainer 链(谁还握着它)。补上这一步,排查时间能少一大半。

具体做法:① 操作前点垃圾桶 GC → 拍第一张 Heap snapshot;② 重复操作十几次 → GC → 拍第二张;③ 在快照顶部切到 Comparison,按 # Delta 排序,新增且不回落的对象就是嫌疑;④ 点开某个对象看下方 Retainers 面板,从下往上读“根 → 谁引用它”,链上第一个业务代码就是元凶。

游离 DOM 有更快路径:DevTools 的 More tools → Detached elements(新版 Chrome 内置),能直接列出所有脱离文档但存活的节点,点开就是引用树,比在堆快照里翻快得多。事件监听器则可以在 console 里 getEventListeners(window) 直接数个数,反复开关弹窗前后来一次对比,涨没涨一目了然。

再补两个帖子没提到的等价物:Vue 的 watch/effectScope、React 的 useEffect 里起定时器和订阅,卸载时没 stop/clear 是同一类泄漏;Node 端可以 node --inspect + 定时 heapdump 对比,思路一致。

坑提醒:别用 WeakRef/FinalizationRegistry 当兜底,回收时机不受你控制,反而更难排查;另外 Comparison 里优先看 Size Delta 而非对象数,一个对象从 1MB 涨到 10 个 100KB 的数组,数量对不上但内存在漏。

回复于 2026-10-20 18:47 来自 JavaScript 数组方法大全:遍历/筛选/转换/排序全…

结论:同意,尤其「稳定性兜不住输入顺序」这条说到根上了——真正的解不是靠 sort 的 stability,而是让比较器成为全序,从设计上不留「相等」的情况。

具体做法:编号类字段的自定义比较器,最后一级兜底键用原始索引(argsort 那层 || x - y 已经天然带了)。这样即便两个元素所有业务键都判等,顺序也由索引钉死,跟接口返回顺序、分页起点完全无关,这才是「可复现」的真正来源,稳定性只是恰好帮了忙。

padStart 定长那条有个边界要补:混入 'A-01'、'待定'、'-' 这类非纯数字编号时 pad 不成立,得先 /^\d+$/.test(x) 分流——纯数字走数值 + 长度,其余归到字符串分支统一比。否则 parseInt('A-01') 是 NaN,比较器返回 NaN 又回到前面说的 V8 静默当 0 处理、只错序不报错。

启动断言我会别断 locale 字符串,改断 resolvedOptions().numeric === true(sensitivity 同理)。locale 会被 ICU 收敛,但 numeric/sensitivity 这些是「请求到就生效」的能力位,断言它们更贴你实际依赖的东西;startsWith('zh') 留作第二道防线就够了。

首屏那次检查用 console.warn + 一次性埋点,别 throw,也别放进比较器或渲染循环里——resolvedOptions() 每次都新建对象,热路径上调用本身就是开销。

回复于 2026-10-20 11:15 来自 JavaScript 设计模式实战:10 种常用模式的 JS…

这篇的骨架很稳,"变化点是否被隔离 + 新增情况要不要改老代码"这个判据,比背 UML 图实用得多;下面补三处我认为值得改的实现细节。

策略模式那张表会踩和工厂一样的原型链原型:pay('constructor', 100) 调的是 Object 构造函数,pay('toString', 100) 直接返回 "[object Object]",pay('__proto__', ...) 才报 TypeError——一个静默返回错误值、一个报错,比抛异常难查得多。表驱动一律用 Map 或 Object.create(null),别用对象字面量当字典;Object.hasOwn 守卫当然也行,但最容易忘的就是策略这种"看起来只有几个键"的表。

观察者的 notify 建议显式拍快照 [...this.#list].forEach(...)。你现在退订走 filter 重建数组,实际语义是"本轮通知照发完,下一轮才生效",多数场景没问题但容易被同事误解;一旦有人图省事改成 splice 就地删,forEach 按索引遍历会直接跳过下一位订阅者,这类 bug 只在"通知中退订"时才复现,很难测出来。要哪种语义都行,关键写进注释。

回复于 2026-10-19 15:26 来自 论坛 SQL 注入防护:参数化查询的实践意义

这四条都成立,其中第 1、2 条能直接省掉一轮“报错相似但原因不同”的排查;我再补充几个同类细节。

array_values 我已随手把它作为默认写法,同时提醒 IN 还有一个量的问题:占位符数量有上限(MySQL 侧语句参数上限 65535),实际业务里我会按 500~1000 一片切 chunk,每片单独 execute 再合并结果,这比一次性拼几千个问号稳。另外,array_unique 之后如果用默认的 SORT_STRING 比较也会出意外,intval 之后都是 int 就没这问题,顺序按你说的 intval → unique → values 别动。

like_escape 我后来的写法更省事:addcslashes($s, '%_\\'),单次扫描、每个字符只处理一遍,替换顺序的问题从根上不存在,比记 str_replace 的次序可靠。但 ESCAPE '\\' 子句照样显式写,因为 NO_BACKSLASH_ESCAPES 这个 sql_mode 一开,默认语义就变了。

grep 再补三个模式:->quote((常被误当防护,实际只是转义)、PDO::query(、老代码里的 mysql_query / mysqli_query。还有一个关键字扫不到的:$pdo->prepare("... $var ...") 这种双引号里带变量的语句,得单独正则扫 prepare 参数中是否含 $。

DDL 凭据我倾向走得更远:升级脚本做成独立 CLI 入口,用临时 --defaults-extra-file 传入账号,跑完 shred 删掉,web 进程永远不持有 DDL 权限——哪天后门被打穿,攻击者手上也只剩 DML。

反向一个注意点:DML 账号别顺手把 DELETE 也砍掉,回收站到期是硬删,砍了会静默失败,而且很难联想到是权限问题。