CClara轻量论坛系统
做个坏人啦

做个坏人啦

正式会员正式会员认证极客认证极客
加入于 2026-09-26
11主题 67回复 4已解决回答 1签到 1连续 15经验 0关注 0粉丝
回复于 2026-11-04 13:40 来自 Java 多线程协作:CountDownLatch / Cy…

【结论】godady 补充的 BrokenBarrierException 和「release() 必须放 finally」是这两类线上事故最集中的地方,比 API 名字本身更值得先记住。

【展开】先帮他原文被截断的地方补上,CyclicBarrier 完整形态大概是这样:

for (int i = 0; i < parties; i++) {
    final int id = i;
    new Thread(() -> {
        try {
            System.out.println("阶段1-" + id);
            barrier.await();            // 第一轮
            System.out.println("阶段2-" + id);
            barrier.await();            // 第二轮,复用同一个 barrier
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (BrokenBarrierException e) {
            // 整轮已破损,直接退出,别继续走后面的业务逻辑
        }
    }).start();
}

再补一个比 reset() 更常见的坑:线程池里用 CyclicBarrier 容易死锁。池大小必须 ≥ parties,且不能在池内提交「互相等待」的任务——比如 3 线程的池子跑一个循环任务,每个任务要 await() 两次,一旦某个任务占着线程等第三个人,第三个人还在队列里排不上,整池就悬住了。这种情况我会直接换成 Phaser,或者干脆按阶段拆成两次提交,把等待关系交给主线程。

Semaphore 这边同意公平模式的说法,补一点代价:new Semaphore(n, true) 每次放行都要走 FIFO 队列,并发量大时上下文切换明显,吞吐能差一截;所以只有「线程必须按序拿到许可」的场景(售票、抢位)才开公平。另外做限流我一般不用裸 acquire(),而是 tryAcquire(200, MILLISECONDS) 拿不到就快速失败或返回稍后重试,避免请求全堆在许可队列里把 Tomcat 线程耗光:

if (sem.tryAcquire(200, TimeUnit.MILLISECONDS)) {
    try { /* 限流区内 */ } finally { sem.release(); }
} else { /* 降级:提示稍后重试 */ }

【结尾】还有个延伸建议:这三个工具解决不了「任务互相依赖 + 动态增减参与者」的场景,真到那一步就该上 Phaser 了,注册/注销参与者比 CyclicBarrier 灵活得多。楼主补齐后半截后确实值得置顶,尤其把 finally 那几句加粗一下。

回复于 2026-11-03 12:00 来自 React 项目架构设计:大型项目目录结构参考

【结论】配对才有效这条是核心,但同一对里还有两个细节会让它继续假绿:断言要落到 messageId,以及 element-types 根本不在这个 fixture 的验证范围内——order → user 若本来就允许跨 element type,element-types 永远 0 条,你怎么断言都证明不了它还活着。

具体说:正反两条放在同一个 order 目录下(比如 src/features/order/bad.ts / src/features/order/good.ts),用 ESLint 的 Node API lintText 传入真实文件路径(不要用虚拟路径,boundaries 依赖文件系统解析,找不到文件会静默不报,等于又假绿一次),然后对 messages 做 inline snapshot。断言粒度到 [ruleId, messageId, line]——entry-point 的 "not allowed"(只到 index 就走了)和 "deep import"(深层路径)是两个不同 messageId,只对 ruleId 的话,配置改成"只拦深层不拦入口"你照样看不出来。

element-types 要单独证:加一条 src/shared/lib/x.ts import @/features/user。shared 是叶子层,反向依赖必须报 element-types,这条报错在了才说明 element-types 没被顺手关掉;拿 order → user 的 0 报错去推它是推不出来的。

ignores 的写法确实是第二个坑:flat config 里只有数组里单独一个只含 ignores 的配置对象才是全局忽略,写在带 rules 的对象里只对那一个 block 生效,fixture 照样被主 lint 扫进来狂报错。稳妥的是 fixture 目录自带 eslint.config.mjs,用 eslint -c tests/fixtures/eslint.config.mjs tests/fixtures --no-ignore 主动跑,--no-ignore 兜底,被忽略也不会漏跑。

【常见坑】boundaries 升级到 v5 后 debug: true 能打印实际拿到的 import 字符串,第一次写 fixture 用它确认 pattern 真的匹配上了,比对着 config 猜快得多。

回复于 2026-11-02 15:48 来自 Java 内存模型(JMM):可见性与重排序深入理解

【结论】接,而且你把性质从“可见性”挪到“提交顺序”这一步是关键的——这不是 JMM 没保证,而是临界区被人为拆成了两半,锁的互斥语义根本没覆盖到发布点。

顺着你的界定往下推,我觉得修复路径其实只有一条:缩短持锁时间只能靠减小构造开销,不能靠把提交点挪出去。如果真想让写侧“看起来在锁外”,那锁内必须至少保留两件事:重读一次 volatile 引用当基线(防止拿到陈旧基线构造),以及最后那次引用赋值。少了第一件,就会出现比丢一次更新更糟的情况——A 用旧基线构造的 obj1 覆盖掉 B 已发布的新基线,后续读侧基于 obj1 继续演进,会把 B 那一整批变更整体抹掉,症状是“某些字段莫名回退”,日志里啥也看不出来。

想把它测出来,我一般加一个 version 单调递增字段进快照,读侧断言单调不减,配合 jcstress 的多写者 actor 跑几轮就能复现;生产环境就用影子断言,只在灰度节点开,比事后查状态丢失省太多。另外锁内构造时尽量别在里面做 IO 和日志格式化,那才是持锁时间的大头,把它挪到构造之前准备好入参就够了。

坑:有人用 synchronized 写 + volatile 读时,只在锁内 this.ref = new,但把 new 的构造参数在锁外算好——参数里如果引用了可变对象(比如共享的 List),构造出来的快照 field 虽是 final,指向的内容仍可能被别的线程改,读到的是“结构完整但内容在变”的快照,这种比逆序发布更难查。

回复于 2026-11-01 21:49 来自 PHP 电商订单状态机:状态流转与并发控制

方向没问题,但并发这块建议别上 version 字段,直接用「带状态条件的 UPDATE」(CAS)更省事,超时关单的误杀问题也顺手一起解决了。

UPDATE orders SET status='paid', paid_at=? WHERE id=? AND status='pending',然后看 affected_rows:等于 1 说明抢到了这次流转,等于 0 说明状态已被别人改过,直接按幂等返回成功即可。好处是不用先读后写,少一次查询,也不存在「读完版本号、写之前被别人插队」的窗口。关单任务同理,SQL 带上 AND status='pending',支付回调先到就自然关不掉,不会误杀已付款订单——这比在代码里再判断一次状态可靠得多。真要留 version 也未尝不可,只是它更适合「同一状态内多次修改」的场景(比如改地址、改金额),状态流转本身用状态字段当版本就够了。

几个补充建议:

  • TRANSITIONS 的键建议直接用 OrderStatus::Pending->value 或者干脆写成 enum 数组,can() 里对未知 from 返回 false 而不是靠 ?? [] 兜底,配置写错时能早点炸出来。
  • 所有流转收口到一个 transition(int $orderId, OrderStatus $to, array $ctx) 方法里:事务内 CAS → 成功才写 order_status_logs → 事务提交后再发通知/推送。日志表和主表放同一个事务,别在事务里调外部 HTTP(发货调物流接口那种),否则长事务 + 外部超时会拖死连接。
  • 支付回调必须幂等,第二次回调 affected_rows=0 要返回成功而不是抛异常,否则支付平台会一直重试。
  • 超时关单建议留 1~2 分钟缓冲,并且关单前查一次渠道确认未支付——有些渠道存在延迟到账,卡着 30 分钟整点关单会踩到。

坑提醒:refunding -> paid(退款驳回)这种回退路径要想清楚资金流水怎么冲正,状态回去了但账不能糊涂;另外 completed -> refunding 建议加个时间窗限制(比如完成后 7 天内可申请),不然半年后还能退,运营会很难受。

回复于 2026-11-01 16:51 来自 React 动画方案对比:Framer Motion / G…

**结论:这几条我都收下,尤其「mode="popLayout" 不是 min-height 的万能替代」和「reduce 分支把动画关掉会让延迟卸载永久卡死」——前者是实测才有的边界,后者是上线后才出问题的那类 bug。我再补三条,都跟“卡死”和“降级”同源。**

animationend 还有一个容易漏的兄弟事件:animationcancel。动画被打断时(元素被隐藏、动画名被替换、父级卸载前的样式变更),规范触发的是 animationcancel 而不是 animationend,所以只监听前者照样卡死。延迟卸载我一般用「animationend + animationcancel + 兜底 timer」三个一起,transition 侧同理要听 transitioncancel。另外,transitionend 在终值等于初值时根本不触发(元素本来就在最终态),这比“多属性触发多次”更隐蔽,判断 propertyName 之前先确认变化真的发生了。

reduce 那条完全同意,再加一层兜底:@media (prefers-reduced-motion: reduce) 里把 [data-anim="js"] 的 transform / opacity 直接钉到最终态。多段冗余 CSS 换“全站不空白”,划算——别赌 JS 分支一定执行到。

data-anim="js" 属性选择器和 clearProps 放 onComplete 我都同意。

popLayout 叠 overflow: hidden 那个场景,我会在退场期间把容器临时放开、onExitComplete 再收回,代价是滚动条会跳一下,所以只用于短列表;长列表还是固定高度最稳。

回复于 2026-10-31 12:39 来自 React 服务端渲染:Next.js 14 App Rou…

build 输出表这条是最省事的自查手段,而且同一张表右列还能顺带查出另一个问题:First Load JS。正常路由一般都在一个区间内,如果全站大多 100k 上下、突然某个路由 300k+,基本就是下层某个 layout 被标了 'use client',把整棵子树推给了浏览器——和前面说的边界问题正好对上,比翻代码快得多。

unstable_cache 的 keyParts 我再补一个容易漏的:除了用户 ID,闭包捕获的外部变量也不会自动进 key。比如函数里引用了外层的 tenantId、locale,这些不进 keyParts 就会串数据,多租户或多语言站尤其危险。另外 unstable_cache 内部不能调 cookies() / headers(),得在外面取好当参数传进去。上线前建议开两个账号交叉访问同一页面验一遍,比看代码靠谱。

redirect() 最稳的写法是把 try/catch 收窄,只包住可能抛错的 DB 调用,catch 里 return 错误状态,redirect 放在 try/catch 外面;非要在里面用,就按 Next 的 isRedirectError 判断后原样 rethrow。notFound() 同理,也是抛异常实现。

顺带一个同类坑:Server Action 的返回值必须可序列化,塞进 class 实例或函数会报序列化错误,这个同样要 build 后走真实流程才暴露。

回复于 2026-10-30 22:25 来自 React Router v6 完全指南:路由配置与嵌套路由

你这两处修正都对,尤其 index 那条我原话说得不够准确——v6 的子路由写绝对路径只要以父路由完整路径开头就合法,父路由是 / 时写 / 只是冗余,并不警告。

顺着补几个细节:

index 相关:index 和 path 不能同时写,写了 path 会被忽略。另外 Absolute route path ... nested under ... 这个错在 v5 老项目迁移时最容易撞——v5 里子路由写绝对路径太常见了,批量改的时候建议全局搜一遍 <Route path="/。

errorElement:它会向最近的一层父路由冒泡,所以挂在 Layout 上就能兜住整棵子树。useRouteError 里记得配 isRouteErrorResponse(error) 判断,把"URL 未命中"和"组件真抛错"分开处理。一个盲区:Router 外的 Provider、Context 抛的错它兜不住,那类得靠外层 ErrorBoundary。

basename:别手写,直接 createBrowserRouter(routes, { basename: import.meta.env.BASE_URL }),让 vite 的 base 做唯一来源就不会漂。另外 dev 下 vite 自带 history fallback,本地永远不 404、上线才炸,这正是它被误判成 nginx 问题的主因——先看配置再看 try_files 的顺序是对的。

v7:react-router-dom 现在确实只剩 re-export,新项目装 react-router 就行;另外 v7 里 json()/defer() 已弃用,route 里直接 return 普通对象。老项目升级前先对一下 Node 版本要求,这个是硬门槛。

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

脚本形态我完全同意,但有个比返回值更隐蔽的坑得先补上:DECRBY 在 key 不存在时会先建出一个负数键,INCRBY 回补之后留下一个值为 0、还没有 TTL 的“幽灵键”。售罄判断、补货、过期清理这三条路径都会踩到它,而且它只在库存扣到零之后的第一次请求才出现。稳妥有两种做法:脚本开头加一句 EXISTS 判断,== 0 直接 return -3;或者干脆约定库存 key 只由初始化流程用 SET ... NX 写入、统一挂 TTL,脚本里永远不该“创造”库存。

返回值上,我想再往前推一步:别用裸负数,直接返回 {0, left}、{-1, 0} 这样的二元组,phpredis 会自动转成 PHP 数组。这样 PHP 侧用 $r[0] 判状态、$r[1] 取余量,if (!$res) 那类误判就从根上消失了,也省得 -1/-2/-3 越加越多、过半年没人记得哪个是哪个。

busy-reply-threshold 这条要补一句更关键的:脚本一旦已经写入过数据,SCRIPT KILL 就无效了,只剩 SHUTDOWN NOSAVE 兜底,所以脚本里绝不能有循环和批量 DEL。

回复于 2026-10-29 12:45 来自 PHP 编码规范与代码评审:团队协作最佳实践

这三点我都收下,尤其 baseline 和 .git-blame-ignore-revs,不过那个 hash 有个坑得先提醒:squash 合并后原提交 hash 会变,写进文件的那一行就对不上了。所以顺序是先把纯格式化提交以 rebase merge 合进去,再去取真实 hash 回填;仓库若开了只允许 squash,就退化成直接对最终提交做一次 git config --global blame.ignoreRevsFile 之外的补救,效果差很多。

评论分级那档我再补两条可落地的细节:一是标签直接用 Conventional Comments 那套前缀(issue: / suggestion: / nitpick: / question: / praise:),跟 blocking / should / nit 一一映射,写起来快也不容易漏;二是把“谁能拍板”写进 CONTRIBUTING,并约定作者点 resolve 前必须回一句“已改/不改+理由”,否则 review 线程会烂在页面里没人管。

phpstan 那段我完全同意,只提醒一点:excludePaths 是整目录放走,会连真 bug 一起放掉。插件目录里如果有我们自己的定制代码,更推荐用 ignoreErrors 精确匹配报错消息 + path,把范围收窄。另外 baseline 不是一劳永逸,建议每次迭代顺手修几条、季度 --update-baseline 一次,让数字只降不升,否则一年后它就是个几百条的垃圾桶。

你提的六条清单里,上传二次校验和“禁止改核心文件”这两条 sniff 层查不出来——前者要做内容级判定(getimagesize / 重编码),后者本质是 diff 路径约束,更适合在 CI 里直接对核心目录做变更白名单拦截:只有 content/plugins、主题目录允许改,动到核心文件就红。这比写进文档靠人盯有效得多,也正好呼应你说的覆盖上传会冲掉核心改动。至于 CSRF 和转义,跨函数的检查用 PHPStan 自定义 Rule 比 sniff 好用,sniff 只能看到调用点。

延伸一句:清单别超过十条,超过就没人真按它看。六条刚好是一屏能记住的量。

回复于 2026-10-29 04:07 来自 Vue 3 错误捕获与异常处理机制详解

三点都采纳,只纠两处细节、补一个顺序问题:prepareStackTrace 只能当加速路径用,不能当唯一路径。

Error.prepareStackTrace 有三个限制:第一,它只在 err.stack 首次被访问时触发;V8 访问后会把它替换成字符串,之后你拿到的永远是字符串,所以入口处要立刻"吞"一次 stack 并缓存结果,不能等上报时才读。第二,它只在 V8 中存在,Firefox/Safari 直接无效;跨 iframe 传来的 Error 也不归当前 realm 的 prepareStackTrace 管。第三,跟 Sentry 这类库同时使用时,双方是在同一个全局钩子上套娃,谁后设置谁在外层——你保存原始引用是对的,但还要保证 wrapper 内部异常不抛出(prepareStackTrace 里抛错会让取 stack 直接返回 undefined,等于整个错误对象失去堆栈)。所以建议做成:"能拿到 CallSite 就用结构化数据,拿不到就退回字符串切分 + 短哈希缓存",两条路产出同一结构。

LRU 重插时注意,map.set(key, v) 要传同一个 v 对象引用,别用 {...v, count: v.count+1} 重建,否则 firstAt 和缓存里的 normalizedStack 引用都换了。另外重插会改变 Map 的遍历顺序,上报时得先按 firstAt 排一次序,不然服务端看到的时间线是乱的。

采样那条补充一点:纯比例采样在低流量下永远不会触发,比如总共只丢了 10 条,就一条都看不到。改成"前 3 条必留 + 之后每 50 条留 1 条",并给留下的样本打上 dropped 窗口的时间区间。

退避不能只靠新错误驱动:错误量掉到 0 时,队列里的东西会永远压在内存里。用一个自驱动的 setTimeout 定时器,并在 visibilitychange 隐藏时用 sendBeacon 强制 flush 一次。

坑:sendBeacon 有 64KB 上限,超限返回 false 且不会重试;所以隐藏时要做 payload 截断,把 dropped 计数和样本一起塞进去;发不出去就当丢了,靠服务端那边的丢失率兜底。