奖励回调重复发送导致Node.js服务端积分重复入账如何解决?
问题根因
你现有的实现存在两个核心错误直接导致重复入账:
- 代码逻辑错误:你在异步数据库操作的外层直接写了
return res.send(event_id + ":OK").status(200);,这会导致请求刚进来、数据库操作还没执行完甚至还没开始校验事件ID的时候,就直接返回了200响应,同时后续异步执行的数据库逻辑没有做重复校验,重发请求进来后会重复执行所有写入操作。 - 幂等性实现逻辑顺序错误:正确的幂等校验需要先判断事件ID是否已处理,只有未处理的请求才执行后续积分操作,你没有在请求入口第一步就做幂等校验,也没有原子化的幂等标记写入逻辑。
服务端可落地的解决方案
完全可以在服务端直接解决,按以下步骤修改即可:
第一步:调整请求处理逻辑顺序,入口先做幂等校验
请求进来第一步就以event_id为唯一键查询是否存在已处理的记录,注意这个校验和幂等标记的写入要做原子操作,避免并发请求穿透校验。
如果用的是MongoDB,直接用updateOne的upsert特性实现原子校验+写入:
// 请求进来第一步先做幂等校验,原子操作 const idempotentRes = await db.collection('pointIdempotent').updateOne( { event_id: event_id }, // 提前给event_id字段建唯一索引 { $setOnInsert: { event_id: event_id, uid: uid, create_time: Date.now(), status: 'processing' // 标记处理中 } }, { upsert: true } ) // 如果matchedCount不为0,说明这个event_id已经被处理过/正在处理,直接返回200即可 if (idempotentRes.matchedCount > 0) { return res.status(200).send(event_id + ":OK"); }
第二步:把所有数据库操作放到事务里执行,避免部分成功部分失败导致数据不一致
所有积分写入、历史记录写入的逻辑都放到同一个事务中,要么全部成功,要么全部失败。
第三步:删除外层的提前返回逻辑,只有所有业务逻辑执行成功后再返回200
修正后的完整逻辑参考:
try { // 1. 入口原子幂等校验,已处理直接返回 const idempotentRes = await db.collection('pointIdempotent').updateOne( { event_id: event_id }, { $setOnInsert: { event_id, uid, create_time: Date.now(), status: 'processing' } }, { upsert: true } ) if (idempotentRes.matchedCount > 0) { return res.status(200).send(event_id + ":OK") } // 2. 执行所有业务操作,建议放到事务中 const session = await db.startSession() await session.withTransaction(async () => { await db.collection('pointHistory').doc(uid).collection('pointHistory').add(newPointHistory, { session }) await db.collection('users').doc(uid).update({ point: addTotalPoint }, { session }) await db.collection('offerwallHistory').doc(uid).collection('offerwallHistory').add(offerwallHistory_data, { session }) await db.collection('adminPointHistory').add(adminPointHistory_data, { session }) await setDailyPointStatus(reward, 12, session) await setOtherPersonalPointStatus(reward, uid, "dailyPersonalPointStatus", session) await setOtherPersonalPointStatus(reward, uid, "weeklyPersonalPointStatus", session) await setOtherPersonalPointStatus(reward, uid, "monthlyPersonalPointStatus", session) // 更新幂等表状态为成功 await db.collection('pointIdempotent').updateOne({ event_id }, { $set: { status: 'success' } }, { session }) }) await session.endSession() // 3. 所有逻辑执行成功后再返回200 return res.status(200).send(event_id + ":OK") } catch (err) { console.log(err) // 处理失败的话更新幂等表状态为失败,不要返回200,让奖励服务器重试 await db.collection('pointIdempotent').updateOne({ event_id }, { $set: { status: 'fail', error: err.message } }) return res.status(500).send(event_id + ":FAIL") }
额外优化点
- 给
pointIdempotent表的event_id字段建唯一索引,彻底避免重复写入 - 可以定期清理超过奖励服务器重发周期的幂等记录,减少表体积
内容的提问来源于stack exchange,提问作者peterbbong
相关产品推荐
相关产品推荐

