Spring Boot 中实现池化的短信发送并支持服务商故障自动切换
在 Spring Boot 中实现池化的短信发送并支持服务商故障自动切换,核心是通过统一接口抽象、连接池优化资源复用、故障检测与切换机制实现高可用。以下是详细方案:
一、核心设计思路
统一接口抽象:定义短信发送标准接口,屏蔽不同服务商(阿里云、腾讯云等)的实现差异。
连接池化:为每个服务商配置 HTTP 连接池(如 OkHttp、HttpClient),避免频繁创建 / 销毁连接,提升性能。
故障自动切换:通过管理器监控服务商状态,发送失败时自动切换到下一个可用服务商,并支持状态恢复。
二、具体实现步骤
- 定义核心模型与接口
(1)请求与响应模型
封装短信发送的参数和结果,明确成功 / 失败原因:
// 短信发送请求
@Data
public class SmsRequest {
private String phone; // 手机号
private String templateId; // 模板ID
private Map<String, String> params; // 模板参数(如验证码)
}
// 短信发送响应
@Data
public class SmsResponse {
private boolean success; // 是否成功
private String message; // 响应信息(成功/错误原因)
private String provider; // 实际发送的服务商名称
}
(2)短信发送接口
定义统一发送方法,所有服务商实现该接口:
public interface SmsService {
// 发送短信
SmsResponse send(SmsRequest request);
// 获取服务商名称(如"aliyun"、"tencent")
String getProviderName();
}
- 实现多服务商发送逻辑(带连接池)
每个服务商实现SmsService,并集成连接池管理 HTTP 连接。以阿里云为例:
(1)服务商配置类
通过配置文件注入服务商参数(支持连接池配置):
@ConfigurationProperties(prefix = "sms.providers.aliyun")
@Data
public class AliyunSmsProperties {
private boolean enabled = true; // 是否启用
private String accessKey; // 阿里云AccessKey
private String secret; // 阿里云Secret
private String signName; // 签名名称
// 连接池配置
private int maxConnections = 10; // 最大连接数
private int keepAliveSeconds = 30; // 连接保持时间(秒)
private int timeoutMs = 5000; // 超时时间(毫秒)
}
(2)阿里云短信实现(带连接池)
使用 OkHttp 的ConnectionPool管理连接,复用 HTTP 资源:
public class AliyunSmsService implements SmsService {
private final OkHttpClient client;
private final AliyunSmsProperties properties;
// 构造函数注入配置,初始化连接池
public AliyunSmsService(AliyunSmsProperties properties) {
this.properties = properties;
// 配置连接池:最大连接数、保持时间
ConnectionPool connectionPool = new ConnectionPool(
properties.getMaxConnections(),
properties.getKeepAliveSeconds(), TimeUnit.SECONDS
);
// 初始化OkHttp客户端(带连接池)
this.client = new OkHttpClient.Builder()
.connectionPool(connectionPool)
.connectTimeout(properties.getTimeoutMs(), TimeUnit.MILLISECONDS)
.readTimeout(properties.getTimeoutMs(), TimeUnit.MILLISECONDS)
.build();
}
@Override
public SmsResponse send(SmsRequest request) {
try {
// 1. 构建阿里云API请求(签名、参数拼接等)
Request httpRequest = buildAliyunRequest(request);
// 2. 使用连接池发送请求(复用连接)
Response response = client.newCall(httpRequest).execute();
// 3. 解析响应(根据阿里云API文档处理返回值)
if (response.isSuccessful() && isAliyunSuccess(response.body().string())) {
return new SmsResponse(true, "发送成功", getProviderName());
} else {
return new SmsResponse(false, "阿里云API返回错误", getProviderName());
}
} catch (Exception e) {
// 捕获超时、网络异常等
return new SmsResponse(false, "发送失败:" + e.getMessage(), getProviderName());
}
}
@Override
public String getProviderName() {
return "aliyun";
}
// 阿里云API签名与请求构建逻辑(省略具体实现)
private Request buildAliyunRequest(SmsRequest request) { ... }
}
(3)其他服务商实现
同理实现腾讯云、华为云等服务商(TencentSmsService、HuaweiSmsService),均需配置独立连接池。
3. 配置连接池与服务商(Spring Boot 自动配置)
通过配置类注入所有服务商实例,支持外部配置(application.yml):
(1)自动配置类
@Configuration
@EnableConfigurationProperties({AliyunSmsProperties.class, TencentSmsProperties.class})
public class SmsAutoConfiguration {
// 注入阿里云短信服务(仅当enabled=true时)
@Bean
@ConditionalOnProperty(prefix = "sms.providers.aliyun", name = "enabled", havingValue = "true")
public SmsService aliyunSmsService(AliyunSmsProperties properties) {
return new AliyunSmsService(properties);
}
// 注入腾讯云短信服务(同理)
@Bean
@ConditionalOnProperty(prefix = "sms.providers.tencent", name = "enabled", havingValue = "true")
public SmsService tencentSmsService(TencentSmsProperties properties) {
return new TencentSmsService(properties);
}
}
(2)配置文件(application.yml)
sms:
retry-count: 1 # 每个服务商的重试次数
providers:
aliyun:
enabled: true
access-key: your-aliyun-access-key
secret: your-aliyun-secret
sign-name: 你的签名
max-connections: 10 # 连接池最大连接数
keep-alive-seconds: 30
timeout-ms: 5000
tencent:
enabled: true
secret-id: your-tencent-secret-id
secret-key: your-tencent-secret-key
app-id: 1400xxxxxx
max-connections: 10
timeout-ms: 5000
- 实现服务商管理与故障切换
核心组件SmsProviderManager,负责:
管理所有服务商实例与状态(可用 / 不可用)
发送短信时自动选择服务商,失败则切换
定时恢复不可用服务商的状态
(1)管理器实现
@Component
@Slf4j
public class SmsProviderManager {
// 所有可用的短信服务商(Spring自动注入所有SmsService实现)
private final List<SmsService> smsServices;
// 可用服务商名称(线程安全集合,支持动态修改)
private final CopyOnWriteArrayList<String> availableProviders = new CopyOnWriteArrayList<>();
// 每个服务商的重试次数(从配置文件读取)
private final int retryCount;
// 构造函数注入
public SmsProviderManager(List<SmsService> smsServices,
@Value("${sms.retry-count:1}") int retryCount) {
this.smsServices = smsServices;
this.retryCount = retryCount;
// 初始化可用服务商列表
this.availableProviders.addAll(
smsServices.stream().map(SmsService::getProviderName).collect(Collectors.toList())
);
}
// 发送短信,失败自动切换服务商
public SmsResponse send(SmsRequest request) {
// 遍历所有服务商,尝试发送
for (SmsService service : smsServices) {
String provider = service.getProviderName();
// 跳过不可用的服务商
if (!availableProviders.contains(provider)) {
continue;
}
// 对当前服务商重试N次
for (int i = 0; i <= retryCount; i++) {
try {
SmsResponse response = service.send(request);
if (response.isSuccess()) {
return response; // 发送成功,返回结果
} else {
// 判断是否为服务商不可用(如系统错误、超时)
if (isProviderUnavailable(response.getMessage())) {
log.warn("服务商[{}]第{}次发送失败", provider, i + 1);
continue; // 重试当前服务商
} else {
// 业务错误(如手机号无效),无需切换
return response;
}
}
} catch (Exception e) {
// 捕获异常(如网络超时)
log.error("服务商[{}]发送异常:{}", provider, e.getMessage());
if (i < retryCount) {
continue; // 重试
}
}
}
// 重试次数耗尽,标记为不可用
markProviderUnavailable(provider);
log.warn("服务商[{}]已标记为不可用", provider);
}
// 所有服务商均失败
return new SmsResponse(false, "所有服务商均不可用", null);
}
// 判断是否为服务商不可用(如系统错误、超时)
private boolean isProviderUnavailable(String message) {
return message.contains("超时") || message.contains("系统错误") || message.contains("500");
}
// 标记服务商为不可用,并定时检查恢复
private void markProviderUnavailable(String provider) {
availableProviders.remove(provider);
// 定时(如1分钟)检查服务商状态,恢复可用
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.schedule(() -> {
if (checkProviderHealth(provider)) {
availableProviders.add(provider);
log.info("服务商[{}]已恢复可用", provider);
}
scheduler.shutdown();
}, 60, TimeUnit.SECONDS); // 1分钟后检查
}
// 检查服务商健康状态(发送测试短信)
private boolean checkProviderHealth(String provider) {
SmsService service = smsServices.stream()
.filter(s -> s.getProviderName().equals(provider))
.findFirst().orElse(null);
if (service == null) return false;
// 发送测试短信(如给测试手机号发送空内容)
try {
SmsRequest testRequest = new SmsRequest();
testRequest.setPhone("13800138000"); // 测试手机号
return service.send(testRequest).isSuccess();
} catch (Exception e) {
return false;
}
}
}
连接池参数调优:
根据业务量配置最大连接数(如max-connections: 20)、超时时间(如timeout-ms: 3000),避免连接堆积或超时过长。
故障判断精细化:
区分 “服务商不可用”(如 API 500 错误、网络超时)和 “业务错误”(如手机号无效、模板不存在),仅对前者触发切换。
熔断机制集成:
结合 Resilience4j 的CircuitBreaker,当服务商失败率超过阈值(如 50%)时自动熔断,避免无效重试:
// 在SmsService实现类的send方法上添加熔断注解
@CircuitBreaker(name = "aliyunSms", fallbackMethod = "sendFallback")
public SmsResponse send(SmsRequest request) { ... }
// 熔断降级方法
public SmsResponse sendFallback(SmsRequest request, Exception e) {
return new SmsResponse(false, "服务熔断:" + e.getMessage(), "aliyun");
}
动态权重调整:
记录服务商历史成功率,优先选择成功率高的服务商(如权重 = 成功率 * 100),提升发送效率。
四、使用方式
在业务代码中注入SmsProviderManager,直接调用发送方法:
@Service
public class UserService {
@Autowired
private SmsProviderManager smsManager;
// 发送验证码
public void sendVerifyCode(String phone, String code) {
SmsRequest request = new SmsRequest();
request.setPhone(phone);
request.setTemplateId("SMS_123456");
request.setParams(Collections.singletonMap("code", code));
SmsResponse response = smsManager.send(request);
if (!response.isSuccess()) {
throw new RuntimeException("短信发送失败:" + response.getMessage());
}
}
}

浙公网安备 33010602011771号