技术选型
在当今的互联网应用中,高并发和高性能是衡量一个系统优劣的重要指标,无论是即时通讯、在线客服还是电商平台,都会涉及大量的用户交互,这就需要系统能够高效地处理大量的并发请求
在001项目中,我们选用Golang作为主要的开发语言,主要基于以下几点考虑:
- 高性能:Golang的性能非常接近C语言,能够高效地处理大量并发请求
- 简洁易用:Golang的语法简洁,学习曲线平缓,非常适合快速开发和迭代
- 丰富的生态:Golang拥有相对丰富的第三方库和工具,可以方便地实现各种功能
需求描述
这次的需求是针对001项目的(聚合聊天),相关信息和代码均已进行脱敏处理,核心功能是实现全部会话列表的查询及针对每一个会话增加相应的标记新增和标记取消功能,具体包括以下三个方面:
- 会话列表:按条件查询会话列表,优先展示重要会话
- 会话标记新增:可以对重要会话进行标记增加,方便后续查询和统计
- 会话标记取消:可以已标记的会话进行标记取消,方便后续查询和统计
接口功能逻辑
1. 会话列表查询
通过Gin框架接收请求参数,并调用DAO层的分页查询方法获取数据,最后对缓存、接受数据进行一定的优化/美观处理:
// 会话列表
func ConversationList(ctx context.Context, req entity.ListConversationReq) (resp entity.ListConversationResp, err error) {
tag := "conversationservice.List"
// 查询过去24小时的会话
lastDayTime := gtools.UnixMillis() - gtools.MillsSecondADay
offset := (req.Page - 1) * req.PageSize
total, list, err := dao.NewZqConversationDao().Page(“...”)//数据库查询语句,已注释
resp.PageInfo = entity.BuildPageInfo(req.Page, req.PageSize, cast.ToInt(total))
if err != nil {
logger.Ex(ctx, tag, fmt.Sprintf("查询会话列表失败 req = %+v, err = %v", req, err))
return
}
// 后期优化数据
for _, v := range list {
resp.List = append(resp.List, entity.ZqConversationItem{ZqConversation: *v})
}
return
}
2. 会话标记新增
通过Gin框架接收请求参数,并调用DAO层的方法更新数据库中的标记字段:
// 会话新增标记
func MarkConversationAsImportant(ctx context.Context, req entity.MarkConversationReq) (err error) {
tag := "service.MarkConversationAsImportant"
// 查询会话信息
conversationInfo, err := dao.NewConversationDao().Get(ctx, req.ConvId)
if err != nil || conversationInfo.Id == 0 {
logger.Ex(ctx, tag, fmt.Sprintf("查询会话信息失败 req = %+v, err = %v", req, err))
err = openerror.New(cerror.LostRequiredParameters, `会话不存在`)
return
}
// 检查会话状态是否为关闭
if conversationInfo.Status == types.ConvStatusClose {
logger.Ex(ctx, tag, fmt.Sprintf("会话已关闭,不允许添加新的标记 req = %+v", req))
err = openerror.New(cerror.LostRequiredParameters, "会话已关闭,不允许添加新的标记")
return
}
// 新增标记
_, err = dao.NewConversationDao().SetAll(ctx, req.ConvId, map[string]interface{}{
`is_important`: 1,
`important_time`: gtools.UnixMillis(),
`modify_time`: gtools.UnixMillis(),
})
if err != nil {
logger.Ex(ctx, tag, fmt.Sprintf("会话新增标记失败 req = %+v, err = %v", req, err))
return
}
clearCache(ctx, req.ConvId)
return
}
3. 会话标记取消
会话标记取消功能的实现与标记新增类似,只是需要将标记字段恢复为未标记状态
// 会话取消标记
func UnmarkConversationAsImportant(ctx context.Context, req entity.MarkConversationReq) (err error) {
tag := "service.UnmarkConversationAsImportant"
// 查询会话信息
conversationInfo, err := dao.NewConversationDao().Get(ctx, req.ConvId)
if err != nil || conversationInfo.Id == 0 {
logger.Ex(ctx, tag, fmt.Sprintf("查询会话信息失败 req = %+v, err = %v", req, err))
err = openerror.New(cerror.LostRequiredParameters, `会话不存在`)
return
}
// 取消标记
_, err = dao.NewConversationDao().SetAll(ctx, req.ConvId, map[string]interface{}{
`is_important`: 0,
`important_time`: 0,
`modify_time`: gtools.UnixMillis(),
})
if err != nil {
logger.Ex(ctx, tag, fmt.Sprintf("会话取消标记失败 req = %+v, err = %v", req, err))
return
}
clearCache(ctx, req.ConvId)
return
}
通过上述三个接口的实现已经可以实现需求的具体功能,而针对系统整体的性能,我们还进行了多方面的优化
Page方法对性能的优化
func (c *ConversationDao) Page(ctx context.Context, offset int, limit int, order string, query string, args ...interface{}) (total int64, res []*Conversation, err error) {
res = make([]*Conversation, 0)
slaveDB := c.db.Engine.Slave()
total, err = slaveDB.Table(Conversation{}).Where(query, args...).OrderBy(order).Count()
if err != nil {
return
}
err = slaveDB.Table(Conversation{}).Where(query, args...).OrderBy(order).Limit(limit, offset).Find(&res)
if err != nil {
return
}
return
}
- 分离查询总数和结果集:代码将查询总数和查询结果集的操作分离开来,有助于避免在结果集较大时的一些性能问题,因为单独的计数查询会更快地返回总数,而不需要读取大量数据
- 使用索引排序和分页:在执行查询时,使用了 OrderBy 和 Limit 语句,索引排序和分页有助于减少内存占用和提高查询速度,尤其对于大型数据集而言是非常有效的
- 预先分配切片容量:代码通过 make([ ]*ZqConversation, 0) 预先分配了切片的容量,这可以减少在后续操作中的内存分配次数,从而提高性能(这里的切片容量设置为0,实际效果有限,如果能预估结果集大小,可以考虑预分配合理的容量)
- 连接池和从库:使用 slaveDB(从库)来执行查询操作,减轻主库的读压力,提升读写分离架构的性能
查询结果优化
//后期优化数据
for _, v := range list {
resp.List = append(resp.List, entity.ZqConversationItem{ZqConversation: *v})
}
功能解释:将查询结果列表中的每个会话记录转换为entity.ZqConversationItem
类型,并将其添加到响应列表 resp.List
中(转换为响应所需的格式以确保数据的正确性)
详细分析:
-
遍历
list
:for _, v := range list
遍历从数据库查询出来的会话记录列表list
。list
是一个[]*ZqConversation
类型的切片,每个元素都是一个指向ZqConversation
结构体的指针 -
创建
对于每个entity.ZqConversationItem
实例:v
(*ZqConversation
),通过entity.ZqConversationItem{ZqConversation: *v}
创建一个entity.ZqConversationItem
实例。这里的*v
是将指针v
解引用,得到实际的ZqConversation
结构体 -
添加到
resp.List
:resp.List = append(resp.List, entity.ZqConversationItem{ZqConversation: *v})
将新创建的entity.ZqConversationItem
实例添加到响应列表resp.List
中
主要优化点:
- 使用指针来减少内存拷贝
- 确保数据的完整性和一致性
缓存处理
func clearCache(ctx context.Context, convId int64) {
rdsKey := fmt.Sprintf(rediskey.RdsConversationPkIdCacheKey, convId)
err := cache.NewStringRepo().Del(ctx, rediskey.RdsConversationKey, rdsKey)
if err != nil {
return
}
return
}
功能解释:删除 Redis 缓存中的某个会话记录,以确保缓存数据与数据库保持一致(有轮子可以进一步优化错误处理和批量操作)
详细分析
- 生成 Redis 键:
rdsKey := fmt.Sprintf(rediskey.RdsConversationPkIdCacheKey, convId)
使用convId
生成一个特定的 Redis 键。rediskey.RdsConversationPkIdCacheKey
是一个格式字符串,可能类似于"conversation:%d"
,用来生成唯一的会话缓存键
- 删除 Redis 缓存:
err := cache.NewStringRepo().Del(ctx, rediskey.RdsConversationKey, rdsKey)
调用cache.NewStringRepo().Del
方法来删除 Redis 中与rdsKey
对应的缓存条目
主要优化点:
- 错误处理可以更加详细,记录日志或采取其他措施。
- 可以批量处理缓存删除,以提高性能。
- 确保在高并发情况下,缓存删除的原子性和一致性。
希望本文能为读者提供有价值的参考,帮助大家更好地应对类似的技术挑战