多维度排序算法在企业级应用中的性能优化

多维度排序算法在企业级应用中的性能优化

业务需求:复杂的数据排序场景

在我们的系统中,排序需求非常多样化:

应用列表排序

  • 创建时间:最新创建的应用排在前面
  • 优先级:精选应用优先显示
  • 用户权限:自己创建的应用优先
  • 部署状态:已部署的应用优先

用户管理排序

  • 注册时间:按注册时间倒序
  • 用户角色:管理员优先显示
  • 活跃度:最近登录用户优先

聊天历史排序

  • 时间顺序:最新消息在前
  • 消息类型:系统消息优先
  • 游标分页:基于时间戳的高效分页

面对这样复杂的排序需求,我需要设计一套既高效又灵活的排序方案。

技术架构:统一的排序设计

基础分页请求设计

首先,我设计了一个通用的分页请求基类:

// 来源:src/main/java/com/ustinian/cheeseaicode/common/PageRequest.java (第6-27行)
@Data
public class PageRequest {

    /**
     * 当前页号
     */
    private int pageNum = 1;

    /**
     * 页面大小
     */
    private int pageSize = 10;

    /**
     * 排序字段
     */
    private String sortField;

    /**
     * 排序顺序(默认降序)
     */
    private String sortOrder = "descend";
}

设计亮点:

  1. 统一标准:所有分页查询都继承这个基类,保证接口的一致性
  2. 默认值设置:合理的默认分页大小和排序方向,减少前端配置
  3. 灵活排序:支持动态指定排序字段和方向
  4. 简洁设计:只包含核心字段,避免过度设计

应用查询请求实现

// 来源:src/main/java/com/ustinian/cheeseaicode/model/dto/app/AppQueryRequest.java (第12-57行)
@EqualsAndHashCode(callSuper = true)
@Data
public class AppQueryRequest extends PageRequest implements Serializable {

    /**
     * id
     */
    private Long id;

    /**
     * 应用名称
     */
    private String appName;

    /**
     * 应用封面
     */
    private String cover;

    /**
     * 应用初始化的 prompt
     */
    private String initPrompt;

    /**
     * 代码生成类型(枚举)
     */
    private String codeGenType;

    /**
     * 部署标识
     */
    private String deployKey;

    /**
     * 优先级
     */
    private Integer priority;

    /**
     * 创建用户id
     */
    private Long userId;

    private static final long serialVersionUID = 1L;
}

这个设计继承了 PageRequest,既包含了分页和排序功能,又包含了应用相关的查询条件。

核心实现:QueryWrapper 的排序机制

应用列表的排序实现

// 来源:src/main/java/com/ustinian/cheeseaicode/service/impl/AppServiceImpl.java (第104-128行)
@Override
public QueryWrapper getQueryWrapper(AppQueryRequest appQueryRequest) {
    if (appQueryRequest == null) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
    }
    Long id = appQueryRequest.getId();
    String appName = appQueryRequest.getAppName();
    String cover = appQueryRequest.getCover();
    String initPrompt = appQueryRequest.getInitPrompt();
    String codeGenType = appQueryRequest.getCodeGenType();
    String deployKey = appQueryRequest.getDeployKey();
    Integer priority = appQueryRequest.getPriority();
    Long userId = appQueryRequest.getUserId();
    String sortField = appQueryRequest.getSortField();
    String sortOrder = appQueryRequest.getSortOrder();
    return QueryWrapper.create()
            .eq("id", id)
            .like("appName", appName)
            .like("cover", cover)
            .like("initPrompt", initPrompt)
            .eq("codeGenType", codeGenType)
            .eq("deployKey", deployKey)
            .eq("priority", priority)
            .eq("userId", userId)
            .orderBy(sortField, "ascend".equals(sortOrder));
}

实现要点分析:

  1. 条件过滤:使用 eq() 进行精确匹配,like() 进行模糊搜索
  2. 动态排序orderBy(sortField, "ascend".equals(sortOrder)) 实现动态排序
  3. 空值处理:MyBatis-Flex 会自动忽略 null 值的条件
  4. 性能考虑:链式调用构建查询条件,避免重复创建对象

用户列表的排序实现

// 来源:src/main/java/com/ustinian/cheeseaicode/service/impl/UserServiceImpl.java (第157-175行)
@Override
public QueryWrapper getQueryWrapper(UserQueryRequest userQueryRequest) {
    if (userQueryRequest == null) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "请求参数为空");
    }
    Long id = userQueryRequest.getId();
    String userAccount = userQueryRequest.getUserAccount();
    String userName = userQueryRequest.getUserName();
    String userProfile = userQueryRequest.getUserProfile();
    String userRole = userQueryRequest.getUserRole();
    String sortField = userQueryRequest.getSortField();
    String sortOrder = userQueryRequest.getSortOrder();
    return QueryWrapper.create()
            .eq("id", id)
            .eq("userRole", userRole)
            .like("userAccount", userAccount)
            .like("userName", userName)
            .like("userProfile", userProfile)
            .orderBy(sortField, "ascend".equals(sortOrder));
}

用户列表的排序逻辑与应用列表类似,但查询字段根据业务需求有所不同。这体现了我们设计的通用性和灵活性。

聊天历史的特殊排序实现

聊天历史的排序比较特殊,因为涉及到游标分页:

