Docker Compose部署Flask应用后重复发送订阅邮件问题
针对你遇到的「添加PostgreSQL持久化卷后,每次docker-compose up时符合条件的订阅邮件数量翻倍」问题,核心原因大概率和历史数据累积未被正确处理或脚本执行逻辑未做去重标记有关,以下是具体排查方向和解决方法:
1. 价格数据重复插入(最可能)
如果webscraper每次启动都向price表插入新的价格记录,而非更新现有GPU的最新价格,price表会积累多条历史价格。checksubs脚本遍历所有价格记录时,每一条符合desired_price >= price的记录都会触发一次邮件,导致邮件数量随价格记录数递增。
排查操作
进入PostgreSQL容器执行SQL查询,查看是否存在重复价格记录:
SELECT gpu_id, price, created_at FROM price ORDER BY created_at DESC;
解决方法
修改webscraper的爬取逻辑,使用PostgreSQL的upsert(更新或插入)操作,确保每个GPU只保留最新价格:
from sqlalchemy.dialects.postgresql import insert from datetime import datetime def upsert_gpu_price(gpu_id, price): # 构造upsert语句:若gpu_id已存在则更新价格和时间,否则插入新记录 stmt = insert(Price).values(gpu_id=gpu_id, price=price, created_at=datetime.now()) stmt = stmt.on_conflict_do_update( index_elements=['gpu_id'], set_={'price': stmt.excluded.price, 'created_at': stmt.excluded.created_at} ) db.session.execute(stmt) db.session.commit()
2. 订阅未标记「已发送提醒」状态
checksubs脚本没有跟踪哪些订阅已经发送过提醒,导致每次启动时都会重新检查所有历史订阅,只要当前价格符合条件就再次发送邮件。无数据卷时数据库每次重建,订阅记录是新的,所以只发一次;本地运行时脚本定时触发,可能只处理新增数据。
排查操作
查看subscriptions表结构,确认是否存在last_notified_at或notified这类标记字段。如果没有,说明脚本未跟踪已发送的提醒。
解决方法
- 给
subscriptions表添加状态字段:
class Subscription(db.Model): # 现有字段(user_id、gpu_id、desired_price等) last_notified_at = db.Column(db.DateTime, nullable=True) # 记录最后一次发送提醒的时间
- 修改checksubs脚本,仅处理最新价格符合条件且未发送过提醒/上次提醒早于最新价格更新时间的订阅:
from sqlalchemy import func from datetime import datetime def check_subscriptions(): # 获取每个GPU的最新价格及更新时间 latest_prices = db.session.query( Price.gpu_id, func.max(Price.price).label('latest_price'), func.max(Price.created_at).label('price_updated_at') ).group_by(Price.gpu_id).subquery() # 筛选符合条件的订阅 eligible_subs = db.session.query(Subscription, latest_prices.c.latest_price).join( latest_prices, Subscription.gpu_id == latest_prices.c.gpu_id ).filter( Subscription.desired_price >= latest_prices.c.latest_price, # 仅处理未发送过,或上次发送时间早于价格更新时间的订阅 (Subscription.last_notified_at.is_(None)) | (Subscription.last_notified_at < latest_prices.c.price_updated_at) ).all() # 发送邮件并更新标记 for sub, price in eligible_subs: send_alert_email(sub.user.email, sub.gpu.name, price) sub.last_notified_at = datetime.now() db.session.commit()
3. Docker Compose中checksubs服务重复执行
如果checksubs服务的配置导致每次docker-compose up时启动多个实例,或脚本被重复触发,会导致同一批订阅被多次处理。
排查操作
- 查看
docker-compose.yml中checksubs服务的配置,确认是否设置了scale: >1,或restart策略导致重复启动; - 执行
docker ps查看checksubs服务的运行实例数量,是否存在多个。
解决方法
- 确保checksubs服务只启动一个实例,移除
scale配置; - 若脚本需要每日执行,改用容器内部的
cron调度,而非让容器重复启动脚本。例如在Dockerfile中配置:
FROM python:3.9-slim # 安装依赖及cron RUN apt-get update && apt-get install -y cron COPY checksubs.py /app/ COPY run_checksubs.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/run_checksubs.sh # 添加每日0点执行的cron任务 RUN echo "0 0 * * * /usr/local/bin/run_checksubs.sh >> /var/log/checksubs.log 2>&1" | crontab - # 启动cron前台运行 CMD ["cron", "-f"]
4. 数据库初始化逻辑重复插入数据
虽然你添加了表存在性检查,但如果初始化脚本在每次Flask启动时无条件插入测试数据,会导致订阅记录重复,进而触发多倍邮件。
排查操作
检查数据库初始化代码,确认是否存在类似「每次启动都插入测试订阅」的逻辑,且未判断数据是否已存在。
解决方法
确保初始化数据的逻辑仅在表为空时执行:
def init_db(): db.create_all() # 仅在无订阅记录时插入测试数据 if not Subscription.query.first(): # 插入测试订阅代码 pass
内容的提问来源于stack exchange,提问作者mattch

