Amazon SES大批次邮件重复发送问题排查求助(生产环境)
排查方向
- 验证flock锁的实际有效性:如果生产环境是多容器部署,每个容器的
/tmp目录是独立的,那你用的/tmp/routines.lock只能限制单个容器内的任务,多个容器会同时执行cron任务,导致重复发送。另外要确认脚本执行过程中锁是否持续持有——如果API脚本因为超时、崩溃提前退出,flock会自动释放锁,后续的cron任务可能提前启动。 - 排查邮件服务的重试机制:因为重复邮件有不同的Amazon ID,大概率是Amazon SES收到了多次发送请求。检查你的PHP代码调用SES的逻辑,有没有因为网络超时、响应延迟触发了重试?或者AWS SDK默认开启了自动重试,导致单次业务请求触发了多次SES调用。
- 检查数据库的读取与更新逻辑:大批次处理时,是不是读取
$rows之后,还没完成数据库更新,就有其他进程(或重复的cron任务)读取到了同样的未处理数据?比如数据库隔离级别是读未提交,或者更新操作没有加行锁,导致数据被重复读取。 - 对比开发与生产环境的差异:比如生产环境的PHP超时时间更长、邮件发送的网络环境更差(导致重试)、数据库并发更高,这些差异都可能触发开发环境没出现的问题。另外检查生产环境是否开启了某种自动重试机制(比如负载均衡的重试、API网关的重试)。
- 补充生产环境的详细日志:在发送邮件前记录
用户ID+邮件类型+时间戳的唯一标识,发送成功后记录Amazon ID,同时记录脚本的启动/结束时间、锁的获取状态。通过日志可以判断重复邮件是来自同一脚本的重复调用,还是多个脚本实例同时执行。
解决方案
- 确保分布式环境下的锁有效性:如果是多容器部署,把锁文件放到共享存储(比如Docker Volume、NFS)中,让所有cron容器共享同一个锁路径;或者改用Redis分布式锁,比文件锁更可靠。
- 实现邮件发送的幂等性:
- 给每封邮件生成唯一业务标识(比如
user_id_email_type),发送前先查询数据库/Redis,确认该标识未发送过再执行发送; - 调用SES API时,指定
ClientRequestToken参数,SES会根据这个token避免重复处理同一请求。
- 给每封邮件生成唯一业务标识(比如
- 优化数据库的处理逻辑:采用“先锁后处理”的方式,读取数据前先把符合条件的数据标记为「处理中」,避免重复读取:
// 伪代码示例,用事务+更新锁 $db->beginTransaction(); // 先把待处理数据标记为处理中 $db->exec("UPDATE email_tasks SET status = 'processing' WHERE condition = ? AND status = 'pending'", [$condition]); // 再读取已标记的处理中数据 $rows = $db->fetchAll("SELECT * FROM email_tasks WHERE status = 'processing'"); $db->commit(); // 处理完成后更新状态为已发送 foreach($rows as $row) { // 发送邮件逻辑 $db->exec("UPDATE email_tasks SET status = 'sent', amazon_id = ? WHERE id = ?", [$amazonId, $row['id']]); }
- 调整邮件发送的重试策略:如果用AWS SDK,关闭自动重试或者减少重试次数;如果是自己实现的重试,要基于业务标识判断是否已经发送成功,避免无效重试。
- 监控脚本执行时长:如果大批次处理时间超过5分钟,要优化代码性能(比如批量处理、异步发送),避免因为任务超时导致锁提前释放,或者后续cron任务因为锁未释放而跳过。
内容的提问来源于stack exchange,提问作者Ting-Ting Chen
相关产品推荐
相关产品推荐

