进程内的消息通知

背景

我们团队,由于原python项目由于可维护性的降低、开发成本和周期的增长、扩展性差,已经逐渐适应不了公司业务的发展,面临不得不重构项目。该项目主要使用django+celery单体服务、29万行pyton代码、约1500个接口,包含仓库中的商品信息、仓库信息、库存管理、库内流程、出库流程、入库流程等各种业务。在业务发展初期,为了快速的完成业务迭代,模块间的通信方式都是直接函数调用,这种方式简单、直观。但随着业务的发展,直接调用的方式使得整个项目耦合严重、理解难度增加,进而使得开发效率骤减、bug急剧增加。因此,在重构前,我们有必要制定一套能够降低业务复杂度的模块间通信方式标准。

备注: 由于整个供应链技术栈降逐渐转向golang,所以不对其他语言进行探讨。同时,由于担心出现分布式的微服务,所以重构项目暂不考虑服务的拆分。

项目挑战

业务逻辑复杂,细分业务多达上百个,且相互间都有数据关联;

交易数据,不能有任何一条数据错误或者丢失;

业务需求旺盛,要求迭代速度快,且运行足够稳定;

业务场景

复盘过去业务中,遇到的具体模块耦合场景。仓库作业中,不同的业务需要都生成和修改移库任务,移库任务状态的更新需要同步对应的业务。

在这里插入图片描述

这样造成的问题是:移库模块早已在在开发初期完成,但是随着其他可以创建并跟踪移库任务的业务接入,直接函数调用这种方式使得移库业务代码需要经常改动,既增加了改动的风险,同时也增加了移库模块负责人理解其他业务的成本。进而使得整个项目揉成一团,变得难以理解和维护。所以,重构时,我们需要一套完善的解耦方案,既能满足业务需求,同时又能降低代码复杂度、提升代码的可维护性。

备注: 项目中celery的使用方式并没有让业务解耦,只不过是异步,业务逻辑仍然是直接调用,并没有因此变得更简单和清晰。

探寻解决方案

方案一: 依赖注入和控制反转(wire、inject、dig)
缺点:1、需要生成代码,但这部分代码又需要频繁的改动,不方便维护;2、只能同步处理,场景受限制;3、不能被多个业务消费;4、扩展性弱,倘若将来某一天需要将服务拆分,改动成本很大;

方案二: 引入消息中间件(kafka、rabbitmq)
缺点:1、引入第三方组件,潜在的降低了服务的可用性。公司的kafka和rabbitmq都出现过短暂时间内的不可用;2、直接使用消息中间件,有丢消息的可能,所以,还需要做一些封装;

方案三: 进程内的消息通知
在这里插入图片描述

移库任务view层操作后,发布领域事件,需要的业务方订阅并消费领域事件,更新各自的数据即可。这种方式降低了业务模块之间的耦合,且满足业务需求。

方法简介

type HandlerMethod = func(ctx context.Context, message interface{}) *wmserror.WMSError {}

type ProcessTask struct {
    taskName      string
    handleName    string
    handlerMethod map[string]HandlerMethod
    message       interface{}
}

var ProcessTaskMap = make(map[string]*ProcessTask)

//注册消息处理
func RegisterMessageHandler(taskName string, handler HandlerMethod, message interface{}) {
    handlerName := getMethodName(handler)

    //同一个taskName有多个处理方法
    if task, ok := ProcessTaskMap[taskName]; ok {
        task.handlerMethod[handlerName] = handler
        return
    }

    ProcessTaskMap[taskName] = &ProcessTask{
        taskName:      taskName,
        handlerMethod: map[string]HandlerMethod{handlerName: handler},
        message:       message,
    }
}

//发送本进程异步消息
func SendMessageInProcess(ctx context.Context, taskName string, message interface{}) *wmserror.WMSError {
    for tHandlerName, tHandler := range task.handlerMethod {
        handler := tHandler
        go func() {
            ct := newContext(ctx)
            err := handler(ct, message)
            if err != nil {
                return err
            }
        }()
    }
    return nil
}

//发送本进程同步消息 
func SyncMessageInProcess(ctx context.Context, taskName string, message interface{}) *wmserror.WMSError {
    for tHandlerName, tHandler := range task.handlerMethod {
        handler := tHandler
        func() {
            err := handler(ct, message)
            if err != nil {
                return err
            }
        }()
    }
    return nil
}

RegisterMessageHandler: 注册taskName队列的消息处理函数、消息的反序列化对象(类型); SendMessageInProcess: 异步将message对象发送至taskName队列, 此时注册的handler方法将不在方法执行的协程内执行; SyncMessageInProcess: 同步将message对象发送至taskName队列, 此时注册的handler方法将在方法执行的协程内执行;

数据一致性的保障

大部分场景中,消息的消费需要一致性的保障。

方案一: 事务中消息通知同步执行;
优点:

强一致性。在同一个协程中,将SyncMessageInProcess放在事务中执行,所用handler方法也会在事务中执行;

实现简单,框架和业务都没有多余的代码要写,非常适合部分业务场景;缺点:

业务一定逻辑成功的消费消息中出现网络失败时,将导致主流程事务的失败。如果出现网络请求对端处理请求成功,但返回失败,但用户不再重试了,那么系统将出现不一致。所以,这种方式也有自身的局限性;

