共享redis缓存污染问题
公司为了控制成本,不同测试环境是共享一套redis缓存的,比方说,test01环境命中的redis缓存,可能是来自于test02环境设置的redis缓存,也可能是test03环境设置的。
我在实际开发过程中就遇到过redis缓存共享的污染问题。test01环境部署的是dev/2026-05-26分支代码,test02环境部署的是dev/2026-05-19分支代码,我在test01环境新增了几个多语言key,本地验证是正常的,但是在test01环境偶现bug:获取不到新增的多语言key,旧多语言key获取正常。
排查如下:
- 电脑本地是正常的,因为本地运行的是本地的redis。
- 在代码里加入调试代码
req.header['no-cache'] === '1'就不走redis缓存,重新部署到test01环境,测试时添加请求头no-cache,发现bug不会复现了,说明是redis缓存的问题。 - 当test01环境走redis缓存的逻辑,就打印日志看,从日志上分析,获取的redis缓存没有我新增的多语言key。
- 凭经验和灵机一动,我猜测有可能是不同测试环境共享一套redis缓存导致的bug,咨询了管理测试环境的同事,同事回复,不同测试环境是共享一套redis缓存,这验证了我的猜想。
- 复盘:test01环境部署的是
dev/2026-05-26分支代码,有我新增的多语言key,test02环境部署的是dev/2026-05-19分支代码,没有我新增的多语言key。有人访问了test02环境,代码逻辑是先查有没有redis缓存,没有就请求多语言key,然后把请求结果缓存起来(不包含我新增的多语言key)。这个时候缓存还没过期,我访问了test01环境,代码逻辑查到有redis缓存,就返回缓存结果(这个结果其实是test02请求的结果)。test01环境的redis缓存被test02环境影响污染,因此,在test01环境会偶现bug:获取不到新增的多语言key,旧多语言key获取正常。 - 总结为一句话就是,不同git分支的代码写入了相同的缓存键,导致高版本代码读取到低版本代码生成的旧数据。
- 解决方案就是,判断在测试环境中,缓存键添加测试环境专属前缀。
redis键名过长问题
团队前期没有针对redis缓存的键名做约束规范,导致不同的开发人员自由发挥,出现键名设置过长的key滥用问题。键名过长问题会导致redis成本增加和redis缓存命中率下降。
解决方案:
- 输出一套键名的规范文档,增加多种业务场景的示例,宣传使用。
- 定期扫描项目中的大key
http header溢出问题
我们有一些H5页面是内嵌在App中的,而App的AB实验是有做缓存的,为了App和H5的AB实验保持一致,App需要把全量的AB实验传递给H5页面。
从App -> H5,是请求html的GET请求,全量的AB实验是放在请求头abt-params中全量返回。
AB实验是一个庞大的json字符串,大概的格式如下:
{
ListPageStar: {
param: {
position: 5,
visible: 'show'
}
},
SearchPageWord: {
param: {
scene: '11',
visible: 'noshow'
}
}
}
http协议没有强制规定header的最大值,但是像nginx、node服务都有自己的实现限制。当请求头abt-params过于庞大时,node服务就会返回431状态码和Request Header Fields Too Large,表示总header大小或单个header字段超过node服务的限制,结果就是H5页面无法正常打开。
解决方案:
和App同事沟通,App和H5使用同一种压缩算法,比如gzip,对AB实验的json字符串进行压缩后再传递给H5。
新增一个请求头abt-gzip通知H5表示AB实验是否有经过压缩。H5识别到有请求头abt-gzip,那么就对请求头abt-params进行解压处理。最终完美解决了该问题。
JS对象经多层传递,难以定位属性修改位置的问题
场景:有时我们想从万千行代码中,找出某js对象的某个属性在哪里被修改了,但是呢,这个js对象数据可能在十几个文件(每个文件可能有千行代码)中间被传递了不知道多少层,调用栈很深,仅靠查看调用链是很难分析出来的(老项目是可能达到这种复杂度的,笔者就曾经遇到过),那我们就可以借助Object.defineProperty、Proxy来帮我们找出属性被修改处。
Object.defineProperty(obj, prop, descriptor):
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。其存取描述符set可以提供一个 setter 方法,在属性值被修改时,会触发 setter 方法。
new Proxy(target, handler):
Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义。
// 找出哪里把xxx.submitData给重置掉了,当执行xxx.submitData = { a: 1 }会触发断点
Object.defineProperty(xxx, "submitData", {
set() {
debugger;
},
});
let handler = {
set(obj, prop, v) {
if (prop === "sfv" && v === "2022") {
// 当属性sfv被修改为2022时触发断点
debugger;
}
if (prop === "idux" && v === undefined) {
// 当扩展一个新属性idux时触发断点
debugger;
}
return v;
},
};
xxx.submitData = new Proxy(xxx.submitData, handler);
还有这里说一句,变量命名尽量语义化场景化,不建议所有表单数据都叫submitData,不然后面排查问题搜submitData,搜出来几十处还得一个个排除,submitData语义化也不够好,不知道是什么类型的表单数据。建议可以叫submitXxxData,比如路由器表单数据就叫submitRouterData。