共计 2989 个字符,预计需要花费 8 分钟才能阅读完成。
背景痛点分析
在开发技能管理系统时,随着用户量和技能数据量的增长,传统的全表扫描查询方式会带来严重的性能问题。主要表现在:

- 页面加载时间超过 3 秒,用户体验急剧下降
- 数据库服务器 CPU 和内存占用率高
- 前端渲染大量 DOM 节点导致页面卡顿
技术选型对比
1. ORM 查询
优点:
- 开发效率高
- 代码可读性好
- 支持多种数据库
缺点:
- 生成的 SQL 可能不够优化
- 复杂查询性能较差
2. 原生 SQL
优点:
- 可以精确控制 SQL 语句
- 复杂查询性能好
缺点:
- 开发效率低
- 存在 SQL 注入风险
3. 缓存策略
优点:
- 减少数据库压力
- 响应速度快
缺点:
- 数据一致性需要额外处理
- 内存占用高
核心实现细节
数据库索引优化
-
为常用查询字段建立索引
CREATE INDEX idx_skill_name ON skills(name); CREATE INDEX idx_skill_category ON skills(category); -
避免索引失效的情况
- 不要在索引字段上使用函数
- 避免使用不等于 (!=, <>) 查询
分页查询的最佳实践
后端实现:
@GetMapping("/skills")
public Page<Skill> getSkills(@RequestParam int page,
@RequestParam int size) {return skillRepository.findAll(PageRequest.of(page, size));
}
前端实现:
async function loadSkills(page) {const response = await axios.get(`/skills?page=${page}&size=20`);
this.skills = response.data.content;
}
前端虚拟滚动技术
使用 vue-virtual-scroller 组件实现:
<template>
<RecycleScroller
:items="skills"
:item-size="50"
key-field="id"
>
<template v-slot="{item}">
<div class="skill-item">
{{item.name}}
</div>
</template>
</RecycleScroller>
</template>
完整代码示例
后端 Spring Boot 代码
@RestController
@RequestMapping("/api")
public class SkillController {
@Autowired
private SkillRepository skillRepository;
@GetMapping("/skills")
public ResponseEntity<Page<Skill>> getSkills(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
// 使用 JPA 分页查询
Page<Skill> skills = skillRepository.findAll(PageRequest.of(page, size, Sort.by("name"))
);
return ResponseEntity.ok(skills);
}
// 添加缓存注解
@Cacheable(value = "skills", key = "#id")
@GetMapping("/skills/{id}")
public ResponseEntity<Skill> getSkillById(@PathVariable Long id) {return skillRepository.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
前端 Vue 代码
<template>
<div class="skill-container">
<RecycleScroller
:items="pagedSkills"
:item-size="50"
key-field="id"
class="scroller"
>
<template v-slot="{item}">
<div class="skill-item">
<h3>{{item.name}}</h3>
<p>{{item.description}}</p>
</div>
</template>
</RecycleScroller>
<div class="pagination">
<button
v-for="page in totalPages"
:key="page"
@click="loadPage(page - 1)"
>
{{page}}
</button>
</div>
</div>
</template>
<script>
export default {data() {
return {skills: [],
currentPage: 0,
pageSize: 20,
totalItems: 0
};
},
computed: {pagedSkills() {return this.skills;},
totalPages() {return Math.ceil(this.totalItems / this.pageSize);
}
},
methods: {async loadPage(page) {
this.currentPage = page;
const response = await axios.get(`/api/skills?page=${page}&size=${this.pageSize}`);
this.skills = response.data.content;
this.totalItems = response.data.totalElements;
}
},
created() {this.loadPage(0);
}
};
</script>
性能测试
优化前:
– 查询 1000 条数据:1200ms
– 内存占用:150MB
优化后:
– 查询 1000 条数据:200ms
– 内存占用:30MB
生产环境避坑指南
N+ 1 查询问题防范
-
使用 JOIN FETCH 或实体图
@EntityGraph(attributePaths = {"category"}) List<Skill> findAll(); -
使用 DTO 投影
@Query("SELECT new com.example.SkillDTO(s.id, s.name, c.name)" + "FROM Skill s JOIN s.category c") List<SkillDTO> findAllWithCategory();
缓存雪崩应对策略
-
设置不同的过期时间
@Cacheable(value = "skills", key = "#id", expireAfterWrite = 30, timeUnit = TimeUnit.MINUTES) -
使用多级缓存
-
实现缓存降级策略
前端渲染性能优化
- 使用虚拟滚动技术
- 避免在 v -for 中使用复杂计算
- 合理使用 v -if 和 v -show
总结与思考
在实际项目中,我们需要根据业务场景选择最合适的查询策略:
- 对于简单的 CRUD 操作,可以使用 ORM 提高开发效率
- 对于复杂的报表查询,建议使用原生 SQL
- 对于高频访问但不常变化的数据,使用缓存
- 大数据量展示时,前后端都需要进行分页优化
最终的技术选型应该以业务需求为导向,在开发效率和系统性能之间找到平衡点。
正文完
