PHP项目短信发送失败如何重试补发

wen PHP项目 31

本文目录导读:

PHP项目短信发送失败如何重试补发

  1. 方案一:基于数据库的重试队列
  2. 方案二:使用消息队列(推荐)
  3. 方案三:使用异步任务处理
  4. 最佳实践建议
  5. 完整使用示例

在PHP项目中实现短信发送失败的重试补发机制,建议采用队列+定时任务数据库状态机的方案,以下是几种常见的实现方式:

基于数据库的重试队列

创建短信发送记录表

CREATE TABLE `sms_send_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `phone` varchar(20) NOT NULL COMMENT '手机号',
  `content` text NOT NULL COMMENT '短信内容',
  `template_id` varchar(50) DEFAULT NULL COMMENT '模板ID',
  `params` json DEFAULT NULL COMMENT '模板参数',
  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '发送状态:0待发送,1成功,2失败',
  `retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '已重试次数',
  `max_retry` int(11) NOT NULL DEFAULT '3' COMMENT '最大重试次数',
  `error_msg` varchar(500) DEFAULT NULL COMMENT '错误信息',
  `next_retry_time` datetime DEFAULT NULL COMMENT '下次重试时间',
  `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_status_next_retry` (`status`, `next_retry_time`),
  KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短信发送记录表';

短信发送类(带重试机制)

<?php
class SmsService
{
    private $db;
    private $maxRetry = 3;
    private $retryInterval = [5, 30, 120]; // 重试间隔:5秒、30秒、120秒
    public function __construct()
    {
        $this->db = new PDO('mysql:host=localhost;dbname=your_db', 'user', 'password');
    }
    /**
     * 发送短信并记录
     */
    public function send($phone, $content, $templateId = null, $params = [])
    {
        // 1. 记录发送日志
        $logId = $this->insertSendLog($phone, $content, $templateId, $params);
        // 2. 尝试发送
        return $this->attemptSend($logId);
    }
    /**
     * 尝试发送
     */
    private function attemptSend($logId)
    {
        $log = $this->getLogById($logId);
        try {
            // 调用短信网关
            $result = $this->callSmsGateway($log['phone'], $log['content']);
            if ($result['success']) {
                // 发送成功
                $this->updateLogStatus($logId, 1);
                return true;
            } else {
                throw new \Exception($result['error_msg'] ?? '发送失败');
            }
        } catch (\Exception $e) {
            // 发送失败,判断是否需要重试
            return $this->handleFailure($log, $e->getMessage());
        }
    }
    /**
     * 处理发送失败
     */
    private function handleFailure($log, $errorMsg)
    {
        $newRetryCount = $log['retry_count'] + 1;
        if ($newRetryCount <= $log['max_retry']) {
            // 计算下次重试时间
            $interval = $this->retryInterval[$log['retry_count']] ?? 120;
            $nextRetryTime = date('Y-m-d H:i:s', time() + $interval);
            // 更新日志状态
            $this->updateLogForRetry($log['id'], 2, $newRetryCount, $errorMsg, $nextRetryTime);
            // 如果是短间隔,立即尝试重试
            if ($interval <= 30) {
                sleep($interval);
                return $this->attemptSend($log['id']);
            }
            return false;
        } else {
            // 超过最大重试次数,标记为最终失败
            $this->updateLogStatus($log['id'], 3, $errorMsg);
            return false;
        }
    }
    /**
     * 重试待发送的短信(由定时任务调用)
     */
    public function retryPendingSends($limit = 50)
    {
        $sql = "SELECT * FROM sms_send_log 
                WHERE status = 2 
                AND next_retry_time <= NOW() 
                AND retry_count < max_retry 
                ORDER BY next_retry_time ASC 
                LIMIT :limit";
        $stmt = $this->db->prepare($sql);
        $stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
        $stmt->execute();
        $logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
        foreach ($logs as $log) {
            $this->attemptSend($log['id']);
        }
    }
    /**
     * 调用短信网关
     */
    private function callSmsGateway($phone, $content)
    {
        // 这里集成你的短信服务商API
        // 示例:阿里云短信、腾讯云短信、七牛云等
        $url = 'https://api.sms-provider.com/send';
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
            'phone' => $phone,
            'content' => $content
        ]));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 5); // 设置超时时间
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($httpCode == 200) {
            $data = json_decode($response, true);
            return ['success' => true, 'data' => $data];
        }
        return ['success' => false, 'error_msg' => "HTTP {$httpCode}"];
    }
    // 数据库操作方法
    private function insertSendLog($phone, $content, $templateId, $params)
    {
        $sql = "INSERT INTO sms_send_log (phone, content, template_id, params, status, max_retry, next_retry_time) 
                VALUES (:phone, :content, :template_id, :params, 0, :max_retry, NOW())";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            ':phone' => $phone,
            ':content' => $content,
            ':template_id' => $templateId,
            ':params' => json_encode($params),
            ':max_retry' => $this->maxRetry
        ]);
        return $this->db->lastInsertId();
    }
    private function getLogById($id)
    {
        $sql = "SELECT * FROM sms_send_log WHERE id = :id";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([':id' => $id]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    private function updateLogStatus($id, $status, $errorMsg = null)
    {
        $sql = "UPDATE sms_send_log SET status = :status";
        $params = [':id' => $id, ':status' => $status];
        if ($errorMsg !== null) {
            $sql .= ", error_msg = :error_msg";
            $params[':error_msg'] = $errorMsg;
        }
        $sql .= " WHERE id = :id";
        $stmt = $this->db->prepare($sql);
        $stmt->execute($params);
    }
    private function updateLogForRetry($id, $status, $retryCount, $errorMsg, $nextRetryTime)
    {
        $sql = "UPDATE sms_send_log 
                SET status = :status, 
                    retry_count = :retry_count, 
                    error_msg = :error_msg, 
                    next_retry_time = :next_retry_time 
                WHERE id = :id";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            ':id' => $id,
            ':status' => $status,
            ':retry_count' => $retryCount,
            ':error_msg' => $errorMsg,
            ':next_retry_time' => $nextRetryTime
        ]);
    }
}

