注:文接上回
需求描述
这次的需求同样是针对 001 项目的(聚合聊天),背景为在会话资源获取失败的情况下,进行重新获取,针对这次需求的实现,分为了两个部分:接口和逻辑。接口进行相应的判断,验证是否需要调用 RPC 协议进行重新请求,逻辑对应请求成功后进行数据更新、前端推送两个部分。话不多说直入正题
消息重新获取 接口
1、通过会话 ID 查询会话信息
//查询会话信息
conversationInfo, err := dao.NewZqConversationDao().Get(ctx, req.ConvId)
if err != nil || conversationInfo.Id == 0 {
logger.Ex(ctx, tag, fmt.Sprintf("查询会话信息失败 req = %+v, err = %v", req, err))
return
}
轮子 Get :
func (c *ZqConversationDao) Get(ctx context.Context, id int64) (res *ZqConversation, err error) {
res = new(ZqConversation)
slaveDB := c.db.Engine.Slave()
_, err = slaveDB.Table(ZqConversation{}).ID(id).Get(res)
if err != nil {
return
}
return
}
轮子通过指定的 id
从数据库的从库中获取一条 ZqConversation
记录,它首先初始化一个新的 ZqConversation
实例,并将查询结果填充到该实例中,使用从库来执行读操作也减轻了主库的负担
2、查询消息表信息
//查询消息表信息
hkUserMsgInfo, err := dao.NewZqMsgDao().GetMessageByMsgId(ctx, conversationInfo.HkUserid, req.MsgId)
if err != nil || hkUserMsgInfo.Id == 0 {
logger.Ex(ctx, tag, fmt.Sprintf("查询消息表信息失败 req = %+v, err = %v", req, err))
return
}
轮子 GetMessageByMsgId :
func (c *ZqMsgDao) GetMessageByMsgId(ctx context.Context, hkUserId string, msgId int64) (res *ZqMsg, err error) {
res = new(ZqMsg)
slaveDB := c.db.Engine.Slave().Table(new(ZqMsg).TableName(hkUserId))
_, err = slaveDB.Where(` hk_userid = ? and msg_id = ?`, hkUserId, msgId).Get(res)
if err != nil {
return
}
return
}
注:该轮子的查询操作中使用了动态表名(通过 TableName(hkUserId)
生成)
3、针对资源类型进行区分、添加 if 语句即可
4、针对消息内容进行解密,采用 DecryptMsgContent 方法
// 消息内容解密
func DecryptMsgContent(ctx context.Context, content string) (decryptContent string, err error) {
aesKey := config.GetConf("MsgEncrypt", "AesKey")
if aesKey == "" || len(aesKey) != 32 {
err = errors.New("aes key is empty or length is not 32")
return
}
if content == "" {
err = errors.New("content is empty")
return
}
//如果消息内容是json格式,则不需要解密
if strings.HasPrefix(content, `{`) && strings.HasSuffix(content, `}`) {
decryptContent = content
return
}
decodeString, err := base64.StdEncoding.DecodeString(content)
if err != nil {
return
}
aesByte, err := gtools.AesDecrypt(decodeString, []byte(aesKey))
if err != nil {
return
}
decryptContent = string(aesByte)
return
}
5、针对解密后的消息进行 content 字段析出,采用 json.Unmarshal 即可
6、对 content 字段下的 file_path 路径进行判断(是否正常)
7、不正常的话则调用 RPC 协议进行请求
// 调用RPC协议重新获取资源
rpcReq := &proto.RetryUploadMediaRequest{
Request: proto.Request{
HkUserId: hkUserMsgInfo.HkUserid,
CorpId: conversationInfo.CorpId,
},
MsgId: hkUserMsgInfo.MsgId,
ServerId: hkUserMsgInfo.ServerId,
Type: hkUserMsgInfo.ContentType,
}
rpcResp := &proto.RetryUploadMediaResponse{}
err = client.RetryUploadMedia(ctx, rpcReq, rpcResp)
logger.Ix(ctx, tag, fmt.Sprintf("开始重新获取资源 req:%+v resp:%+v", rpcReq, rpcResp))
if err != nil {
logger.Ex(ctx, tag, fmt.Sprintf("RPC调用失败 req = %+v, err = %v", req, err))
return
}
Consume 消费逻辑
1、配置文件根据逻辑的实现方案进行相应的增加,Kafka、Redis、Redis 的有序队列等
2、handle 配置
// IM消息重新加载
handleMap["im_msg_reload"] = handle.ImMsgReload
3、消费逻辑实现
1)解析 data 数据(涉及反序列化、验证等操作),并进行相应的 msg_id、file_path判断
//解析event.data数据
eventData, err := json.Marshal(imMsg.Data)
if err != nil {
logger.Ex(ctx, tag, fmt.Sprintf("event data json.Marshal hk_userid:%s req:%+v err:%+v", imMsg.HkUserid, imMsg.Data, err))
return
}
2)查询消息并进行消息内容解密
//查询消息
_, err = dao.NewZqMsgDao().GetSlaveDB(imMsg.HkUserid).Where("hk_userid = ? and msg_id = ?", imMsg.HkUserid, eventBody.MsgId).Get(&hkUserMsgInfo)
if err != nil {
logger.Ex(ctx, tag, fmt.Sprintf("get msg err,hk_userid:%s msg_id: %d err:%+v", imMsg.HkUserid, eventBody.MsgId, err))
return
}
if hkUserMsgInfo.Id == 0 {
logger.Ex(ctx, tag, fmt.Sprintf("msg not found,hk_userid:%s msg_id: %d", imMsg.HkUserid, eventBody.MsgId))
return
}
content := ""
//消息内容解密
content, err = msgdata.DecryptMsgContent(ctx, hkUserMsgInfo.Content)
if err != nil {
logger.Ex(ctx, tag, fmt.Sprintf("msg content decrypt err,hk_userid:%s msg_id: %d err:%+v", imMsg.HkUserid, eventBody.MsgId, err))
return
}
if len(content) == 0 {
logger.Ex(ctx, tag, fmt.Sprintf("消息解密失败 hk_userid:%s msg_id: %d", imMsg.HkUserid, eventBody.MsgId))
return
}
if content == "" {
logger.Ex(ctx, tag, fmt.Sprintf("msg content empty,hk_userid:%s msg_id: %d", imMsg.HkUserid, eventBody.MsgId))
return
}
3)重新序列化并进行加密、更新资源路径
4)更新消息后推送给前端
总结与思考
在构建一个高并发、高性能的系统时,尤其是在处理像消息重新获取这样的功能时,涉及到多个复杂的环节和技术难点,消息的重新获取不仅仅是一次简单的RPC调用和数据更新,而是在整个系统的运行中,如何有效地处理大量并发请求,保持系统的高可用性和响应速度,这些都是必须要考虑的问题
1、高并发场景下的资源争用与锁机制
在一个高并发的系统中,当多个请求同时访问同一个资源时,资源争用的问题便会显现出来
以消息重新获取为例,如果多个并发请求同时尝试更新同一条消息,可能会导致数据的不一致性或资源的死锁,因此,在设计这类功能时,如何正确地使用锁机制,避免资源争用就成为关键的一环,在消息重新获取的实现中,可以考虑在更新消息内容前使用乐观锁,通过比较消息的版本号或时间戳来判断是否有其他请求已经修改了该消息,避免由于并发修改导致的数据不一致性
2、异步处理与消息队列的引入
在高并发系统中,直接同步处理所有请求往往会导致系统的瓶颈,影响响应时间,为了提高系统的性能,异步处理和消息队列的引入显得尤为重要
消息重新获取的场景中,重新获取资源的操作可能涉及到复杂的业务逻辑和外部服务的调用,这些操作的耗时和不确定性较高。因此,将这些操作放在消息队列中异步处理,可以极大地提升系统的响应速度,用户的请求可以立即返回,而资源的重新获取操作则在后台进行处理,当处理完成后,可以通过回调或消息通知的方式告知用户
引入消息队列不仅能提高系统的吞吐量,还能实现请求的削峰填谷,通过控制消息的消费速度,系统能够在高峰期平稳处理大量请求,避免系统因瞬时的高并发而崩溃。此外,消息队列还具有良好的扩展性,能够通过增加消费者实例来水平扩展系统的处理能力
3、缓存策略的应用、数据库的读写分离与分库分表
在高并发场景下,合理地使用缓存、同时为了提高系统的扩展性和性能,可以考虑将数据库进行读写分离或分库分表
4、限流与熔断机制
限流是通过对请求的频率或数量进行限制,来保护系统的资源不被耗尽,对于消息重新获取的场景,可以对RPC调用的频率进行限制,避免瞬时大量的重新获取请求导致服务的崩溃
熔断机制则是当检测到某个服务的调用失败率或响应时间超过一定阈值时,自动停止对该服务的调用,从而保护系统的其他部分不受影响,熔断机制通常可以与降级策略结合使用,当某个服务被熔断时,系统可以返回预设的降级数据或执行其他备选方案,以保证用户的体验
5、日志与监控的全方位覆盖
及时的日志记录和监控可以帮助我们快速定位问题,进行故障排查和性能优化
日志不仅仅是记录系统的运行状态,更是帮助我们分析系统瓶颈、发现潜在问题的重要工具,在消息重新获取的过程中,每一个关键步骤都有详细的日志记录,如RPC调用的请求与响应、数据的加密与解密、数据库的查询与更新等,当系统出现异常时,日志能够提供丰富的上下文信息,帮助开发人员快速找到问题的根源
监控则是对系统运行状态的实时监测,通过监控可以了解系统的负载情况、请求的响应时间、数据库的性能等关键指标,通过监控可以及时发现系统的瓶颈,如请求排队时间过长、数据库连接数耗尽等,从而进行有针对性的优化
写在后面
在构建一个高并发、高性能的系统时,需要综合考虑各个方面的因素,合理设计系统的架构和策略,只有这样,才能在面对大规模流量和复杂业务场景时,依然保持系统的高可用性和高性能,希望本文能为读者提供有价值的参考,帮助大家更好地应对类似的技术挑战