多用户并发下网站数据库单值自增的同步问题咨询
并发点击场景下全局计数器实现方案(Flask + MongoDB/Redis)
这个问题本质是读-改-写竞态冲突,如果采用「先查询数据库拿到当前x值 → 应用层代码执行x+1 → 把新值写回数据库」的流程,高并发下必然出现计数丢失、用户读到错误值的问题。核心解决思路是把自增操作下推到存储层执行原子命令,完全规避应用层计算带来的同步问题,两种存储选型的具体实现如下:
方案1:Redis实现(优先选,适合高频点击场景)
Redis单线程执行模型从根本上避免了并发冲突,原生自带的原子自增命令性能极高,适合点击量很大的场景。
- 先安装依赖:
pip install redis - Flask服务中初始化连接:
from flask import Flask import redis app = Flask(__name__) # 生产环境记得配置密码、持久化规则 r = redis.Redis(host='127.0.0.1', port=6379, db=0, decode_responses=True) # 初始化计数器,不存在则默认设为0 if not r.exists('click_counter:x'): r.set('click_counter:x', 0)
- 点击接口直接调用
INCR原子命令即可,命令执行完会直接返回自增后的最新值,不需要额外查询:
@app.route('/click', methods=['POST']) def click_handler(): current_x = r.incr('click_counter:x') return {"code": 0, "data": {"x": int(current_x)}}
- 普通查询接口直接读值就行:
@app.route('/get_x', methods=['GET']) def get_x(): return {"code": 0, "data": {"x": int(r.get('click_counter:x'))}}
注意:如果担心服务重启丢数据,一定要开启Redis的RDB/AOF持久化配置,不要用默认的无持久化配置。
方案2:MongoDB实现(适合已有Mongo技术栈,不想额外引入组件的场景)
MongoDB单文档更新是天然原子的,直接用$inc自增操作符即可,不需要自己处理并发锁。
- 先安装依赖:
pip install pymongo - Flask服务中初始化连接:
from flask import Flask from pymongo import MongoClient app = Flask(__name__) client = MongoClient('127.0.0.1', 27017) db = client['counter_db'] counter_col = db['counters'] # 初始化计数器文档,不存在则插入初始值 if counter_col.count_documents({"_id": "x"}) == 0: counter_col.insert_one({"_id": "x", "value": 0})
- 点击接口用
find_one_and_update搭配$inc,可以在原子自增的同时直接返回更新后的文档,避免二次查询带来的不一致问题:
@app.route('/click', methods=['POST']) def click_handler(): updated_doc = counter_col.find_one_and_update( {"_id": "x"}, {"$inc": {"value": 1}}, return_document=True # 配置后返回更新后的新值,默认是返回更新前的旧值 ) current_x = updated_doc["value"] return {"code": 0, "data": {"x": current_x}}
- 普通查询接口直接查单条文档即可:
@app.route('/get_x', methods=['GET']) def get_x(): doc = counter_col.find_one({"_id": "x"}) return {"code": 0, "data": {"x": doc["value"]}}
必看避坑点
- 别在Flask应用层自己加线程锁/进程锁:如果用gunicorn、uWSGI多进程部署服务,应用层锁不跨进程,完全起不到作用;就算单进程部署,锁也会严重拖低接口吞吐能力。
- 别用应用层计算后写回的逻辑:高并发下多个请求会查到同一个x值,最终出现多次点击只加1的计数丢失问题。
- 给用户返回自增结果时,一定要用存储层自带的「自增后返回新值」的能力,不要自增完成后再单独发一次查询请求,中间可能被其他请求更新,导致返回给用户的数值错误。
内容的提问来源于stack exchange,提问作者Nathan Dsouza
相关产品推荐
相关产品推荐

