共计 2678 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点:技能大全系统的典型挑战
开发技能大全系统时,我们常遇到几个核心挑战:

- 高并发查询压力:当用户量达到 10 万 + 时,频繁的列表页加载和详情页访问会导致数据库 QPS 飙升
- 模糊匹配性能瓶颈 :技能名称的模糊搜索(如 ”Ja” 匹配 ”Java”)在 MySQL 中使用
LIKE会导致全表扫描 - 数据一致性难题:技能与标签的多对多关系维护,在分布式环境下容易产生脏读
- 扩展性限制:传统单体架构难以应对技能维度持续增加(如新增编程语言分类)
技术选型:数据库方案对比
MySQL 关系型方案
- 优点:
- 完善的 ACID 事务支持
- 成熟的索引优化方案(B+ 树)
- 缺点:
- 全文检索性能差(即使使用 FULLTEXT 索引)
- 水平扩展困难
MongoDB 文档型方案
- 优点:
- 灵活的 Schema 设计
- 内置文本搜索能力
- 缺点:
- 没有真正的模糊匹配
- 事务支持较弱
Elasticsearch 搜索引擎方案
- 核心优势:
- 倒排索引实现毫秒级搜索
- 支持同义词扩展、拼音搜索等高级特性
- 天然分布式架构
- 典型应用场景:
// 技能索引 Mapping 示例 { "properties": {"skill_name": { "type": "text", "analyzer": "ik_max_word"}, "tags": {"type": "keyword"} } }
核心实现:三明治架构设计
基础 CRUD 层(SpringBoot + MyBatisPlus)
// 符合 Alibaba 规范的技能实体类
@Data
@TableName("t_skill")
public class Skill {@TableId(type = IdType.AUTO)
private Long id;
@NotBlank(message = "技能名不能为空")
private String skillName;
// 其他字段...
}
// 使用 MyBatisPlus 实现基础服务
@Service
public class SkillServiceImpl extends ServiceImpl<SkillMapper, Skill> {
// 批量插入时启用事务
@Transactional(rollbackFor = Exception.class)
public void batchInsert(List<Skill> skills) {saveBatch(skills);
}
}
搜索增强层(Elasticsearch)
// 使用 RestHighLevelClient 构建搜索请求
SearchRequest request = new SearchRequest("skill_index");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 组合查询:匹配技能名 + 标签过滤
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
.must(QueryBuilders.matchQuery("skill_name", "Java"))
.filter(QueryBuilders.termQuery("tags", "编程语言"));
sourceBuilder.query(boolQuery);
request.source(sourceBuilder);
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
缓存加速层(Redis)
// 防止缓存击穿的方案:互斥锁
public Skill getSkillWithLock(Long id) {
String cacheKey = "skill:" + id;
// 1. 先查缓存
Skill skill = redisTemplate.opsForValue().get(cacheKey);
if (skill != null) return skill;
// 2. 获取分布式锁
String lockKey = "lock:skill:" + id;
try {boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);
if (locked) {
// 3. 查数据库
skill = getById(id);
// 4. 写缓存(设置随机过期时间防雪崩)redisTemplate.opsForValue().set(
cacheKey,
skill,
60 + (int)(Math.random() * 30),
TimeUnit.MINUTES
);
return skill;
}
// 5. 没抢到锁则短暂休眠后重试
Thread.sleep(100);
return getSkillWithLock(id);
} finally {redisTemplate.delete(lockKey);
}
}
性能优化实战
JMeter 压测对比(单节点 4 核 8G)
| 场景 | QPS | 平均响应时间 |
|---|---|---|
| 纯 MySQL 查询 | 128 | 780ms |
| MySQL+Redis 缓存 | 2100 | 45ms |
| ES 搜索 +Redis | 3500 | 28ms |
Nginx 负载均衡配置
upstream skill_server {
server 192.168.1.101:8080 weight=3;
server 192.168.1.102:8080;
keepalive 32;
}
server {
location /api/skill {
proxy_pass http://skill_server;
proxy_set_header Connection "";
}
}
避坑指南
Elasticsearch 分片策略
- 分片数计算 :建议每个分片大小在 10-50GB 之间,可用公式:
总分片数 = 数据总量 /30GB - 副本设置:生产环境至少 1 个副本,但副本数过多会影响写入性能
- 热数据分离:对高频访问的技能分类(如 ” 编程语言 ”)可以单独建立索引
缓存雪崩预防
- 差异化过期时间:基础数据缓存设置
基础过期时间 + 随机浮动值 - 多级缓存:本地缓存(Caffeine) + 分布式缓存(Redis)
- 熔断降级:当缓存不可用时启用本地静态数据
分布式事务建议
对于技能标签关联操作:
1. 优先考虑最终一致性(如通过 binlog 同步)
2. 复杂场景使用 Seata 的 AT 模式
3. 对实时性要求不高的操作可以使用本地消息表
思考题:实时推荐系统设计
现有技能大全系统需要新增 ” 根据用户浏览记录实时推荐相关技能 ” 的功能,请考虑:
1. 如何建立技能之间的关联关系?
2. 实时数据采集选用 Kafka 还是 Flink?
3. 推荐算法如何平衡实时性和准确性?
欢迎在评论区分享你的设计方案,我们将选取优秀回答进行深度解析。
正文完
发表至: 技术分享
近一天内
