Spring Boot 中实现池化的短信发送并支持服务商故障自动切换

在 Spring Boot 中实现池化的短信发送并支持服务商故障自动切换,核心是通过统一接口抽象、连接池优化资源复用、故障检测与切换机制实现高可用。以下是详细方案:
一、核心设计思路
统一接口抽象:定义短信发送标准接口,屏蔽不同服务商(阿里云、腾讯云等)的实现差异。
连接池化:为每个服务商配置 HTTP 连接池(如 OkHttp、HttpClient),避免频繁创建 / 销毁连接,提升性能。
故障自动切换:通过管理器监控服务商状态,发送失败时自动切换到下一个可用服务商,并支持状态恢复。
二、具体实现步骤

  1. 定义核心模型与接口
    (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();
}
  1. 实现多服务商发送逻辑(带连接池)
    每个服务商实现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
  1. 实现服务商管理与故障切换
    核心组件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());
        }
    }
}
posted @ 2025-07-17 17:15  spiderMan1-1  阅读(63)  评论(0)    收藏  举报