// 来源:src/main/java/com/ustinian/cheeseaicode/service/impl/ChatHistoryServiceImpl.java (第86-116行)
@Override
public QueryWrapper getQueryWrapper(ChatHistoryQueryRequest chatHistoryQueryRequest) {
    QueryWrapper queryWrapper = QueryWrapper.create();
    if (chatHistoryQueryRequest == null) {
        return queryWrapper;
    }
    Long id = chatHistoryQueryRequest.getId();
    String message = chatHistoryQueryRequest.getMessage();
    String messageType = chatHistoryQueryRequest.getMessageType();
    Long appId = chatHistoryQueryRequest.getAppId();
    Long userId = chatHistoryQueryRequest.getUserId();
    LocalDateTime lastCreateTime = chatHistoryQueryRequest.getLastCreateTime();
    String sortField = chatHistoryQueryRequest.getSortField();
    String sortOrder = chatHistoryQueryRequest.getSortOrder();
    // 拼接查询条件
    queryWrapper.eq("id", id)
            .like("message", message)
            .eq("messageType", messageType)
            .eq("appId", appId)
            .eq("userId", userId);
    // 游标查询逻辑 - 只使用 createTime 作为游标
    if (lastCreateTime != null) {
        queryWrapper.lt("createTime", lastCreateTime);
    }
    // 排序
    if (StrUtil.isNotBlank(sortField)) {
        queryWrapper.orderBy(sortField, "ascend".equals(sortOrder));
    } else {
        // 默认按创建时间降序排列
        queryWrapper.orderBy("createTime", false);
    }
    return queryWrapper;
}

聊天历史排序的特殊处理:

  1. 游标分页:使用 lastCreateTime 作为游标,实现高效的时间分页
  2. 默认排序:当没有指定排序字段时,默认按创建时间降序
  3. 时间过滤lt("createTime", lastCreateTime) 实现游标查询

分页查询的完整流程

控制器层的分页处理

// 来源:src/main/java/com/ustinian/cheeseaicode/controller/AppController.java (第200-213行)
User loginUser = userService.getLoginUser(request);
// 限制每页最多 20 个
long pageSize = appQueryRequest.getPageSize();
ThrowUtils.throwIf(pageSize > 20, ErrorCode.PARAMS_ERROR, "每页最多查询 20 个应用");
long pageNum = appQueryRequest.getPageNum();
// 只查询当前用户的应用
appQueryRequest.setUserId(loginUser.getId());
QueryWrapper queryWrapper = appService.getQueryWrapper(appQueryRequest);
Page<App> appPage = appService.page(Page.of(pageNum, pageSize), queryWrapper);
// 数据封装
Page<AppVO> appVOPage = new Page<>(pageNum, pageSize, appPage.getTotalRow());
List<AppVO> appVOList = appService.getAppVOList(appPage.getRecords());
appVOPage.setRecords(appVOList);
return ResultUtils.success(appVOPage);

分页流程解析:

  1. 参数校验:限制每页最多查询 20 个应用,防止大查询
  2. 权限控制:设置 userId 确保只查询当前用户的应用
  3. 查询执行:使用 page() 方法执行分页查询
  4. 数据转换:将实体对象转换为 VO 对象,保护敏感数据
  5. 结果封装:返回包含分页信息的完整结果

性能优化策略

1. 数据库层面的优化

索引设计:

-- 应用表的复合索引
CREATE INDEX idx_app_user_priority_time ON app(userId, priority, createTime DESC);
CREATE INDEX idx_app_codegen_time ON app(codeGenType, createTime DESC);

-- 用户表的索引
CREATE INDEX idx_user_role_time ON user(userRole, createTime DESC);

-- 聊天历史的索引
CREATE INDEX idx_chat_app_time ON chat_history(appId, createTime DESC);

索引选择原则:

  1. 高选择性字段优先:将选择性高的字段放在索引前面
  2. 排序字段在最后:排序字段通常放在复合索引的最后
  3. 覆盖索引优化:尽量让索引覆盖查询的所有字段

2. 应用层面的优化

分页大小限制:

// 限制每页最多 20 个
long pageSize = appQueryRequest.getPageSize();
ThrowUtils.throwIf(pageSize > 20, ErrorCode.PARAMS_ERROR, "每页最多查询 20 个应用");

查询缓存策略:

// 来源:src/main/java/com/ustinian/cheeseaicode/controller/AppController.java (精选应用缓存)
@PostMapping("/good/list/page/vo")
@Cacheable(
        value = "good_app_page",
        key = "T(com.ustinian.cheeseaicode.utils.CacheKeyUtils).generateKey(#appQueryRequest)",
        condition = "#appQueryRequest.pageNum <= 10"
)
public BaseResponse<Page<AppVO>> listGoodAppVOByPage(@RequestBody AppQueryRequest appQueryRequest) {
    // 查询逻辑
}

游标分页优化:

// 来源:src/main/java/com/ustinian/cheeseaicode/service/impl/ChatHistoryServiceImpl.java (第152-155行)
QueryWrapper queryWrapper = QueryWrapper.create()
        .eq(ChatHistory::getAppId, appId)
        .orderBy(ChatHistory::getCreateTime, false)
        .limit(1, maxCount);
posted @ 2025-09-09 17:05  你小志蒸不戳  阅读(5)  评论(0)    收藏  举报