配置页那个"保存成功"的假象:我差点被缓存骗了两次
上周给后台加了个"站点公告"的开关,前端表单提交、后端入库、返回200,一气呵成。我点了保存,刷新页面,开关稳稳停在"开启"位,心里还嘀咕这需求也太简单了。结果第二天用户群里有人问公告怎么没显示,我愣了——数据库里明明开着啊。
查了半天,发现是配置读取走了缓存,但保存后没清。更坑的是,我为了"优化"还加了个5分钟的文件缓存,导致后台看到的和前端拉到的完全两个世界。这还不算完,修完这个我又踩了第二个坑:缓存清是清了,可配置项用的是`file`驱动,高并发下几个请求同时写配置,直接把缓存文件干成了半截JSON,解析报错全站白屏。
现在我的配置页保存流程变成了固定四步走,少一步都睡不着:
第一步:表单校验必须拆两层
以前我图省事,Controller里直接`$request->post()`一把梭。现在先在Validate层卡死字段类型和范围,比如公告开关只能是0或1,超时时间必须是正整数。第二层在逻辑层再验业务规则,比如"开启公告时内容字段不能为空"。两层分开,前端改需求时不至于动到核心校验。
第二步:数据库事务包裹配置写入
单条配置更新本不用事务,但我现在习惯性包一层。因为配置表经常和"配置分组""配置日志"联动写入,中间哪步断了至少能回滚。之前有次加配置历史记录,日志表字段超长抛异常,主表却写进去了,两边对不上查了一下午。
第三步:缓存刷新要"双杀"
我现在的做法是:先删缓存键,再写数据库,最后再删一次。别笑,真有请求卡在中间那几毫秒,读到旧缓存。驱动换成`redis`后,用`tag`批量清更省事,但`file`驱动不支持标签,只能挨个键名硬删。最近试了个折中方案:配置项按模块分前缀,比如`config:site:*`、`config:pay:*`,清的时候`Cache::clear('config:site')`一锅端。
第四步:返回数据必须带"实际生效值"
这是被坑最惨后加的。保存接口不再只返回`{code:1, msg:'保存成功'}`,而是把最终入库且刷新缓存后的值再读一遍塞回去。前端表单控件用返回数据回显,而不是自己维持状态。这样哪怕缓存刷新延迟,用户看到的也是真实生效值。
还有个细节:配置项多了之后,我搞了个`config:lock`的Redis锁,保存时先抢锁,5秒过期。防的就是自己手快连点两次,或者两个管理员同时改同一项。锁没抢到直接抛"配置正在更新",比覆盖写入后互相覆盖强。
现在回头看,那个"保存成功"的toast弹窗真是最大的陷阱——它只告诉你HTTP请求走通了,至于配置有没有真的落地、缓存有没有真的同步、并发下会不会脏写,全得自己兜底。你们后台配置页是怎么处理缓存一致性的?有没有更骚的操作?