接口耗时增加。其他业务的耗时会拖累主要业务的执行;

方案二: 事务中写消息,异步中消费消息;
优点:

不影响主流程功能、耗时;缺点:

只能保证最终一致性;

业务可能逻辑失败的消费消息中,需要写发送消息发的回滚方法(这会导致业务代码过于复杂,所以在实际业务中,是禁止在消费消息中也逻辑失败的业务代码);

目前,两种方式都有支持。业务开发可根据场景自己选择。

需要注意的点

1. 消息发送和消费记录
达到重试次数依然失败、服务异常终止场景中,消息记录可以确保一定成功消费;

2. 优雅退出
当进程内还有未完成消费的消息,进程不关闭。若直接关闭,将导致消息消费失败,这样只能在定时核对消息时,再次触发,会导致系统更长时间的不一致;

3. 支持重试机制
部分场景中,消费者需要调用外部接口,一定的重试次数可以保证消费尽可能成功,同时,可以减少人工干预,降低系统运营的成本;

4. 消息可由外部触发
方便调试,提高开发效率。失败消息可手动再次触发;

5. 消息循环消费检查
保证整个项目的健壮性;

未来的使用场景

仓库业务中,通常会有非常复杂的查询条件。目前,我们系统中,根据查询条件的不同,联不同的表,出现的最复杂的查询条件需要7张表的联表操作。这种方式扩展性差,随着业务迭代,性能、可维护性都不再能满足业务需求。数据的大量冗余,会来的问题是数据库中的字段和数据膨胀,理解和维护难度的增加。所以,引入其他存储组件是必然,经过一定的调研后,ES成为我们主要考虑引入的查询存储,高性能,对索引非常友好;

同步VS异步

方案一: 同步多写
缺点:

增加了写逻辑负担、耗时;

ES的写入不能在事务中进行。在以下场景中,可能出现不一致: 开启事务,先写Mysql,再写ES,当出现Mysql写入成功,es实际写出成功,但返回失败或者超时的时候,Mysql中的事务将会回滚,而ES已经成功无法回滚。

方案二: 同步发送消息,异步消费消息
优点:1、不影响主逻辑执行;2、可以确保消息消费成功;

结论: 随着复杂查询的业务场景和请求量的增加,进程内的消息通知(事务中写消息,异步消费消息)将发挥更重要的作用,同步发送消息保证事务性,异步写消息保证性能和可用性,消息发送和消费记录保证数据的最终一致性。

使用建议:

1. 消息通知统一在view发送,禁止在manager中发送
前提:任何一个manager的所有写操作尽量在某一个view中,即manager是写收拢的。消息发送放在manager中,业务开发更方便,但同时也增加了循环消费的风险

2. 消息处理函数的实现在view中
由于业务逻辑都在view中,这种方式可以方便view中业务逻辑的复用

3. 不推荐(禁止)消息处理函数再发消息
已经变成异步了,对执行时长就没有这么敏感了。如果没有顺序依赖,可以注册多个处理方法,如果对顺序有依赖,可以在一个注册方法里面顺序完成。消息消费过程中再发消息,会增加软件整体的复杂性,导致业务逻辑不集中、理解成本增加,问题难以定位

4. 消息处理函数不能有需要处理的逻辑失败
即消息处理函数只能是:不需要处理的逻辑失败,网络失败。若消息可以逻辑失败,则发送消息必须有回滚方法,这样会增加整体的复杂度,增加维护成本

#5. 消息使用过程中,不做有序性假设
6. 消息的消费应该是等幂的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园整体解决方案是响应国家教育信息化政策,结合教育改革和技术创新的产物。该方案以物联网、大数据、人工智能和移动互联技术为基础,旨在打造一个安全、高效、互动且环保的教育环境。方案强调从数字化校园向智慧校园的转变,通过自动数据采集、智能分析和按需服务,实现校园业务的智能化管理。 方案的总体设计原则包括应用至上、分层设计和互联互通,确保系统能够满足不同用户角色的需求,并实现数据和资源的整合与共享。框架设计涵盖了校园安全、管理、教学、环境等多个方面,构建了一个全面的校园应用生态系统。这包括智慧安全系统、校园身份识别、智能排课及选课系统、智慧学习系统、精品录播教室方案等,以支持个性化学习和教学评估。 建设内容突出了智慧安全和智慧管理的重要性。智慧安全管理通过分布式录播系统和紧急预案一键启动功能,增强校园安全预警和事件响应能力。智慧管理系统则利用物联网技术,实现人员和设备的智能管理,提高校园运营效率。 智慧教学部分,方案提供了智慧学习系统和精品录播教室方案,支持专业级学习硬件和智能化网络管理,促进个性化学习和教学资源的高效利用。同时,教学质量评估中心和资源应用平台的建设,旨在提升教学评估的科学性和教育资源的共享性。 智慧环境建设则侧重于基于物联网的设备管理,通过智慧教室管理系统实现教室环境的智能控制和能效管理,打造绿色、节能的校园环境。电子班牌和校园信息发布系统的建设,将作为智慧校园的核心和入口,提供教务、一卡通、图书馆等系统的集成信息。 总体而言,智慧校园整体解决方案通过集成先进技术,不仅提升了校园的信息化水平,而且优化了教学和管理流程,为学生、教师和家长提供了更加便捷、个性化的教育体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值