100万的并发,如何设计一个商品搜索系统?
今天我们来看一道比较有深度的面试题百万并发下商品搜索系统你如何设计呢假设场景某电商平台大促期间需支撑每秒100万次的商品搜索请求要求响应时间≤200ms同时应对商品数据量超10亿条。假设给你来做系统设计怎么做呢如果是我来回答面试官这道题的话我会按照这些思路来跟面试官阐述为什么不能用MySQL的llike总体架构设计核心关键设计1.为什么不能用mysql的like我们每次提到关键词搜索大家很容易就想到数据库的like比如SELECT * FROM products WHERE title LIKE %智能手机% LIMIT 10;但是显然商品数据量超10亿条搜索不能用like。LIKE %keyword%无法利用索引触发全表扫描。10亿数据时单次查询可能耗时数秒。不支持分词搜索如手机壳无法拆分为手机和壳单独匹配分库分表后跨库LIKE查询复杂度指数级上升可以使用Elasticsearch 但是我们是做系统设计肯定不能直接回答面试官说用Elasticsearch呀而是按照系统设计的思想高可用 可扩展说一整个链路。2. 总体架构设计用户层前端 CDN接入层Nginx 服务层Search Gateway 检索层搜索引擎如 Elasticsearch数据层商品数据服务 / DB / 缓存2.1 用户层前端 CDN请求分发通过CDN加速静态资源图片/JS/CSS浏览器缓存利用LocalStorage缓存高频搜索关键词请求合并合并相似搜索请求如防抖机制// 前端防抖示例减少无效请求 let searchTimer; function handleSearch(keyword) { clearTimeout(searchTimer); searchTimer setTimeout(() { fetch(/api/search?q${encodeURIComponent(keyword)}); }, 300); // 300ms防抖 }2.2 接入层Nginx流量管控限流、熔断、鉴权负载均衡轮询/一致性哈希分发请求协议转换HTTP/2 → HTTP/1.1内部通信Nginx 关键配置# 限流配置每秒1000请求/ip limit_req_zone $binary_remote_addr zonesearch_limit:10m rate1000r/s; location /api/search { limit_req zonesearch_limit burst200; proxy_pass http://search_cluster; # 缓存热门请求结果5秒 proxy_cache search_cache; proxy_cache_valid 200 5s; }2.3 服务层Search Gateway业务逻辑请求参数校验、结果格式化多级缓存本地缓存 → Redis → Elasticsearch降级策略超时返回兜底数据// 搜索网关伪代码 public class SearchGateway { Cacheable(value localCache, key #keyword) public ListProduct search(String keyword) { // 1. 检查Redis缓存 String redisKey search: keyword.hashCode(); ListProduct cached redis.get(redisKey); if (cached ! null) return cached; // 2. 查询Elasticsearch ListProduct result elasticsearch.search(buildQuery(keyword)); // 3. 异步写入缓存 executor.submit(() - { redis.setex(redisKey, 30, result); // 缓存30秒 }); return result; } }2.4 检索层Elasticsearch索引构建商品标题/类目/属性倒排索引分布式查询分片并行计算相关性排序BM25算法优化索引设计demoPUT /products { settings: { number_of_shards: 40, number_of_replicas: 1, refresh_interval: 30s // 降低写入实时性要求 }, mappings: { properties: { title: { type: text, analyzer: ik_max_word }, price: { type: double }, sales: { type: integer } } } }2.5 数据层DB 缓存持久化存储MySQL分库分表数据同步Binlog → Canal → Elasticsearch冷热分离Redis缓存热数据HBase存历史数据分库分表demo-- 按商品ID分64个库每个库分256表 CREATE TABLE products_%02d.t_product_%03d ( id BIGINT PRIMARY KEY, title VARCHAR(255), price DECIMAL(10,2) ) ENGINEInnoDB; -- 分片路由算法hash(product_id) % 64 → 分库 -- hash(product_id) / 64 % 256 → 分表3. 核心关键设计3.1 分片与容量设计水平扩展使用 Elasticsearch每个索引进行合理分片Sharding每个分片大小控制在 20-50 GB避免 OOM 和延迟增加按业务维度如商品类目、国家分索引或路由分片分片副本Replica数设置提升可用性3.2 深度分页性能优化我们在使用mysql做查询的时候会遇到深分页的问题比如回表十万次我们可以用标签记录法来解决深分页问题。其实Elasticsearch 也存在深分页的问题当用户翻页到几百页时ES 会做全量扫描性能陡降。避免传统 from size 深分页Search After推荐基于上一页最后一个 sort_value 做游标分页。类似标签记录法思想Scroll API适用于数据导出不推荐用户查询或业务限制分页范围如最多展示 100 页3.3 避免缓存穿透设计跟大家一起复习一下什么是缓存穿透指查询一个一定不存在的数据由于缓存是不命中时需要从数据库查询查不到数据则不写入缓存这将导致这个不存在的数据每次请求都要到数据库去查询进而给数据库带来压力。在百万并发商品搜索系统时我们要避免这个问题可以用布隆过滤器简单流程图如下核心业务逻辑代码简单demoService public class SearchService { // 布隆过滤器存储所有有效关键词 Autowired private BloomFilterString searchBloomFilter; // Redis缓存操作 Autowired private RedisTemplateString, Object redisTemplate; public SearchResult search(String keyword) { // Step 1: 布隆过滤器校验 if (!searchBloomFilter.mightContain(normalizeKeyword(keyword))) { return SearchResult.EMPTY; } // Step 2: 查询缓存 String cacheKey search: keyword.hashCode(); SearchResult cached (SearchResult) redisTemplate.opsForValue().get(cacheKey); if (cached ! null) return cached; // Step 3: 查询Elasticsearch SearchResult result elasticsearchClient.search(buildQuery(keyword)); // Step 4: 更新缓存 if (result.isEmpty()) { redisTemplate.opsForValue().set(cacheKey, SearchResult.EMPTY, 30, TimeUnit.SECONDS); } else { redisTemplate.opsForValue().set(cacheKey, result, 5, TimeUnit.MINUTES); } return result; } // 关键词标准化处理如去空格、转小写 private String normalizeKeyword(String keyword) { return keyword.trim().toLowerCase(); } }这里有个点可能要注意一下哈布隆过滤器需要初始化一下// 初始化所有有效关键词到布隆过滤器 PostConstruct public void initBloomFilter() { ListString allKeywords productDao.getAllSearchKeywords(); // 获取所有商品标题/标签 allKeywords.stream() .map(this::normalizeKeyword) .forEach(searchBloomFilter::put); } // 动态更新新增商品时 public void addProduct(Product product) { // ... 其他业务逻辑 searchBloomFilter.put(normalizeKeyword(product.getTitle())); product.getTags().forEach(tag - searchBloomFilter.put(normalizeKeyword(tag)) ); }3.4 GC调优既然是百万并发的系统设计少不了GC调优。尽量降低 Full GC 频率使用 G1 或 ZGC调大堆内存视机器资源避免频繁创建临时对象使用对象池如 Query 对象在上线前我们要进行压力测试然后调出最优最优JVM参数。JVM 最优参数配置不是一成不变的根据实际压测得到的。压力测试可以用loadrunner或者jemeter进行高并发模拟测试。JVM 参数配置demo# elasticsearch/jvm.options # 基础配置 -Xms31g -Xmx31g -XX:UseG1GC -XX:MaxGCPauseMillis200 # G1调优参数 -XX:InitiatingHeapOccupancyPercent35 -XX:G1ReservePercent25 -XX:G1HeapRegionSize4m # 内存锁防止Swap -XX:AlwaysPreTouch -XX:DisableExplicitGC3.5 灾备与高可用设计高并发系统少不了容灾和高可用的设计要点可以采取以下几种方式多 AZ 部署分布在不同机房 / 可用区Elasticsearch 设置跨机房副本replica shard allocation awareness服务注册中心如 Nacos与服务熔断、降级策略配合主从切换、故障自动转移Failover