线上MySQL首次更新JSON字段时winnerCount字段未生效,本地环境无此问题
看起来你遇到了个挺诡异的环境差异问题——本地调试一切正常,一上线首次更新JSON字段里的winnerCount就不生效,第二次调用又恢复正常。我帮你梳理下可能的原因和对应的解决思路:
可能的原因分析
「读取-修改-写入」模式的潜在一致性问题
你的代码先通过getCurrentFileUrl读取JSON字段,修改后调用updateWinner,而updateWinner里又重新查了一次getCurrentWinnerCount。虽然你说首次调用就出问题,并发概率低,但线上数据库可能存在缓存或事务隔离级别的差异,导致两次读取的数据不一致,最终写回的JSON不是你修改后的版本。JSON序列化/反序列化的环境差异
本地和线上的Node.js、MySQL版本可能不一致:- 老版本Node.js对
JSON.stringify处理新增属性的对象时,可能存在隐式忽略的情况(比如对象被意外冻结); - 老版本MySQL(比如5.7早期)对JSON字段的字符串赋值兼容性不如8.0,可能解析时丢弃了未定义过的键(这种情况虽然少见,但确实存在)。
- 老版本Node.js对
直接替换整个JSON字段的风险
你现在是把修改后的JSON序列化成字符串,直接赋值给fileUrl字段。如果序列化后的字符串在转义过程中(escapeSqlString)出现意外格式问题,MySQL可能会静默修正JSON格式,导致新增的winnerCount字段丢失。
针对性解决方案
方案1:用MySQL JSON函数直接修改指定字段,规避全量替换
这是最可靠的方式,跳过「读取-修改-写入」的流程,直接在数据库层面修改JSON里的目标值,完全规避序列化和并发问题。
修改updateWinner函数,用MySQL的JSON_SEARCH找到数组中对应fileUrlId的元素路径,再用JSON_SET更新winnerCount:
const updateWinner = async (id, fileUrlId) => { // 获取当前外层winnerCount(如果需要更新该字段) const resultGet = await getCurrentWinnerCount(id); if (resultGet.length !== 1) return null; const currentWinnerCount = resultGet[0].winnerCount ? resultGet[0].winnerCount + 1 : 1; // 使用MySQL JSON函数精准更新数组中指定元素的winnerCount const sql = ` UPDATE vs_make_lists SET fileUrl = JSON_SET( fileUrl, JSON_UNQUOTE(JSON_SEARCH(fileUrl, 'one', '${fileUrlId}', NULL, '$[*].id')), JSON_SET( JSON_EXTRACT(fileUrl, JSON_UNQUOTE(JSON_SEARCH(fileUrl, 'one', '${fileUrlId}', NULL, '$[*].id'))), '$.winnerCount', COALESCE(JSON_EXTRACT(fileUrl, CONCAT(JSON_UNQUOTE(JSON_SEARCH(fileUrl, 'one', '${fileUrlId}', NULL, '$[*].id')), '.winnerCount')), 0) + 1 ) ), winnerCount = '${currentWinnerCount}' WHERE list_id = ${id} `; return execSQL(sql); };
注:
JSON_SEARCH返回带引号的路径,需要用JSON_UNQUOTE转成实际路径;COALESCE用来处理winnerCount不存在的情况,默认从0开始累加。
方案2:增加日志排查序列化环节问题
如果暂时不想改数据库操作逻辑,可以先在代码里加日志,定位问题出在哪一步:
// 在router的reduce修改后添加日志 console.log("修改后的tempFileUrl:", tempFileUrl); console.log("序列化后的JSON字符串:", JSON.stringify(tempFileUrl)); // 在updateWinner里添加SQL执行日志 console.log("执行的UPDATE语句:", sql);
查看线上日志:
- 如果序列化后的字符串里没有
winnerCount,说明是reduce修改逻辑的问题(比如线上环境中item.id === fileUrlId的判断不成立,可以加日志打印item.id和fileUrlId的类型与值); - 如果序列化后的字符串有
winnerCount但数据库里没有,说明是MySQL的问题,检查线上MySQL版本、确认fileUrl字段是JSON类型而非VARCHAR,也可以用JSON_VALID函数验证序列化后的字符串是否为合法JSON。
方案3:优化现有逻辑,减少重复查询
把updateWinner里的getCurrentWinnerCount查询去掉,直接用router里已经读取到的数据,避免两次查询的不一致:
// 修改router逻辑,传递已获取的winnerCount router.get("/winner", async function (req, res, next) { let id = Number(req.query.id); let fileUrlId = req.query.fileUrlId; if (!id && !fileUrlId) { return res.send(failModel(2004, false, null)); } const resGetWC = await getCurrentFileUrl(id); if (resGetWC.length === 1) { const currentWinnerCount = resGetWC[0].winnerCount ? resGetWC[0].winnerCount + 1 : 1; const tempFileUrl = JSON.parse(resGetWC[0].fileUrl).reduce((result, item) => { if (item.id === fileUrlId) { item.winnerCount = item.winnerCount ? item.winnerCount + 1 : 1; } result.push(item); return result; }, []); const resultUpdate = await updateWinner(id, JSON.stringify(tempFileUrl), currentWinnerCount); if (resultUpdate?.affectedRows === 1) { return res.send(successModel()); } return res.send(failModel(-1, false, null)); } return res.send(failModel(-1, false, null)); }); // 修改updateWinner函数 const updateWinner = async (id, fileUrl, currentWinnerCount) => { const sql = ` UPDATE vs_make_lists SET fileUrl = ${escapeSqlString(fileUrl)}, winnerCount ='${currentWinnerCount}' WHERE list_id = ${id} `; return execSQL(sql); };
备注:内容来源于stack exchange,提问作者Tokyo

