百万流量的短信网关系统
- 1、百万流量的短信网关系统
- 1、业务背景
- 2、短信服务商基本信息
- 3、短信路由网关
- 4、基于不可变模式改造代码
说明:儒猿技术窝Java并发编程学习笔记
1、百万流量的短信网关系统
1、业务背景
1.有一个每天有百万流量的短信网关系统,这个系统会使用第三方短信服务商(比如说阿里云、腾讯云、百度云等
等)的短信发送功能
2.短信网关后面对接着多家三方短信服务提供商,当我们需要发送短信的时候,短信网关会根据一定的策略(比如说
选择费率最低的、或者到达率最高的)从三方短信厂商中选择一家,调用他们的接口给用户发送短信
3.短信网关会定时对短信服务商进行PK,如果发现某个服务商不行了,则会在短信网关后台管理服务中更新短信服务
商列表,也就是把某些PK中输掉的服务商替换
2、短信服务商基本信息
短信服务商信息包括服务商请求的url以及每次发送的字节数量
public class SmsInfo {
//短信服务商请求url
private String url;
//短信内容最多多少个字节
private Long maxSizeInBytes;
public SmsInfo(String url, Long maxSizeInBytes) {
this.url = url;
this.maxSizeInBytes = maxSizeInBytes;
}
}
3、短信路由网关
1.短信服务商信息列表是保存在数据库中的,由于这个数据会比较常用,而每次发送短信之前都需要根据一定的策略
来选择服务商,所以在系统启动的时候,会将所有的短信服务商列表从数据库中加载出来放在内存里
public class SmsRouter {
//短信网关对象,通过volatile修饰来保证其他线程的可见性
private static volatile SmsRouter instance = new SmsRouter();
//短信服务商信息map,key表示服务上的优先级
private final Map<Integer, SmsInfo> smsInfoMap;
//初始化短信网关对象
public SmsRouter() {
//从数据库中维护的路由信息加载到jvm内存中
this.smsInfoMap = this.loadSmsInfoMapFromDb();
}
//从数据库加载短信服务商信息
private Map<Integer, SmsInfo> loadSmsInfoMapFromDb() {
//模拟DB
Map<Integer, SmsInfo> routerMap = new HashMap<>();
routerMap.put(1, new SmsInfo("aliyun", 180L));
routerMap.put(2, new SmsInfo("tencent", 181L));
routerMap.put(3, new SmsInfo("baidu", 182L));
return routerMap;
}
}
2.当短信服务商发生变更的时候的时候,会先更新更新数据库,然后去更新内存中的短信服务商信息。但是这里
有一个问题,因为这里设置url和设置maxSizeInBytes并不是一个原子操作,可能出现其中一个线程刚刚设置了
URL,另一个线程过来读取服务商排名为3的服务商的场景,这样读取排名为3的服务商得到的一个中间状态的结果,
其中url和maxSizeInBytes并不是属于同一个服务商的,这样很可能会导致程序出现问题
//更新短信服务商列表
public void changeRouterInfo() {
Map<Integer, SmsInfo> smsInfoMap = instance.smsInfoMap;
SmsInfo smsInfo = smsInfoMap.get(3);
smsInfo.setUrl("极光短信");
smsInfo.setMaxSizeInBytes(183L);
}
4、基于不可变模式改造代码
1.将SmsInfo改造为不可变对象
public final class SmsInfo {
//短信服务商编号
private final Long id;
//短信服务商请求url
private final String url;
//短信内容最多多少个字节
private final Long maxSizeInBytes;
public SmsInfo(Long id, String url, Long maxSizeInBytes) {
this.id = id;
this.url = url;
this.maxSizeInBytes = maxSizeInBytes;
}
public Long getId() {
return id;
}
public String getUrl() {
return url;
}
public Long getMaxSizeInBytes() {
return maxSizeInBytes;
}
//初始化
public SmsInfo(SmsInfo smsInfo) {
this.id = smsInfo.getId();
this.url = smsInfo.getUrl();
this.maxSizeInBytes = smsInfo.getMaxSizeInBytes();
}
}
2.获取服务商列表的代码改造为防御性复制
//获取短信服务商
public Map<Integer, SmsInfo> getSmsInfoRouterMap() {
//防止对短信路由信息更改,进行防御性的复制
return Collections.unmodifiableMap(deepCopy(smsInfoRouterMap));
}
private Map<Integer, SmsInfo> deepCopy(Map<Integer, SmsInfo> smsInfoRouterMap) {
Map<Integer, SmsInfo> result = new HashMap<>(smsInfoRouterMap.size());
Iterator<Map.Entry<Integer, SmsInfo>> iterator = smsInfoRouterMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, SmsInfo> mapEntry = iterator.next();
result.put(mapEntry.getKey(), new SmsInfo(mapEntry.getValue()));
}
return result;
}
3.提供一个直接替换SmsRouter实例的方法,便于用来刷新整个服务商信息
//短信网关对象,通过volatile修饰来保证其他线程的可见性
private static volatile SmsRouter instance = new SmsRouter();
//获取短信网关对象
public static SmsRouter getInstance() {
return instance;
}
//短信服务商列表变更,更新短信网关对象
public static void setInstance(SmsRouter smsRouter) {
instance = smsRouter;
}
4.当短信服务商列表发生变化的时候,我们通过调用changeRouteInfo方法,更新数据库中的服务商信息,接着替换整个SmsRouter实例
这样一来,SmsRouter在构造函数的时候会调用loadSmsInfoRouteMapFromDb方法将更新后的短信服务商列表从数据库中读取出来,
然后更新到内存中
//短信服务商列表发生变更
public void changeRouterInfo() {
//1.更新数据库中短信服务商信息
updateSmsRouteInfoLists();
//2.更新内存的短信服务商列表
SmsRouter.setInstance(new SmsRouter());
}