经典面试之 每天有1000万笔订单查询怎么优化

以下是针对 每天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。
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;
      
  • 连接池配置
    • HikariCP参数(Java):
      spring.datasource.hikari:
        maximumPoolSize: 50   # 根据压测调整
        minimumIdle: 10
        connectionTimeout: 3000
        idleTimeout: 600000
      

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 缓存一致性方案
  • 写操作流程
    1. 更新数据库订单状态。
    2. 删除Redis缓存(非更新,避免并发写导致脏数据)。
    3. 通过 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;
      

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%。

最终效果

场景优化前优化后
单订单查询(缓存命中)200ms5ms
批量查询(100订单)10s300ms
历史订单搜索(ES)15s800ms
数据库峰值QPS5000500

通过以上方案,系统可稳定支撑 日均1000万订单查询(峰值QPS 2000+),并具备横向扩展能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值