并发写丢数据是因为全局变量、字典和文件操作非线程安全,多请求同时修改导致覆盖;应使用数据库行级锁(如SELECT ... FOR UPDATE)、Redis原子命令或正确实现乐观锁并处理冲突。

Flask 应用里直接用全局变量或文件存数据时,为什么并发写会丢数据
因为 Python 的 global 变量、内置字典、甚至普通文件写入(如 open(..., 'w'))都不是线程安全的。多个请求同时执行 data['count'] += 1 或 f.write(),底层可能只读到旧值、覆盖彼此修改,最终结果小于预期——这不是 Flask 的锅,是共享状态没加锁。
常见现象包括:计数器跳变、JSON 文件写乱码、用户提交后数据“消失”或重复写入。尤其在开发模式下用 flask run(默认多线程)或部署到 Gunicorn(多 worker)时更容易暴露。
- 别用
dict/list等内存结构存跨请求状态;临时缓存可用threading.local(),但不解决跨进程冲突 - 文件写入必须加锁:
threading.Lock()只管同进程内,多 worker 需filelock或外部协调服务 - 若用 SQLite,注意它默认只支持一个写连接,高并发下会抛
OperationalError: database is locked
用 SQLAlchemy + 数据库行级锁避免更新丢失
最稳妥的方式是把状态交给数据库,并用 SELECT ... FOR UPDATE 显式加锁。Flask-SQLAlchemy 支持 with_for_update(),能保证“读-改-写”原子性。
例如用户积分扣减场景:
from sqlalchemy import text
<p>@app.route('/deduct', methods=['POST'])
def deduct_points():
user_id = request.json['user_id']
amount = request.json['amount']</p><pre class="brush:php;toolbar:false;"># 加锁查余额(阻塞直到锁释放)
user = User.query.with_for_update().filter_by(id=user_id).first()
if not user or user.points <p></p>
- PostgreSQL/MySQL 支持
with_for_update();SQLite 不支持,会静默忽略(务必确认 DB 类型) - 锁范围默认是查到的行,但若 WHERE 条件无索引,可能升级为表锁,拖慢整体性能
- 锁持有时间越短越好——别在
with_for_update()块里调外部 API 或做耗时计算
用 Redis 实现带重试的原子计数器
当需要高频、轻量的并发写(如限流、投票、PV 统计),Redis 的单线程模型和原子命令是更优解。Python 用 redis-py 调用 INCR、HINCRBY 等命令天然避开了应用层竞争。
复杂逻辑(如“余额充足才扣减”)需用 Lua 脚本保证原子性:
# Lua 脚本:检查并扣减,失败返回 nil
lua_script = """
if redis.call('HGET', KEYS[1], 'balance') >= tonumber(ARGV[1]) then
redis.call('HINCRBY', KEYS[1], 'balance', -ARGV[1])
return 1
else
return nil
end
"""
<h1>Python 调用</h1><p>result = redis.eval(lua_script, 1, f"user:{user_id}", amount)
if result is None:
abort(400, "balance insufficient")
</p>
- Redis 连接池要复用,别每次新建
Redis()实例,否则连接数爆炸 - Lua 脚本不能含阻塞操作(如
SLEEP),也不能调用非确定性命令(如TIME) - 网络分区时 Redis 不可用?得有降级策略,比如先写本地内存+异步补偿,而非直接报错
为什么乐观锁在 Flask 里容易被误用
乐观锁靠版本号(version 字段)或时间戳比对实现,不加锁,适合读多写少。但它依赖“先读再比对再更新”的三步,而 HTTP 请求天然离散——两次请求之间可能已有其他写入,导致 UPDATE ... WHERE version = ? 影响行为 0。
典型误用是把乐观锁当成万能解药,却不处理更新失败后的业务逻辑:
- 不重试:用户提交后提示“操作失败,请重试”,体验差
- 盲目重试:没合并用户本次修改,直接覆盖,造成数据丢失
- 版本号放 URL 或表单隐藏域?可能被篡改,必须服务端校验来源
真正该做的是:捕获 db.session.execute(...).rowcount == 0,加载最新数据,提示用户冲突,并给出差异对比或手动合并入口——这已经超出纯技术范畴,涉及产品设计。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











