以下是针对 每天1000万订单查询 的详细优化方案,涵盖架构设计、技术选型、代码实现细节及运维策略,确保高并发、低延迟和高可用性。
1. 数据库层优化
1.1 分库分表设计
- 分片策略:
- 分片键选择:以
user_id
作为分片键(用户查询最多),采用 一致性哈希 分片,避免扩容时数据迁移量过大。 - 分片规则:
-- 分16个库,每个库分16张表,总计256张表 shard_key = hash(user_id) % 16 -- 确定库 table_suffix = hash(user_id) % 16 -- 确定表
- 全局表:小表(如订单状态字典表)全库冗余,避免跨库JOIN。
- 分片键选择:以
- 分页查询优化:
- 禁止跨分片排序:分页需携带
user_id
,确保查询落在同一分片。 - 二次查询法:若必须跨分片,汇总各分片结果后内存排序(牺牲性能换功能)。
- 禁止跨分片排序:分页需携带
1.2 索引与SQL优化
- 索引设计:
-- 高频查询场景索引 CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time DESC); CREATE INDEX idx_shop_time ON orders(shop_id, create_time DESC);
- SQL优化:
- **避免SELECT ***:只查询必要字段,减少网络传输和磁盘IO。
- 强制索引:对复杂查询强制使用索引(需谨慎,可能影响优化器判断)。
SELECT * FROM orders FORCE INDEX(idx_user_status_time) WHERE user_id=123;
- 慢查询监控:
- 开启MySQL慢查询日志(
long_query_time=0.1s
),用 pt-query-digest 分析TOP 10慢SQL。
- 开启MySQL慢查询日志(
1.3 读写分离与连接池
- 读写分离:
- ProxySQL配置:
-- 定义主库和从库组 INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (10, 'master', 3306); INSERT INTO mysql_servers(hostgroup_id, hostname, port) VALUES (20, 'slave1', 3306), (20, 'slave2', 3306); -- 路由规则:写操作走主库,读操作走从库 INSERT INTO mysql_query_rules(active, match_pattern, destination_hostgroup) VALUES (1, '^SELECT', 20), (1, '.*', 10);
- 强制读主库:对实时性要求高的读操作(如支付状态),在SQL注释中标记路由:
SELECT /* FORCE_MASTER */ * FROM orders WHERE order_id=123;
- ProxySQL配置:
- 连接池配置:
- HikariCP参数(Java):
spring.datasource.hikari: maximumPoolSize: 50 # 根据压测调整 minimumIdle: 10 connectionTimeout: 3000 idleTimeout: 600000
- HikariCP参数(Java):
2. 缓存层设计
2.1 多级缓存架构
- 本地缓存(Caffeine):
Cache<String, Order> localCache = Caffeine.newBuilder() .maximumSize(10_000) // 缓存1万条订单 .expireAfterWrite(5, TimeUnit.MINUTES) // 5分钟过期 .build(); // 读流程:先查本地缓存 → 未命中查Redis → 再未命中查DB Order order = localCache.getIfPresent(orderId); if (order == null) { order = redis.get(orderId); if (order == null) { order = db.query(orderId); redis.setex(orderId, 300, order); // 缓存5分钟 } localCache.put(orderId, order); }
- Redis集群:
- 数据分片:采用Redis Cluster,16384个槽位,每个节点负责部分槽。
- 热点Key处理:
- 本地缓存:对极热点订单(如明星商家),在应用层做本地缓存。
- 分片打散:对同一个订单ID,增加随机后缀(如
order:123_${0~9}
),分散到不同节点。
2.2 缓存一致性方案
- 写操作流程:
- 更新数据库订单状态。
- 删除Redis缓存(非更新,避免并发写导致脏数据)。
- 通过 Canal 监听MySQL Binlog,异步再次删除缓存,确保最终一致。
- 防击穿与雪崩:
- 互斥锁:
String lockKey = "lock:order:" + orderId; if (redis.setnx(lockKey, "1", 3, TimeUnit.SECONDS)) { try { Order order = db.query(orderId); redis.setex(orderId, 300, order); } finally { redis.delete(lockKey); } } else { Thread.sleep(100); // 等待其他线程加载 return redis.get(orderId); }
- 随机过期时间:在基础TTL上增加随机值(如
300 + random(0, 60)
秒)。
- 互斥锁:
3. 查询服务优化
3.1 服务拆分与API设计
- 独立订单服务:
- 接口定义:
@GetMapping("/orders/{id}") public Order getOrder(@PathVariable String id) { ... } @PostMapping("/orders/batch") public List<Order> getOrdersBatch(@RequestBody List<String> ids) { ... }
- 批量查询实现:
List<Order> orders = ids.stream() .parallel() // 并行查询(注意线程池隔离) .map(id -> orderService.getOrder(id)) .collect(Collectors.toList());
- 接口定义:
3.2 异步化与响应压缩
-
异步查询(CompletableFuture):
public CompletableFuture<Order> asyncGetOrder(String orderId) { return CompletableFuture.supplyAsync(() -> { return orderService.getOrder(orderId); }, asyncExecutor); // 指定专用线程池 }
-
响应压缩:
- GZIP压缩:在API网关或Nginx中开启GZIP,减少传输体积。
gzip on; gzip_types application/json;
- GZIP压缩:在API网关或Nginx中开启GZIP,减少传输体积。
4. 搜索引擎优化(Elasticsearch)
4.1 索引设计
- Mapping定义:
{ "mappings": { "properties": { "order_id": { "type": "keyword" }, "user_id": { "type": "keyword" }, "shop_id": { "type": "keyword" }, "status": { "type": "keyword" }, "create_time": { "type": "date" }, "geo_location": { "type": "geo_point" } // 骑手位置 } } }
- 数据同步:
- 方案:通过 Canal 监听MySQL Binlog → 写入Kafka → Logstash消费到ES。
- 延迟处理:用户查询时,若ES数据延迟,可降级查数据库。
4.2 查询优化
- 分页深度限制:禁止
from + size
超过1000页,改用 search_after:{ "size": 10, "query": { ... }, "sort": [{"create_time": "desc"}, {"_id": "asc"}], "search_after": ["2023-07-01T12:00:00", "123456"] }
5. 流量治理与容灾
5.1 限流与熔断
- Sentinel规则:
// 定义资源名 @SentinelResource(value = "getOrder", blockHandler = "handleBlock") public Order getOrder(String id) { ... } // 配置规则:QPS限流1000 FlowRule rule = new FlowRule(); rule.setResource("getOrder"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(1000); FlowRuleManager.loadRules(Collections.singletonList(rule));
5.2 多活与容灾
- 跨机房部署:
- 数据库:使用MySQL MGR(多主模式),机房内延迟<1ms,跨机房<50ms。
- 缓存:Redis Cluster跨机房部署,开启
cluster-allow-cross-region-migrations
。
6. 监控与告警
6.1 指标监控
- Prometheus指标:
order_query_duration_seconds
:查询耗时分布。redis_hit_rate
:缓存命中率(keyspace_hits / (keyspace_hits + keyspace_misses)
)。
- Grafana看板:
- 实时监控QPS、延迟、缓存命中率、数据库连接池使用率。
6.2 日志与追踪
- ELK日志:订单服务日志接入Elasticsearch,按
order_id
快速检索。 - SkyWalking追踪:分析查询链路,定位慢调用(如跨分片查询)。
7. 压测与调优
- 压测工具:使用 JMeter 模拟峰值流量(如5000 QPS)。
- 调优目标:
- 平均响应时间 < 50ms(P99 < 200ms)。
- 数据库CPU利用率 < 60%。
- Redis内存占用 < 70%。
最终效果
场景 | 优化前 | 优化后 |
---|---|---|
单订单查询(缓存命中) | 200ms | 5ms |
批量查询(100订单) | 10s | 300ms |
历史订单搜索(ES) | 15s | 800ms |
数据库峰值QPS | 5000 | 500 |
通过以上方案,系统可稳定支撑 日均1000万订单查询(峰值QPS 2000+),并具备横向扩展能力。