使用消息队列(推荐)

使用Redis作为队列

<?php
class SmsQueueService
{
    private $redis;
    public function __construct()
    {
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
    }
    /**
     * 将短信加入队列
     */
    public function enqueue($phone, $content, $retryCount = 0, $maxRetry = 3)
    {
        $data = json_encode([
            'phone' => $phone,
            'content' => $content,
            'retry_count' => $retryCount,
            'max_retry' => $maxRetry,
            'created_at' => time()
        ]);
        // 使用延迟队列实现重试间隔
        $delay = $this->getDelayTime($retryCount);
        $this->redis->zAdd('sms_queue', time() + $delay, $data);
    }
    /**
     * 处理队列中的短信
     */
    public function process()
    {
        while (true) {
            // 获取到期的任务
            $tasks = $this->redis->zRangeByScore('sms_queue', 0, time(), ['limit' => [0, 10]]);
            foreach ($tasks as $task) {
                $data = json_decode($task, true);
                // 从有序集合中移除
                $this->redis->zRem('sms_queue', $task);
                // 发送短信
                $result = $this->sendSms($data['phone'], $data['content']);
                if ($result['success']) {
                    // 发送成功,记录日志
                    $this->logSuccess($data);
                } else {
                    // 发送失败,判断是否重试
                    if ($data['retry_count'] < $data['max_retry']) {
                        $data['retry_count']++;
                        // 重新入队
                        $this->enqueue($data['phone'], $data['content'], $data['retry_count'], $data['max_retry']);
                    } else {
                        // 超过最大重试次数,记录失败日志
                        $this->logFailure($data, $result['error_msg']);
                    }
                }
            }
            // 避免空循环占用CPU
            if (empty($tasks)) {
                sleep(1);
            }
        }
    }
    private function getDelayTime($retryCount)
    {
        $delays = [0, 10, 60, 300]; // 立即、10秒、1分钟、5分钟
        return $delays[$retryCount] ?? 600;
    }
    private function sendSms($phone, $content)
    {
        // 短信发送实现
        try {
            // 调用API
            return ['success' => true];
        } catch (\Exception $e) {
            return ['success' => false, 'error_msg' => $e->getMessage()];
        }
    }
}

使用异步任务处理

<?php
// 使用supervisor或crontab实现定时重试
// retry_sms.php - 由crontab每分钟执行
require_once 'SmsService.php';
$smsService = new SmsService();
$smsService->retryPendingSends(100);
// crontab配置
// * * * * * /usr/bin/php /path/to/retry_sms.php

最佳实践建议

重试策略

  • 指数退避:2^n 秒(1s, 2s, 4s, 8s...)
  • 有限次数:通常3-5次
  • 设置最大重试时间窗口(如24小时)

异常处理

// 处理不同类型的错误
$errorCategories = [
    'timeout' => ['重试间隔短', 3次重试],
    'balance_insufficient' => ['不重试,通知管理员'],
    'phone_error' => ['不重试,标记无效号码'],
    'content_error' => ['修改内容后重试'],
];

监控告警

// 记录失败次数超过阈值的手机号
public function monitorFailedSends()
{
    $sql = "SELECT phone, COUNT(*) as fail_count 
            FROM sms_send_log 
            WHERE status = 3 
            AND created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)
            GROUP BY phone 
            HAVING fail_count > 5";
    $failedPhones = $this->db->query($sql)->fetchAll();
    if (!empty($failedPhones)) {
        // 发送告警通知
        $this->sendAlert("短信发送失败率高: " . json_encode($failedPhones));
    }
}

使用第三方服务

推荐使用成熟的短信服务商SDK:

这些服务商通常都已内置重试机制,你只需要调用其SDK即可。

完整使用示例

// 发送短信
$smsService = new SmsService();
// 方式1:同步发送(带自动重试)
$result = $smsService->send('13800138000', '您的验证码是:123456');
// 方式2:异步发送(放入队列)
$queueService = new SmsQueueService();
$queueService->enqueue('13800138000', '您的验证码是:123456');
// 方式3:批量发送
$phones = ['13800138001', '13800138002', '13800138003'];
foreach ($phones as $phone) {
    $queueService->enqueue($phone, '促销活动通知');
}

选择哪种方案取决于你的项目规模和需求:

  • 小项目:数据库方案简单易实现
  • 中大型项目:消息队列方案更可靠
  • 高并发场景:结合Redis队列和异步处理

抱歉,评论功能暂时关闭!