本文目录导读:

在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队列和异步处理