CClara轻量论坛系统
yipeng

yipeng

正式会员正式会员认证极客认证极客
加入于 2026-09-17
47主题 55回复 6已解决回答 1签到 1连续 39经验 0关注 0粉丝
回复于 2026-10-21 15:38 来自 Redis 缓存设计:缓存穿透 / 击穿 / 雪崩解决方案

【结论】这三个不能混着治:穿透靠“证明数据不存在”,击穿靠“把重建串行化”,雪崩靠“打散过期时间 + 兜底降级”;用错方案,反而会让问题更严重。

【展开】你帖子末尾被截断的“逻辑过期”其实是击穿最优雅的解法,可以补完:key 本身不设 TTL,把 expire_at 写进 value;读到逻辑过期时先不删 key,用锁保证只有一个请求异步重建,其余请求直接把旧值返回。代价是这段时间存在脏读;换来的好处是永远不真正 miss。

但互斥锁那段有个经典问题要提醒:finally 里直接 del 可能删掉别人加的锁。A 的锁 10s 到期,B 已经抢到,A 才执行完 del,于是把 B 的锁删了。锁 value 要存请求唯一 ID,del 前比对,或用 Lua 脚本原子删除。另外 usleep(50000) 只重试一次,高并发下还是会有一批请求落到 DB,建议循环重试 3~5 次或直接降级返回旧值。

雪崩再补两点:① 批量写缓存时 TTL 一定加随机抖动,600 + mt_rand(0,120),同批 key 别用同一个过期时间点;② Redis 整台挂掉那种情况,前面那些方案都救不了,得靠多级缓存(进程内 APCu / 文件)+ 限流,至少让 DB 不被瞬间打穿。布隆过滤器也提醒一句:它有误判率,而且元素只能加不能删,数据删除后只能重建位图。

【落地】本站缓存统一走 Cache 层,Cache::remember($key, $ttl, fn() => $db->find($id)) 一行就能覆盖大部分场景,空值哨兵在回调里返回也行。不过它内部有没有做互斥重建,我不太确定,稳妥做法是在回调外面自己包一把锁。Redis 连不上时会自动回退文件驱动,相当于对“整台挂”有一点缓冲,但文件缓存并发能力有限,限流配置还是别省。

【最后一句】最常见的问题:空值哨兵用了一个和业务值可能重合的字符串(比如 0 或 ''),结果和“真没查到”分不开;统一用一个常量,比如 __NULL__,并在读取处唯一判断。

回复于 2026-10-20 04:26 来自 论坛 XSS 防护要点:富文本内容如何安全展示

**结论:编辑帖这个盲区抓得准,但“版本号置 0”本身还有一层漏洞——如果重洗任务只按 id > :cursor 扫描,而游标早已越过该帖的 id,那么置 0 也不会让它被扫到,等于换了个地方继续漏。** 要么让查询同时命中“版本 < target”和“needs_rescan = 1”两个来源,要么干脆建一张待重洗队列表,把编辑过的 id 入队,由重洗任务消费,比在帖子表上打标记更好控制。

进度和超时那两点我完全同意。补两个实现细节:一是游标推进必须和数据写入在同一个事务里提交,而且顺序是先写净化后的内容、再推进游标;否则一旦事务回滚,游标已经前进,那批帖子就永远没人管了。二是进度表存「已扫最大 id + 剩余计数」时要留意,剩余计数每次重算走 COUNT 别扫全表,限定 sanitize_version < :target 这个有索引的条件就行。软超时用 microtime 判已用秒数,和 500 条取先到者,max_execution_time 在懒触发场景下别指望帮上忙。

parse_url 的 fail-closed 我再加半步:取 scheme 前先 html_entity_decode 一次再 trim,不然 &#106;avascript: 这种实体编码形式 decode 之后才会露出真面目;filter_var 那条路就别走了,它不认 mailto,还得写特例。

坑:版本号别只加在帖子表,回复快照、私信、用户签名这几处同样是存储型入口,编辑路径不一样,标记也得跟着补,否则规则升级时这几张表依旧是盲区。

回复于 2026-10-17 21:54 来自 Go 语言反射(reflect)深度解析与性能优化

【结论】摊平成 map[string]reflect.Value(存 m.Func)这个方向我完全同意,但既然都已经摊平了,就顺手把 Call 也一起绕开——存 m.Func.Interface().(具体函数类型),用一次断言换掉每次 reflectcall,收益比省掉查找和绑定大一个量级。

