每秒10万写入的订单系统:MySQL分库分表、缓冲设计、批量写入优化实战

17 阅读5分钟

每秒10万写入的订单系统:MySQL分库分表、缓冲设计、批量写入优化实战

在电商、金融等高并发场景中,订单系统每秒需处理10万级写入请求,这对MySQL的存储架构、缓存策略和写入性能提出了严苛挑战。本文结合实战经验,从分库分表、缓冲设计、批量写入优化三个维度,拆解如何构建高吞吐、低延迟的MySQL订单存储方案。

一、分库分表:拆解数据压力的基石

1. 水平分库:突破单机写入瓶颈

当单库写入QPS超过5万时,磁盘I/O、CPU资源会成为瓶颈。以用户ID(user_id)为分片键,采用哈希取模策略将数据分散到多个库中:

sql
-- 配置ShardingSphere分库规则(YAML示例)
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
    sharding:
      default-database-strategy:
        inline:
          sharding-column: user_id
          algorithm-expression: ds$->{user_id % 2}

关键设计

  • 分片键选择:优先选择查询条件中的高频字段(如user_id),避免跨库查询。
  • 扩容规划:初始设计为2的幂次方(如2/4/8库),便于后续通过“双倍扩容”平滑迁移数据。
  • 大V用户处理:为粉丝量超千万的用户分配独立分片,防止数据倾斜。

2. 水平分表:降低单表数据量

单表数据量超过5000万行时,索引效率显著下降。按时间范围(如按月)拆分订单表:

sql
CREATE TABLE orders_202604 (
    order_id BIGINT PRIMARY KEY,
    user_id BIGINT,
    amount DECIMAL(10,2),
    create_time DATETIME
) PARTITION BY RANGE (YEAR(create_time)*100 + MONTH(create_time)) (
    PARTITION p202601 VALUES LESS THAN (202602),
    PARTITION p202602 VALUES LESS THAN (202603),
    PARTITION p202603 VALUES LESS THAN (202604),
    PARTITION p202604 VALUES LESS THAN (202605)
);

优化点

  • 冷热分离:历史订单(如超过1年)迁移至HBase,成本降低90%。
  • 索引优化:对user_idcreate_time建立复合索引,加速范围查询。

二、缓冲设计:削峰填谷的关键

1. 多级缓存拦截读请求

订单查询场景中,90%的请求集中在最近3天的数据。通过“本地缓存+Redis+MySQL”三级缓存拦截请求:

  • 本地缓存(Caffeine) :缓存用户最近1小时的订单,命中率约30%,无网络开销。

  • Redis集群:缓存近7天订单,key设计为order:user_id:date_range,采用Redis Pipeline批量获取数据。

  • 缓存穿透/击穿/雪崩防护

    • 穿透:用布隆过滤器拦截不存在的user_id查询。
    • 击穿:通过Redisson分布式锁保证热点key的缓存重建原子性。
    • 雪崩:为缓存key设置随机TTL(如600±300秒),避免集中失效。

2. 异步化处理非实时写入

对非核心场景(如日志记录、统计数据)采用消息队列异步写入:

python
# RabbitMQ生产者示例
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='order_logs')

def log_order(order_data):
    channel.basic_publish(
        exchange='',
        routing_key='order_logs',
        body=json.dumps(order_data)
    )

优势

  • 削峰填谷:通过消息队列缓冲写入压力,避免MySQL瞬时过载。
  • 解耦系统:将日志处理与主订单流程分离,提升系统稳定性。

三、批量写入优化:提升吞吐的核心

1. JDBC批处理+rewriteBatchedStatements

通过JDBC批处理合并多条INSERT语句,并开启rewriteBatchedStatements参数优化SQL执行:

java
// JDBC批处理示例
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
PreparedStatement ps = conn.prepareStatement(
    "INSERT INTO orders (user_id, amount) VALUES (?, ?)"
);

for (Order order : orders) {
    ps.setLong(1, order.getUserId());
    ps.setBigDecimal(2, order.getAmount());
    ps.addBatch();
}

ps.executeBatch();  // 合并为一条SQL执行
conn.commit();

关键配置

  • rewriteBatchedStatements:必须设置为true,否则批处理无效。
  • max_allowed_packet:调整至64MB,避免大批量数据包被截断。

2. 多线程并发写入

利用多线程并行处理不同分片的写入请求:

java
ExecutorService executor = Executors.newFixedThreadPool(16);
List<Future<?>> futures = new ArrayList<>();

for (int shardId = 0; shardId < 16; shardId++) {
    futures.add(executor.submit(() -> {
        // 写入对应分片的数据
        writeToShard(shardId, orders);
    }));
}

// 等待所有任务完成
for (Future<?> future : futures) {
    future.get();
}

性能数据

  • 单线程批处理:QPS约2000。
  • 16线程并发:QPS提升至3.2万,延迟降低80%。

3. LOAD DATA INFILE加速导入

对于批量数据导入场景(如初始化订单库),使用LOAD DATA INFILE替代INSERT:

sql
LOAD DATA INFILE '/tmp/orders.csv' 
INTO TABLE orders 
FIELDS TERMINATED BY ',' 
LINES TERMINATED BY '\n'
(user_id, amount, create_time);

优势

  • 速度极快:比INSERT快5-10倍,100万行数据导入仅需3秒。
  • 低负载:绕过SQL解析层,直接写入存储引擎。

四、实战案例:某电商订单系统优化

1. 优化前痛点

  • 单库写入QPS:1.2万(已达MySQL极限)。
  • 95%延迟:2.3秒(用户下单后无法实时查看订单)。
  • 硬件成本:每月支出超10万元(SSD+高配服务器)。

2. 优化方案

  • 分库分表:按user_id哈希拆分为4库32表,单分片写入QPS降至3000。
  • 批量写入:开启rewriteBatchedStatements+16线程并发,写入吞吐提升至8万QPS。
  • 冷热分离:历史订单迁移至HBase,MySQL存储成本降低70%。

3. 优化后效果

  • 写入性能:QPS稳定在12万+,95%延迟<500ms。
  • 资源利用率:CPU使用率从90%降至40%,磁盘I/O等待时间减少85%。
  • 成本节约:硬件支出降至每月3万元,年节省超80万元。

五、总结与避坑指南

1. 核心优化策略

  • 分库分表:优先水平拆分,分片键选择需兼顾查询场景与数据均衡。
  • 缓存设计:多级缓存拦截90%以上读请求,异步化处理非实时写入。
  • 批量写入:JDBC批处理+多线程并发+LOAD DATA INFILE,写入性能提升10倍。

2. 常见坑与解决方案

  • 跨库JOIN:禁止跨库查询,通过冗余字段或ES辅助查询。
  • 分布式事务:避免强一致性,采用最终一致性+消息补偿机制。
  • 主键生成:禁用自增ID,改用雪花算法或号段模式。

通过分库分表、缓冲设计和批量写入优化的组合拳,MySQL完全可支撑每秒10万级订单写入场景。实际落地时需结合业务特点灵活调整,并通过压测验证方案有效性。