Flask中Postback与定时函数并发访问同列表的线程安全问题咨询
嘿,这个问题我之前做短信回调服务时刚好踩过坑!本质就是多线程(甚至多进程)下共享可变数据的竞态条件问题——两个函数同时读写列表,很容易出现数据丢失、重复处理或者列表状态混乱的情况。给你几个实用的解决方案,按需选:
1. 用线程锁直接控制访问(最适合单进程多线程场景)
这是最简单直接的方案,给你的共享列表配一把threading.Lock,不管是短信回调还是定时任务,只要要操作列表,就先拿到锁,操作完再释放。Python的with语句能帮你自动管理锁的获取和释放,哪怕操作中出异常也不会死锁。
举个代码例子:
from flask import Flask, request import threading import time app = Flask(__name__) # 共享列表 + 对应的线程锁 shared_items = [] item_lock = threading.Lock() # 处理短信的Postback接口 @app.route('/sms/incoming', methods=['POST']) def handle_incoming_sms(): # 先拿到短信内容(这里假设你的回调参数是phone和content) phone = request.form.get('phone') content = request.form.get('content') # 操作共享列表前必须加锁 with item_lock: shared_items.append({"phone": phone, "content": content}) return "Received", 200 # 定时处理函数 def process_periodically(): while True: # 同样,操作列表前加锁 with item_lock: # 读取并移除前N条,比如取前10条处理 items_to_handle = shared_items[:10] del shared_items[:10] # 这里是你的业务处理逻辑,不用拿锁,因为已经把数据取出来了 if items_to_handle: print(f"Processing {len(items_to_handle)} items...") # 写你的处理逻辑 time.sleep(300) # 5分钟 # 启动定时线程(设置daemon=True,主进程退出时自动结束) threading.Thread(target=process_periodically, daemon=True).start() if __name__ == '__main__': app.run(debug=True)
这个方案轻量、改动小,适合你的Flask服务是单进程多线程的情况(比如默认开发服务器,或APScheduler线程池模式)。
2. 改用线程/进程安全的外部存储(适合多进程或持久化需求)
如果你的Flask服务是多进程部署的(比如用Gunicorn开了多个worker),线程锁就不管用了——每个进程都有自己的shared_items副本,锁只能管当前进程内的线程。这时候最好把共享数据放到外部存储里,比如Redis或者SQLite,这些存储本身就内置了并发控制。
比如用Redis的列表结构(天然支持原子操作):
from flask import Flask, request import redis import time app = Flask(__name__) # 连接本地Redis,根据你的配置修改 redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True) @app.route('/sms/incoming', methods=['POST']) def handle_incoming_sms(): phone = request.form.get('phone') content = request.form.get('content') # 用lpush把短信内容加到Redis列表的头部 redis_client.lpush('sms_queue', f"{phone}:{content}") return "Received", 200 def process_periodically(): while True: # 用lrange+ltrim原子性地取出前10条并移除 items = redis_client.lrange('sms_queue', 0, 9) if items: # 移除已经取出的条目 redis_client.ltrim('sms_queue', 10, -1) print(f"Processing {len(items)} items...") # 写你的处理逻辑 time.sleep(300) if __name__ == '__main__': app.run(debug=True)
这个方案的优势:
- 天然支持多进程、多机器共享数据
- 数据持久化,服务重启也不会丢失
- 不用自己管锁,Redis帮你搞定并发
如果不想用Redis,轻量的SQLite也可以,用事务来保证操作的原子性(比如添加短信用INSERT,定时任务用SELECT+DELETE的事务)。
3. 用队列类替代普通列表(适合FIFO场景)
如果你的业务逻辑是先进先出处理短信,那可以直接用queue.Queue——这是Python标准库自带的线程安全队列,所有操作都是原子的,不用自己加锁。
例子:
from flask import Flask, request import threading import time from queue import Queue app = Flask(__name__) # 线程安全的队列 sms_queue = Queue(maxsize=0) # 不限大小 @app.route('/sms/incoming', methods=['POST']) def handle_incoming_sms(): phone = request.form.get('phone') content = request.form.get('content') sms_queue.put({"phone": phone, "content": content}) return "Received", 200 def process_periodically(): while True: items_to_handle = [] # 一次性取出最多10条(如果有的话) while not sms_queue.empty() and len(items_to_handle) < 10: items_to_handle.append(sms_queue.get()) # 标记任务完成(如果用join()的话需要这个,简单场景可省略) sms_queue.task_done() if items_to_handle: print(f"Processing {len(items_to_handle)} items...") # 写你的处理逻辑 time.sleep(300) threading.Thread(target=process_periodically, daemon=True).start() if __name__ == '__main__': app.run(debug=True)
这个方案适合不需要随机访问列表元素,只需要按顺序处理的场景,代码更简洁,不用手动管理锁。
最后总结下选型建议:
- 单进程多线程:优先用
threading.Lock或者queue.Queue - 多进程/需要持久化:用Redis、SQLite等外部存储
- 尽量避免全局可变数据,能交给外部存储就交给外部,减少并发问题
内容的提问来源于stack exchange,提问作者TommySmithers