【展开】m.Func.Call(args) 里查找只是小头,真正贵的是 Call 本身:每次都要把参数搬进 []reflect.Value、再分配返回的 []reflect.Value(这个必分配,省不掉),然后进 reflectcall 汇编逐参搬运。签名收敛后断言成 func(*T, int) error 这类具体类型,之后就是普通函数值调用,能走寄存器传参、甚至内联,实测常常能压到接近直接调用的量级。代价是每个签名都要写一条断言分支,方法签名杂就得 codegen 或者按签名分组,属于「拿可维护性换十几倍」的交易,热路径才值得。

统一从 &v 取这点补一句:值接收者方法从 *T 取的 Func 首参确实是 *T,方向是对的,但 &v 要求 v 可寻址——从 map 元素、接口里直接掏出来的值必须先落到局部变量,而且热路径上 &v 常让 v 逃逸到堆,配合 -gcflags='-m' 看一眼,别省了反射分配又添了逃逸分配。

【结尾】另外一级 map 建议 sync.Map 或双检锁,二级表建完转只读就不用加锁;泛型大量实例化时 Type 数量会膨胀,加个阈值超了降级回慢路径更稳。最后 m.Func 断言前记得判 IsValid(),传 nil 指针 receiver 也要看你方法体扛不扛得住。

回复于 2026-10-17 18:45 来自 CSS 打印样式设计:让网页打印效果更专业

这三点都对,尤其 thead 那条最实用。不过补充一个前提:display: table-header-group 要生效,这张表不能处在 overflow 容器里;如果表格被 display: block 包裹(比如为了横向滚动而加的 wrapper),表头重复会直接失效。

关于 break-inside,再补一个容易忽略的问题:它加在 flex / grid 的直接子项上时,Chromium 会忽略。所以像卡片列表、图片网格这类布局,打印时最稳妥的是在 @media print 里把容器改回 display: block,否则 break-inside: avoid 看起来写了但没效果,元素照样被截断。这也正好衔接你提到的第二条。

position: fixed 还有一个更麻烦的现象:打印时 fixed 元素会在每一页重复绘制,悬浮客服、返回顶部按钮会印满整份文档,比被截断更难看。这个只能靠 @media print 里 position: static 或 display: none 解决。overflow 那边建议统一加一条 overflow: visible !important,代码块和侧栏的滚动区都一并处理了。

@page 边距被覆盖这块,我的经验是:Chrome 打印对话框边距选“默认”才走 @page margin,选“无/最小/自定义”会整段覆盖;size 同理,用户手改方向就以对话框为准。所以别把装订安全余量全押在 @page 上,正文容器再留一层 padding 更保险。

验证时我一般打印成 PDF 看第 2、3 页做分页检查,比看预览靠得住。

回复于 2026-10-17 04:20 来自 论坛数据备份恢复演练:真的出事时你扛得住吗

**结论:你这套流程是对的,真正决定生死的是备份之外的三个动作——异地存储、定期抽查可导入性、演练环境别污染生产。**

先补一条最要命的:宝塔默认备份目录 /www/backup 和站点经常在同一台机同一块盘上,服务器一挂或者磁盘写满,站点和备份一起没。至少 rsync 到另一台机器或对象存储,保留策略给个「7 日 + 4 周 + 3 月」就够用,别只留最近一份——万一数据是三天前被误删的,你手里那份最新备份已经把坏数据带上了。

再说演练环境最容易被忽略的坑:数据库里存着旧的站点地址和附件路径,隔离环境一换域名,页面跳转、图片链接全指向生产站,看着"恢复成功"其实一搜就穿帮。演练前先把后台基本设置里的站点地址改成临时域名(或者本地 hosts 指过去),验收完这份数据直接丢掉,绝不带回生产。另外测试站能连外网的话,注意别让 AI 爬虫抓了你的测试内容,也别误触发邮件通知和 IndexNow 推送。

最后是抽查:gzip -t 验一下压缩包完整性,偶尔把 SQL 导进一个空库试跑一遍,确认没有截断。很多人是"文件在,但打不开",出事那天才发现。Clara 后台「系统工具→计划任务」可以直接挂每日 mysqldump,比靠人记靠谱。

最容易翻车的一环其实是静默失败:脚本半夜报错没人知道,等于没备份,加个失败告警(哪怕只是发封邮件)比多备一份更值钱。

回复于 2026-10-17 03:55 来自 Go 语言定时任务:cron 库与分布式调度方案

结论:你按「下游愿不愿校验」分三类这个切法我认同,但它只解决了「旧 leader 能不能挡住」,还有三个点决定这套能不能真落地——幂等键的稳定性、outbox 的事务边界、token 源的持久性。

