如何同步持续更新Map的路由请求与每小时写入Map数据到数据库的定时任务
解决方案
首先删除路由逻辑中多余的update_count()调用,避免频繁触发数据库更新操作,抵消Map缓存计数的性能优势。以下是两种无数据异常的同步实现方案:
方案1:双Map切换法(推荐,性能无损耗)
利用JS单线程赋值的原子性,通过切换计数存储的Map完全避免读写冲突,实现最简单,性能损耗为0:
- 维护两个Map:请求侧仅写入
temp_count,定时任务仅读取独立的persist_count - 定时任务触发时,先原子交换两个Map的引用:将当前
temp_count赋值给persist_count,同时直接初始化新的空Map给temp_count承接后续新请求的计数,两个操作不会被任何请求逻辑打断 - 定时任务遍历
persist_count的所有键值对批量更新到MongoDB,完成后直接释放persist_count即可 - 新增执行状态标识避免定时任务重入,新增异常兜底逻辑避免持久化失败丢数
参考实现代码:
const Link_URL = require('./你的模型路径'); const cron = require('node-cron'); // 替换原单Map声明 let temp_count = new Map(); let isUpdating = false; async function update_count(){ // 上个定时任务未执行完成直接跳过,避免重入 if (isUpdating) return; isUpdating = true; // 原子交换Map,JS单线程保证该操作不会被中断 const persist_count = temp_count; temp_count = new Map(); try { // 批量生成Mongo增量更新操作,用$inc避免覆盖数据库侧的其他修改 const bulkOps = Array.from(persist_count.entries()).map(([code, count]) => ({ updateOne: { filter: { _id: code }, update: { $inc: { visit_count: count } } } })); if (bulkOps.length > 0) { await Link_URL.bulkWrite(bulkOps); } } catch (err) { console.error('计数持久化失败:', err); // 持久化失败兜底:将未同步的计数合并回当前temp_count,避免丢数 for (const [code, count] of persist_count.entries()) { temp_count.set(code, (temp_count.get(code) || 0) + count); } } finally { isUpdating = false; } } app.get('/teeny/:code', async (req, res) => { try { const url = await Link_URL.findOne({ _id : req.params.code }) if (url) { const code = req.params.code; // 仅写入temp_count,无需触发更新逻辑 temp_count.set(code, (temp_count.get(code) || 0) + 1); return res.redirect(url.URL); } else { return res.status(404).json('No URL Found') } } catch (err) { console.error(err); res.status(500).json('Server Error'); } }) // 修正cron表达式:0 0 * * * 表示每小时整点执行,原表达式* * 1 * *为每月1号每分钟执行 cron.schedule('0 0 * * *', update_count);
方案2:读写锁方案(适合复杂并发场景)
如果存在多段业务逻辑同时读写计数Map的场景,可以引入读写锁保证同步:
- 引入
async-rwlock之类的读写锁库 - 路由侧更新Map时加读锁,允许多个请求同时读取修改Map
- 定时任务执行持久化时加写锁,阻塞所有读操作,持久化完成后释放锁
该方案会对请求响应性能有轻微影响,纯计数缓存场景无需使用。
注意事项
- 数据库更新必须用
$inc增量操作符,不要直接用Map内的数值覆盖数据库字段,避免数据库侧有其他业务修改计数时出现数据丢失 - 服务进程重启前需要主动调用一次
update_count,将内存中未同步的计数持久化到数据库,避免丢数 - 若部署多实例服务,需要改用Redis集中存储计数,避免多实例内存Map数据不一致
内容的提问来源于stack exchange,提问作者Anirudh Singh
相关产品推荐
相关产品推荐

