前提
在上一篇文章中,大致介绍了自己第一份工作做的一些印象深刻的事情。两年青葱岁月,不懈努力,从一个开发新手变成了一个有一些想法的技术人了。
当然期间,自己也犯过一些错误,还记得几个场景:
场景一, 有一次设计给表结构增加一个字段,觉得表有两个旧属性字段定义好像重复了,新需求迭代的时候就和师傅说,我想把这些个字段合并下,改个更符合情景的名字,结果师傅张口大骂,你这不是胡闹嘛,数据库字段只能增不能改。还记得当时自己灰溜溜的。
场景二:有一次定时任务在执行,正好在修复一个线上代码bug,没注意看时间,直接上线,导致后续运营找过来,说某个时期有数据对不上。
场景三:有一次临近下班吃晚饭,新需求要上线,mysql数据库有字段更新,自己脑袋发昏,忘了改线上数据库,直接上线,最后导致线上服务挂了,一直报错。还好当时有两台服务器,看到一台报错,及时停止了上线,只造成一会儿的服务故障。
虽然每次犯了错,师傅都会轻则痛骂重则扣奖金,但依然感谢那个时候发生的那些事,因为只有经历了犯错,自己才会知道事情的严重性,心里有多难过,才会慢慢养成谨慎,认真,靠谱的工作习惯。
介绍前的碎碎念
两年之后,由于公司的发展已经到了瓶劲期,所以离职去了一家中大型在线教育公司。所在的部门是个中台部门,掌管着短信,物流,订单,用户信息,用户积分等公共服务系统,后面自己做了一段时间TOC会员业务。个人感觉该公司技术氛围较好,可能是因为公司有架构组的原因,公司所有中间件都是自研的。之前在设计模式这一块没有学习过,不太懂,现在回过头来看他们写的架构,发现其中很多设计模式用的很好,自己突然了解设计的用意。
中台项目
很多中台项目设计架构都差不多,以短信服务系统为例进行回顾一下。
短信服务项目
项目背景:公司每个产品业务线,都有给用户发短信的功能。而短信的供应商有很多种,每个业务组选择的可能也不一样。现把此功能从各个业务组抽离出来,单独构建一个服务维护,统一管理。
整体架构图如图所示:
一 SMS服务RPC接口
RPC接口具体实现代码内,也需要限流操作。
rpc接口定义有两个参数,一个是 用户电话号码,另一个是短信枚举 SmsType。
SmsType 里面包含两个属性
public enum SmsType {
Tech(1,"tech");
public int type;//1 表示语音 2 表示验证码 3 表示通知类短信
public String bizType; // 业务组名称
SmsType(int type, String bizType) {
this.type = type;
this.bizType = bizType;
}
}
一个是 SmsType,表示短信发送格式类型,总共有三种,voice语音类型,notify 通知类型,verify验证码类型。
另一个 BizType,表示具体业务组类型,例如直播课业务组,此字段需提前各方商量协定。
二 SmsSelector
内部多个短信平台,服务合作商。如亿美软通,阿里云,腾讯云等。后台系统有配置权重,每个供应商的权值不同,优先采用权位高的供应商,后采用策略+工厂模式进行生成实际的执行类。
以亿美供应商为例,生成实际执行类代码如下:
public abstract class ISmsHandler {
public abstract boolean send(String phone,String content,int type);
}
public class YimeiSmsHandler extends ISmsHandler{
@Override
public boolean send(String phone,String content,int type){
// 具体调用合作商提供的url接口,拼接参数等
return true;
}
}
public interface ISmsHandlerFactory {
ISmsHandler createHandler();
}
public class YimeiSmsHandlerFactory implements ISmsHandlerFactory{
@Override
public ISmsHandler createHandler(){
return new YimeiSmsHandler();
}
}
public class SmsHandlerFactoryMap {
//工厂的工厂
private static final Map<String, ISmsHandlerFactory> cachedFactories = new HashMap<>();
static {
cachedFactories.put("yimei", new YimeiSmsHandlerFactory());
// cachedFactories.put("xunjie", new XunjieSmsHandlerFactory());
// cachedFactories.put("aliyun", new aliyunSmsHandlerFactory());
// cachedFactories.put("tengxunyun", new tengxunyunSmsHandlerFactory());
}
public static ISmsHandlerFactory getParserFactory(String type) {
if (type == null || type.isEmpty()) {
return null;
}
ISmsHandlerFactory parserFactory = cachedFactories.get(type.toLowerCase());
return parserFactory;
}
}
三 失败补偿机制
发送短信失败时,采取随机选取一个执行器再次进行发送补偿策略。记录失败次数,超过三次,则放弃发送。
try{
smsHandler.send(phone,content,type)
}catch (Exception e){
failCount++;
compensationService.handle();
}
四 数据存储
1 发送验证码时,用 redis 存储验证信息,到期删除。
2 UserSmsInfo - 用户短信记录信息
数据量大采用mongodb 存储,userId + createTime 做联合索引
3 SmsStatInfo - 短信统计信息
数据量大采用 mongodb 存储,统计每个服务商发送记录,便于后续每月账单付费清算。
敏感词过滤服务
项目背景:公司各业务线发布内容文本时,需要对文本进行敏感词标识。
交互形式如图:
敏感词过滤服务架构:
一 Trie 树
Trie 树结构
Trie 树的本质,就是利用字符串之间的公共前缀,将重复的前缀合并在一起。
public class Trie {
// 存储无意义字符
private TrieNode root = new TrieNode('/');
// 往Trie树中插入一个字符串
public void insert(char[] text) {
TrieNode p = root;
for (char c : text) {
int index = c - 'a';
if (p.children[index] == null) {
TrieNode newNode = new TrieNode(c);
p.children[index] = newNode;
}
p = p.children[index];
}
p.isEnd = true;
}
// 在Trie树中查找一个字符串
//简单模拟一下,实际代码实现是多模式匹配,代码复杂
public boolean find(char[] pattern) {
TrieNode p = root;
for (char c : pattern) {
int index = c - 'a';
if (p.children[index] == null) {
return false; // 不存在pattern
}
p = p.children[index];
}
// 找到pattern
return p.isEnd;
}
public class TrieNode {
public char data;
public TrieNode[] children = new TrieNode[26];
public boolean isEnd = false;
public TrieNode(char data) {
this.data = data;
}
}
}
二 AC 自动机
后续在Trie 树迭代优化,用AC自动机作为底层数据结构支持。
AC自动机数据结构图:
AC 自动机实际上就是在 Trie 树之上,加了类似 KMP 的 next 数组,只不过此处的 next 数组是构建在树上。
public class AcNode {
public char data;
public AcNode[] children = new AcNode[26]; // 字符集只包含a~z这26个字符
public boolean isEnd = false; // 结尾字符为true
public int length = -1; // 当isEndingChar=true时,记录模式串长度
public AcNode fail; // 失败指针
public AcNode(char data) {
this.data = data;
}
}
想要理解AC自动机,需要先理解 KMP 算法中 next 数组的构建过程。 当匹配到 叶子节点,或者中途遇到不匹配字符的时候,通过 fail 指针找到上层指针,再进行循环搜索。
具体实现可详细看极客时间里这篇文章:time.geekbang.org/column/arti…
TOC业务项目
会员业务
背景:隔壁业务会员小组缺人,接手了一段时间app会员相关的业务开发工作。
有一个业务场景,公司直播部门研发了一个新功能,远程调用了会员服务一个接口,查询会员的相关信息。在压测时,发现会员服务这边抗不住,需要进行优化。
优化手段:
1 业务隔离
和本身会员的基本服务隔离开来,新写一个接口提供服务,不影响内部服务调用
2 采用 redis 数据预热缓存
写脚本把所有会员信息加载到redis里,直播查询请求到达,直接从redis里查找,如果不在缓存内,直接返回,不查数据库。
3 数据一致性处理
在用户下单购买会员,退款撤销会员时,提供一个接口修改预热信息,使数据保持一致性。
系统性能优化
背景:有一个每日任务提醒功能,key 为每日日期,value 为 一个 bitmap 存储每位老师当日完成情况。
日常运行时,发现查询较慢,偶有超时问题。研究发现后续参与的老师id 都达到八位数,且都有相似规律,那前面的位置都会不存储达到浪费,则修改数据结构,则直接采用可K-V储存,内存降低百分之五十左右。
之后做的项目
之后去了一家独角兽公司,做的是TOB端工作内容,偏向于对一些课程结构的抽象领域建模,涉及一些实验平台的开发,有了A/B Test 分组优化想法,于是写在了这篇文章里 :juejin.cn/post/704218…
后团队内部想要一个自定义延时队列,满足想要的业务场景,一些技术选型和设计写在了这篇文章:juejin.cn/post/704031…
总结
来日方长,不忘初心,保持热爱。