基于nest、redis实现消息(邮件、短信、websocket、app)推送系统

系统功能


概述

针对各子系统普遍需要消息推送的功能需求(订单流程中的短信、邮件消息推送),为避免冗余代码,将推送的功能抽为一独立的平台,各子系统通过通过普通的RESTful接口或者消息(kafka)生成任务。
推送功能从推送方式可以是邮件、短信、WebSocket、App任意一种类型,任务可以立即执行,也可以是定时执行,为满足多次提醒的功能,在延时任务的基础之上又扩展了循环任务。循环任务是一种特殊的延时任务,任务执行之后根据任务类型标记动态的重新注入任务,直至全部任务完成,(详解见系统编码)

结构图

系统需求功能设计

技术难点

redis任务队列

  1. 定时任务|任务队列
    定时任务的实现方式有很多,node定时模块node-schedule、原生API中的定时器适合单一任务的执行,并不适合多任务并行的情况(不适合 !== 不能)。

* 引入消息队列主要是用于各服务的解耦;
* 就系统本身而言,推送任务可能会存在看消息量大,并发量高的问题,而引入消息队列可以起到削峰的作用
* 消息推送中有特别重要的一点:异步,服务的调用方在大部分的场合可能对消息推送结果的回调的需求并不是很高,消息生产者通过特定的通道发起一个消费的请求后,可以继续之后的流程;(只适合不依赖执行反馈的情景【串行不适合】)

系统中使用了node生态中比较好的模块bull,流程图如下,系统调用方发起任务请求,系统首先对请求的合法性进行校验(权限校验见下一部分),之后根据不同类型push进任务执行栈,在bull中的任务大致有六种状态,等待,延时,激活,完成,失败,在任务栈中的任务状态标记为激活时,任务进入执行阶段,直到结束进入下一任务。


* bull:github: https://github.com/OptimalBits/bull
* bull:底层利用的redis的发布订阅特性
bull流程图

权限校验

1、身份校验
为防止系统被恶意调用,恶意注入任务,应用入口以及网关层面做身份鉴权是非常有必要的,本文只对应用入口的鉴权进行讲解,关于网关的校验的会在之后的博客中讲解。
系统中也是采用主流的JWT的token机制,服务生成token,之后客户端每一次请求必须携带token,单微服务的设计来说,单系统的身份认证完全可以迁移到网关层面去实现。

2、应用鉴权
应用鉴权在单系统中是主要的功能,系统中采用node中负责安全的模块crypto
crypto的简单使用见一下代码块

  • crypto.publicEncrypt(key, buffer)(加密)
  • crypto.privateDecrypt(privateKey, buffer)(解密)
import moment = require('crypto');
// 加密
const encrypt = (data, key) => {
    return crypto.publicEncrypt(key, Buffer.from(data));
}
// 解密
const decrypt = (data, key) => {
    return crypto.privateDecrypt(key, encrypted);
}
const test = '测试加密信息'
// 加密
const crypted = encrypt(test, keys.pubKey); 
 // 解密
const decrypted = decrypt(crypted, keys.privKey);


keys.privKey   keys.pubKey  分别为对应的私钥和公钥
[crypto](https://nodejs.org/api/crypto.html)

系统编码

此次博文中对各模块的具体实现不做详细讲解,只对通用方法进行讲解

  • redis通用方法封装
## redis封装
import {HttpService, Inject, Injectable} from '@nestjs/common';
import { RedisService } from 'nestjs-redis';
@Injectable()
export class RedisCacheService {[https://github.com/OptimalBits/bull](https://github.com/OptimalBits/bull)
    public client;
    constructor(@Inject(RedisService) private redisService: RedisService) {
        this.getClient().then(r => {} );
    }
    async getClient() {
        this.client = await this.redisService.getClient();
    }

    // 设置值的方法
    async set(key: string, value: any, seconds?: number) {
        value = JSON.stringify(value);
        if (!this.client) {
            await this.getClient();
        }
        if (!seconds) {
            await this.client.set(key, value);
        } else {
            await this.client.set(key, value, 'EX', seconds);
        }
    }

    // 获取值的方法
    async get(key: string) {
        if (!this.client) {
            await this.getClient();
        }
        const data: any = await this.client.get(key);
        if (!data) { return; }
        return JSON.parse(data);
    }
}

  • bull任务注入
// TaskService.ts
export class TaskService {
    constructor(
        // 服务中注入已经创建好的任务队列的实例
        @InjectQueue('email') private readonly emailNoticeQueue: Queue,
        @InjectQueue('message') private readonly messageNoticeQueue: Queue,
    ) { }
    // delayValue: 延时时长
    public async addTask() {
         await this.emailNoticeQueue.add('emitEmail', {
                 id: taskResult.insertedId,
                config: config.name,
         }, { delay: delayValue});
    }
}

@Injectable()
@Processor('email')
export class NoticeEmailProcessor {
    constructor(
        @InjectQueue('email') private readonly emailNoticeQueue: Queue,
        private readonly redisCacheService: RedisCacheService,
        private readonly taskLogService: TaskLogService,
        private readonly taskEmitEmailService: TaskEmitEmailService,
        @Inject(TaskService)
        private readonly taskService: TaskService,
    ) {}

    @Process('emitEmail')
    public async handleTranscode(job: Job) {
     
    }

    /**
     * 下一步执行任务()
     */
    protected async nextLoopTak(task: TaskEntity, isSuccessFlag: boolean, status: number) {
       
    }
}
  • 日期处理采用moment.js

https://github.com/moment/moment

系统部署

系统采用docker的部署,系统默认会启动在10001端口之上

1、DOCKERFILE

FROM ubuntu
MAINTAINER canyuegongzi
ENV HOST 0.0.0.0
RUN mkdir -p /app
COPY .. /app
WORKDIR /app
EXPOSE 10001
RUN apt-get update && \
    apt-get install redis-server && \
    npm config set registry https://registry.npm.taobao.org && \
    npm install && \
    npm install pm2 -g
CMD ["sh", "-c", "redis-server && pm2-runtime start ecosystem.config.js"]

2、pm2启动脚本

## ecosystem.config.js
module.exports = [{
    script: 'dist/main.js',
    name: 'simpleNoticeCenterApi',
    exec_mode: 'cluster',
    instances: 2
}]

结言

系统目前处于持续迭代开发中,可能会存在不同程度的问题,就普通的消息推送还是可以完成的,之后将进一步提升系统的安全性,其他的功能也会陆续迭代。详细代码讲解会在后续博客讲解,包括前后端。
原文地址:http://blog.canyuegongzi.xyz/

资源

github

消息推送系统

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348