本文目录导读:

在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队列和异步处理
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播