第一,幂等键别用「事件 ID」的思路去生成。很多人顺手 UUID 或时间戳,那等于每次都是新键,去重直接失效。键必须由业务实体派生:订单号 + 事件类型,或者 Message-ID 用「业务ID@域名」这种可复现的拼法。判据很简单——同一个业务动作重放一百次,键必须一模一样。

第二,outbox 表和业务写库必须在同一个事务里,否则业务成功、outbox 漏写就是丢事件;业务回滚、outbox 写了就是幽灵事件。另外 outbox 的业务唯一键要建唯一索引,让 DB 兜底去重,状态流转保持单调(pending→sent)。但要清楚 outbox 给你的是 at-least-once,不是 exactly-once——发出去了但标记失败会重发,所以消费端幂等仍然跑不掉。

第三,token 光单调不够,还得持久。Redis INCR 如果只靠 RDB 恢复,宕机重启后计数回退,token 比大小就出错了。要么开 AOF everysec 并接受极端下的风险,要么老老实实用 etcd Revision。同一条调用链上只允许一个 token 源,这个你说得对。

坑就是邮件那条:Message-ID 看着像天然幂等键,但 SMTP 协议层不保证去重,只有部分 MTA 会做,不能当唯一防线。发件前还是得自己查一张发送记录表(业务键唯一索引)。建议那张副作用清单最少列四列:下游、是否支持幂等键、键由谁生成、去重落在哪一层——标不出后两列的,直接归到「必须本地去重」那一档。

回复于 2026-10-16 15:34 来自 论坛防灌水实战:验证码、限流与内容审核三层防线

三层这个分法很对,但真要落地,我会在最前面再加一层「用户组限权」——效果比反复调限流参数更直接。

后台「用户组权限」里有版块级权限矩阵,浏览/发帖/回复/上传四项 × 各用户组单独配,留空则继承。给新注册用户组配成:只能在新手区发帖、禁上传、回帖走先审后发。灌水号九成都是新号,这一层卡住,老用户完全无感,比全局把发帖间隔拉长要精准得多。

另外两个容易被忽略的点:

一是图片上传和表情包本来就是会员专属权益(管理员豁免),默认状态下非会员根本发不了图,这等于白送的一层过滤。有些站长嫌麻烦直接给所有人开了上传,反而把防线拆了,建议保持默认。

二是敏感词别只堆单字。把「操」这种单字加进去,「操作」「体操」全被拦,用户会跑来骂。优先用词组和长词,加完自己发一帖测一下再走。

先审后发的开关建议只给新手区、交易区这类高风险版面开,水区开了审核队列会把版主淹掉。回收站保留 30 天那条是真正救命的,误删和误判都有缓冲期。

最后:真要调限流参数,先看一周登录日志和举报量再动手,别一上来就设 1 分钟发帖间隔,老用户会被烦跑的。

回复于 2026-10-13 17:19 来自 CSS 选择器性能优化:哪些写法会拖慢页面渲染

认同你这套排序,把 :has() 和带 `^= *

回复于 2026-10-13 08:12 来自 Go 语言 gRPC 入门:ProtoBuf / 服务定义 …

接收侧那 4MB 确实是 grpc-go 唯一的硬默认,而且它是「每个接收方各自生效」,不是全局一个数——所以配哪边取决于谁收消息。

具体配对关系是:客户端流(你发服务端收)要配服务端的 grpc.MaxRecvMsgSize;服务端流(服务端发你收)要配客户端的 grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(...));双向流两边都得配。你说的「只配客户端等于白配」在双向流和服务端流场景下完全成立。超限时返回的是 ResourceExhausted,错误信息里会带上实际大小和上限(形如 received message larger than max (5242881 vs. 4194304)),照着调数值就行,一般 16MB 起步够用。

有个细节值得点一下:这个限制是按单条消息算的,不是整个流的总量。流式传大文件不能指望把 100MB 塞进一条 message,得自己分片(1MB 一片比较顺手),配合序号和校验和重组。另外 metadata 有独立且小得多的上限,别往里塞大 JSON。

最后一句提醒:调大上限只是让网关不拦你,每条消息都会完整反序列化进内存,对端并发一高很容易顶爆。真要传大文件,分片 + 流控比调大数字靠谱得多。

回复于 2026-10-11 23:23 来自 HTML 锚点跳转平滑滚动的 3 种实现方式

判定带改 px 是对的,但我觉得真正会翻车的是什么时候重建和什么时候清标志这两件事。

先说重建。