那些年,我写过的项目(二)

549 阅读5分钟

前提

在上一篇文章中,大致介绍了自己第一份工作做的一些印象深刻的事情。两年青葱岁月,不懈努力,从一个开发新手变成了一个有一些想法的技术人了。

当然期间,自己也犯过一些错误,还记得几个场景:

场景一, 有一次设计给表结构增加一个字段,觉得表有两个旧属性字段定义好像重复了,新需求迭代的时候就和师傅说,我想把这些个字段合并下,改个更符合情景的名字,结果师傅张口大骂,你这不是胡闹嘛,数据库字段只能增不能改。还记得当时自己灰溜溜的。

场景二:有一次定时任务在执行,正好在修复一个线上代码bug,没注意看时间,直接上线,导致后续运营找过来,说某个时期有数据对不上。

场景三:有一次临近下班吃晚饭,新需求要上线,mysql数据库有字段更新,自己脑袋发昏,忘了改线上数据库,直接上线,最后导致线上服务挂了,一直报错。还好当时有两台服务器,看到一台报错,及时停止了上线,只造成一会儿的服务故障。  

虽然每次犯了错,师傅都会轻则痛骂重则扣奖金,但依然感谢那个时候发生的那些事,因为只有经历了犯错,自己才会知道事情的严重性,心里有多难过,才会慢慢养成谨慎,认真,靠谱的工作习惯。

 

介绍前的碎碎念

两年之后,由于公司的发展已经到了瓶劲期,所以离职去了一家中大型在线教育公司。所在的部门是个中台部门,掌管着短信,物流,订单,用户信息,用户积分等公共服务系统,后面自己做了一段时间TOC会员业务。个人感觉该公司技术氛围较好,可能是因为公司有架构组的原因,公司所有中间件都是自研的。之前在设计模式这一块没有学习过,不太懂,现在回过头来看他们写的架构,发现其中很多设计模式用的很好,自己突然了解设计的用意。

 

中台项目

很多中台项目设计架构都差不多,以短信服务系统为例进行回顾一下。

短信服务项目

项目背景:公司每个产品业务线,都有给用户发短信的功能。而短信的供应商有很多种,每个业务组选择的可能也不一样。现把此功能从各个业务组抽离出来,单独构建一个服务维护,统一管理。

 

整体架构图如图所示:

image.png

 

一 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 存储,统计每个服务商发送记录,便于后续每月账单付费清算。

 

敏感词过滤服务

项目背景:公司各业务线发布内容文本时,需要对文本进行敏感词标识。

交互形式如图:

image.png

敏感词过滤服务架构:

image.png  

一 Trie 树

Trie 树结构

image.png  

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自动机数据结构图:

image.png

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…

 

总结

来日方长,不忘初心,保持热爱。