
1. 问题背景并发写入时锁竞争到底卡在哪先说一个我在实际项目中遇到的场景一个促销活动系统用户疯狂点按钮领券后端接口除了要更新用户账户表还要往发券流水表里插记录。业务量一上来数据库的CPU不高、磁盘IO也不高但接口耗时蹭蹭往上涨应用日志里全是Lock wait timeout exceeded。当时查show engine innodb status一堆事务卡在等一把行锁。这个问题的本质是并发写入时InnoDB的行锁竞争。你可能会想行锁不是粒度最小了吗怎么还会竞争这么严重关键在于写入要走的链路不只是“加锁”那么一步。INSERT操作要先在索引上定位位置如果表上有唯一索引或主键插入前要先做一次“重复键检查”这个检查就要加锁在RR隔离级别下间隙锁和next-key lock会让锁的范围扩大再加上并发事务都要抢AUTO-INC锁来分配自增ID一旦innodb_autoinc_lock_mode配置不合理自增ID分配就是一个全局串行瓶颈。最经典的锁竞争场景是这三类秒杀、抢购这类热点行更新。比如库存表里同一个SKU只有一行所有请求都要改这一行行锁排队是必然的。大量INSERT往同一张表灌数据且表上有自增主键每次插入都要争抢AUTO-INC锁旧模式下。先查后写的“读改写”逻辑比如select balance from account where id ?;再update ... set balance new_value两个事务交错执行极容易死锁或互相等待。很多人遇到锁等待的第一反应是“优化SQL”“加索引”但并发写入这类场景根因往往不在单条SQL而在“写入模式”本身。如果每秒钟有成百上千次单独的写入请求直接打到数据库不管你SQL写得多漂亮锁竞争都不可能消除。这个就是本文要解决的核心问题。解决思路的钥匙藏在两个词里队列缓冲和批量插入。不直接让高并发的写请求一股脑打进MySQL而是先在中间层把请求“攒起来”按批次合并后一次性写入数据库。听起来简单但里面有很多细节值得较真下面从设计到落地一条条展开。2. 核心设计思路为什么是“队列缓冲 批量插入”2.1 队列缓冲解决的是“压力整形”问题先说队列缓冲。它的本质作用不是减少写入量而是把“瞬间高峰”变成“均匀平峰”。就像早高峰地铁进站如果不限流所有人都堵在闸机口谁都过不去站外排队分批放行虽然每个人等待时间变长了但整体通过效率反而更高。在MySQL写入场景里请求的高峰往往是突发性的——活动开始那一秒、整点放货那一刻、运营批量推送用户那一刻。如果这时候几千个请求同时往库里插数据数据库要同时维护几千个事务、几千把锁系统立刻进入“拥堵瘫痪”状态。但如果你把这些请求先丢进队列写进程按自己节奏一批批取数据、一批批入库数据库在任何时刻只需要处理少量几个事务锁竞争自然降下来了。队列缓冲还有一种“天然降级”的能力。当数据库出现抖动或者某条SQL执行计划走偏导致单条写入变慢时消息队列可以作为缓冲层把请求暂存等数据库恢复后再继续消费避免了“数据库一卡上游接口全部超时”的雪崩效应。2.2 批量插入解决的是“事务数量”问题批量插入的核心价值是减少事务数量。你要理解一个关键事实MySQL的锁竞争和事务数量呈正相关。每个事务都要申请锁、要记录undo日志、要刷redo日志、提交时要fsync一批数据如果分成1000个事务写入它就要重复1000次这些代价合并成10个事务代价直接少两个数量级。举一个实际的对比。某次压测中我用JMeter模拟每秒500次插入请求单条提交模式TPS最终只能稳定在200左右CPU还没跑满但数据库的锁等待时间已经很高了show global status like Innodb_row_lock_waits的数字每秒钟都在涨。改成队列批量提交每次攒够100条再写TPS稳定到了450以上锁等待几乎消失数据库CPU占用反而降了因为事务切换和锁管理的开销减少了。这个对比很直观批量插入不是靠“压榨数据库性能”来提速而是靠“减少MySQL必须干的重复活”来提速。2.3 两个方案必须组合使用单独用队列不批量数据库的压力只是延后了高峰期还是会瞬间被打满单独用批量但不做队列缓冲应用层需要自己做聚合逻辑而且遇到流量突发时批量策略容易僵化——比如攒数据的时间窗口太长导致数据延迟过高。组合起来的效果是队列负责“削峰填谷”批量负责“降低单次代价”两者配合才能既保证写入吞吐又不让锁竞争失控。3. 队列缓冲层的搭建与关键参数选择3.1 队列选型从内存队列到分布式MQ队列的选型直接决定了整个方案的复杂度和上限。这里按规模从小到大排序内存队列ArrayBlockingQueue适用于单机应用、并发量在每秒几百到一千级别、数据允许少量丢失的场景。比如一个后台管理系统内部的操作日志异步写入用内存队列就够了。优点是零额外依赖、延迟极低缺点是进程重启数据就丢也不支持分布式部署。RedisListBRPOP适用于小规模分布式系统作为轻量级缓冲。Redis的RPUSH写入和BRPOP消费都非常快配合持久化配置能做到“基本不丢”但毕竟Redis不是专业的消息队列在消息堆积、消费失败重试、顺序保证等方面都比较弱。RabbitMQ/RocketMQ/Kafka适用于中大规模生产系统。它们有完善的消息确认、重试、堆积、顺序等机制。如果项目本身已经用了消息中间件优先复用即可没必要为了一个写缓冲功能单独引入新组件。我的建议是如果你的系统还在早期阶段不要为了“防锁竞争”直接上消息队列。先用内存队列配合定时刷盘等真出现数据丢失或扩容需求时再升级到MQ。技术上做简单运维上做减法这个原则在写库场景特别重要。3.2 消费时机定时批量还是定量批量队列缓冲层的核心逻辑就是“攒批次”。攒批次的策略有两种定量触发队列里的数据达到N条就批量写入。比如每攒够100条就执行一次批量插入。好处是延迟稳定写入条数和批次大小可控。定时触发每隔N毫秒/秒把队列里当前积累的数据批量写入一次。好处是即使流量很低数据也能被定时刷走不会一直堆积在内存里。实际项目中建议两个条件都用“或”逻辑触发代码类似public class BatchTask implements Runnable { private static final int BATCH_SIZE 200; private static final long MAX_WAIT_MS 1000; private final BlockingQueueOrderInsertEntity queue new LinkedBlockingQueue(); private final ListOrderInsertEntity buffer new ArrayList(); Override public void run() { long lastFlushTime System.currentTimeMillis(); while (true) { try { // 阻塞取一条只要有数据就进入批量逻辑 OrderInsertEntity entity queue.poll(100, TimeUnit.MILLISECONDS); if (entity ! null) { buffer.add(entity); } boolean sizeReached buffer.size() BATCH_SIZE; boolean timeReached System.currentTimeMillis() - lastFlushTime MAX_WAIT_MS; if ((sizeReached || timeReached) !buffer.isEmpty()) { batchInsert(buffer); buffer.clear(); lastFlushTime System.currentTimeMillis(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } }这里有两个关键参数要单独拿出来说BATCH_SIZE不宜设置过大。很多人以为越大越好实际上批量插入的SQL是有限制的max_allowed_packet默认值只有64MB单条SQL超过这个大小直接报错。而且批次太大单个事务的执行时间会很长反而增加了锁的持有时间。根据我的经验普通行宽在几百字节到几KB时单批500-2000条是性能和锁等待时间的均衡区间。MAX_WAIT_MS不宜设置过小。如果设置成100毫秒基本等于每条数据都单独插入设置成5秒低峰期数据延迟又会比较大。建议设置成500ms-2000ms这样在低流量下数据最多延迟2秒入库。3.3 缓冲层的“降级开关”必须提前做队列缓冲方案有一个容易被忽视的风险如果数据库慢到连批量插入都扛不住了队列里的数据会越积越多内存最终被撑爆。所以线上一定要有降级开关。我当时做的方案是监控队列积压量超过阈值比如1万条就触发降级策略——不再攒批立刻把当前积累的数据全部写入如果积压量继续翻倍则直接淘汰非核心数据比如只保留最新的5000条丢弃最老的部分同时报警给值班人员。很多时候我们只关注正常流程的性能忽略了异常路径的兜底设计。其实一个系统能不能稳定跑下去靠的恰恰是这些“不正常”时的表现。4. 批量插入的SQL写法与参数调优4.1 批量插入的SQL怎么写才高效批量插入的SQL写法有几种不同写法在MySQL里的执行路径完全不同。第一种也是最常见的多值INSERT INTOINSERT INTO order_log (user_id, order_id, amount, status, create_time) VALUES (101, 10001, 10.50, 1, NOW()), (102, 10002, 20.00, 1, NOW()), (103, 10003, 30.00, 1, NOW());这种写法MySQL会把它当成单条SQL执行遍历所有值逐行插入。和单条插入相比它省去了多次网络往返、多次SQL解析、多次事务提交的代价。这也是我们最常用的批量写入方式。需要注意INSERT ... ON DUPLICATE KEY UPDATE也可以多值批量执行适合“存在则更新不存在则插入”的场景。但你要知道它的代价——如果表上有多个唯一索引重复键的冲突判断会变得更复杂额外开销不可忽视。能用普通批量插入的场景就别套这个语法。第二种是事务包裹循环插入Connection conn dataSource.getConnection(); try { conn.setAutoCommit(false); for (OrderInsertEntity e : batchList) { PreparedStatement ps conn.prepareStatement(INSERT INTO order_log ...); // set参数 ps.executeUpdate(); ps.close(); } conn.commit(); } catch (Exception e) { conn.rollback(); }这种方式的优势是灵活每条数据可以有不同的写入逻辑缺点是Java代码里反复prepareStatement、executeUpdate的开销也不小。实测发现同样数据量下它的性能只有多值INSERT的1/3到1/2。第三种是使用JDBC的批量特性addBatch()/executeBatch()PreparedStatement ps conn.prepareStatement(INSERT INTO order_log (user_id, order_id, amount, status, create_time) VALUES (?, ?, ?, ?, ?)); for (OrderInsertEntity e : batchList) { ps.setLong(1, e.getUserId()); ps.setLong(2, e.getOrderId()); ps.setBigDecimal(3, e.getAmount()); ps.setInt(4, e.getStatus()); ps.setTimestamp(5, e.getCreateTime()); ps.addBatch(); } ps.executeBatch(); conn.commit();这种方式的关键在于JDBC驱动如何处理addBatch。MySQL JDBC驱动默认情况下executeBatch()内部还是逐条执行并没有真正合并成多值SQL。必须开启一个参数才有真正意义上的批量合并等下单独说。4.2rewriteBatchedStatements最容易忽略的关键参数这是批量插入优化里最坑、也最关键的一个参数。MySQL的JDBC驱动mysql-connector-java/mysql-connector-j在默认配置下PreparedStatement的addBatch只是把参数缓存在内存里调用executeBatch()时仍然一条一条发给服务器。这意味着你用addBatch写的代码如果没有在连接串上追加rewriteBatchedStatementstrue性能提升并不会出现。加了rewriteBatchedStatementstrue之后驱动会在客户端把一批INSERT重写为一条多值INSERT语句再发送给MySQL效果和手写多值INSERT一样。这个参数的收益非常夸张在某些测试场景下性能差距超过10倍。连接串示例jdbc:mysql://127.0.0.1:3306/test_db?useSSLfalseserverTimezoneAsia/ShanghairewriteBatchedStatementstrueuseConfigsmaxPerformance基于常见实践的经验值有几个配套参数建议同时打开useConfigsmaxPerformance内部会附带打开一批性能优化参数包括结果集处理优化等。cachePrepStmtstrueprepStmtCacheSize500缓存PreparedStatement避免相同SQL反复解析。这个在批量场景收益也很大。useServerPrepStmtstrue让服务端也预处理SQL。注意这个参数要和cachePrepStmts配合否则性能未必提升。4.3 事务边界与commit频率控制批量插入要做事务但事务不能太大也不能太小。事务太大比如一次性插10万条单事务执行时间可能超过几秒期间持有大量行锁和undo日志不仅锁竞争加剧回滚段的膨胀也会拖慢数据库。事务太小比如每100条一个事务失去了批量插入的意义。经验上单事务控制在1000-5000条之间比较合理。还是那个原则批量大小要平衡“事务次数”和“单事务时长”两个指标。如果单条记录字段很多、单行很大适当减小批次如果单行只有几个字段、几十字节批次可以放大。事务提交时还有一个隐藏细节commit操作本身要和业务代码解耦。在批量写入任务里不要每批都写一个commit然后flush一次。MySQL在autocommit1默认下每次SQL执行结束自动提交批量插入时务必显式开启事务比如用set autocommit0或begin批量执行成功后统一commit。4.4 InnoDB关键参数刷盘策略和日志缓冲批量插入对性能影响最大的InnoDB参数是innodb_flush_log_at_trx_commit和innodb_log_buffer_size。innodb_flush_log_at_trx_commit有三个取值取值行为权衡0每秒刷一次redo log性能最好但崩溃时可能丢最近1秒数据1每次事务提交都刷盘最安全性能最差2每次提交只写到OS cache每秒刷盘折中方案崩溃不丢已提交事务但OS宕机可能丢1秒如果业务允许批量写入建议设置成2性能提升非常明显。我当时在写肩流水时改成2之后批量插入整体耗时下降了约40%。如果业务对数据安全要求极高不能丢失任何一条已确认的数据那就保持在1但这时批量大小要相应调小否则每批提交的等待时间会放大。innodb_log_buffer_size默认是16MB。大批量插入时redo log写入会很频繁如果这个缓冲太小每一批事务提交前都要把日志从缓冲刷到磁盘IO等待时间变长。建议直接调到64MB-256MB对批量写场景有明显改善。4.5 关掉“非必要”的实时一致性检查批量插入时MySQL会维护二级索引。如果表上有好几个二级索引每插一条记录都要同步更新这些索引的B树写入放大非常严重。如果业务上对查询实时性要求不高比如日志流水表、操作记录表可以降低索引数量。每多一个二级索引插入开销至少增加20%-50%。索引不是越多越好写多读少的表尤其要控制索引数量。另一个值得注意的点是外键。不要在前台高频写入的表上设置外键约束外键检查会让每一条INSERT都触发关联表的锁查询批量插入场景下简直是灾难。我在项目里看到过一张流水表加了外键批量插入性能怎么调都上不去去掉外键后用APPLICATION层逻辑去保证关联完整性性能立刻恢复正常。5. 实战全过程从单条插入到队列批量插入的改造示例5.1 改造前的数据结构与写入代码假设我们有一张订单日志表CREATE TABLE order_log ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id varchar(32) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL COMMENT 用户ID, amount decimal(10,2) NOT NULL COMMENT 金额, status tinyint(4) NOT NULL COMMENT 0-创建 1-支付 2-取消, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_order_id (order_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;改造前业务代码在每个业务动作里直接插入单条public void addOrderLog(OrderLog orderLog) { String sql INSERT INTO order_log (order_id, user_id, amount, status, create_time) VALUES (?, ?, ?, ?, ?); try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, orderLog.getOrderId()); ps.setLong(2, orderLog.getUserId()); ps.setBigDecimal(3, orderLog.getAmount()); ps.setInt(4, orderLog.getStatus()); ps.setTimestamp(5, orderLog.getCreateTime()); ps.executeUpdate(); } }高峰期压测时这条逻辑每分钟插入几万条流水数据库锁等待报表直接飘红。单条SQL本身没什么问题但几百个同时执行就是问题。5.2 改造后的完整链路改造后的链路是业务接口→内存阻塞队列→批量任务线程→批量INSERT→MySQL。第一步定义一个线程安全的队列和批量任务执行器public class OrderLogBuffer { private static final int BATCH_SIZE 500; private static final long FLUSH_INTERVAL_MS 1000; private final BlockingQueueOrderLog queue new LinkedBlockingQueue(10000); public void addLog(OrderLog log) { if (!queue.offer(log)) { // 队列满时降级直接落库保证不丢数据 insertSingle(log); } } // 启动一个后台线程定时把队列里的数据批量写入 public void start() { ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleWithFixedDelay(new BatchFlushTask(), 0, 100, TimeUnit.MILLISECONDS); } private class BatchFlushTask implements Runnable { Override public void run() { ListOrderLog batch new ArrayList(BATCH_SIZE); long start System.currentTimeMillis(); while (batch.size() BATCH_SIZE) { OrderLog log queue.poll(); if (log null) break; batch.add(log); } if (batch.isEmpty()) return; long elapsed System.currentTimeMillis() - start; if (batch.size() BATCH_SIZE elapsed FLUSH_INTERVAL_MS) { // 攒的不够多但等待时间到了也刷一批 // 实际这里用poll100ms延迟来判断更精确简化用固定间隔任务 } batchInsert(batch); } } }第二步批量插入方法里使用addBatch和rewriteBatchedStatementsprivate void batchInsert(ListOrderLog batch) { String sql INSERT INTO order_log (order_id, user_id, amount, status, create_time) VALUES (?, ?, ?, ?, ?); try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (OrderLog log : batch) { ps.setString(1, log.getOrderId()); ps.setLong(2, log.getUserId()); ps.setBigDecimal(3, log.getAmount()); ps.setInt(4, log.getStatus()); ps.setTimestamp(5, log.getCreateTime()); ps.addBatch(); } ps.executeBatch(); } conn.commit(); } catch (SQLException e) { // 回滚并记录错误这部分日志要单独处理不能跟着丢 log.error(batch insert fail, size{}, batch.size(), e); } }这里有一点要注意executeBatch()返回的int[]表示每一条的执行影响行数不用逐条检查只要没抛异常默认全部成功。第三步修改连接池配置。这里用的是HikariCPHikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://127.0.0.1:3306/test_db?rewriteBatchedStatementstruecachePrepStmtstrueuseServerPrepStmtstrueuseConfigsmaxPerformance); config.setMaximumPoolSize(20); config.setMinimumIdle(5);改造后压测效果很明显单条插入模式1000条数据500并发耗时约8秒期间大量锁等待。批量插入模式1000条数据每批500条共2批耗时不到200毫秒锁等待完全消失。你可能会问1000条数据才2个事务那和1000个事务的对比当然差异巨大。对这正是批量插入最朴素的原理——减少事务数量就减少了锁竞争的概率。5.3 数据延迟和最终一致性说明队列缓冲方案牺牲的是“写入延迟”和“实时一致性”换来的是“吞吐稳定性”。上游接口调用写入方法后数据不会立刻出现在数据库里可能延迟几百毫秒到几秒。这对日志、流水、统计类数据可以接受但对那些写入后必须立刻读取的数据比如账户余额变动这个方案就不适合了。判断依据很简单写入后同一个用户需要立刻看到最新值吗如果是就不要走缓冲队列如果只是后台查询、离线统计、审计追踪用那就很适合。6. 常见问题与排查技巧实录6.1 锁等待超时Lock wait timeout exceeded现象日志里高频出现Lock wait timeout exceeded; try restarting transaction。排查步骤先看show variables like innodb_lock_wait_timeout确认超时阈值默认50秒可以根据业务适当调小到5-10秒避免接口长时间挂起。再执行select * from information_schema.innodb_trx\G看到底哪些事务在跑重点看trx_started时间和trx_state。长时间RUNNING状态的事务通常是问题源。继续查sys.innodb_lock_waits视图看看谁在等谁。很多时候锁超时不是批量插入引起的而是有一个慢事务占住了一批行锁不放。批量插入改造前先检查项目里有没有SELECT ... FOR UPDATE后逻辑处理太慢的代码。6.2 批量插入反而更慢怎么回事有一种反直觉的场景批量改完后性能没提升反而下降了。排查顺序检查连接串有没有加rewriteBatchedStatementstrue这是最高频的问题。没加的话addBatch等于白写。检查批量大小。如果每批几十万条单条SQL的解析和锁定范围都变大可能会锁表在MyISAM下或者长时间持锁。检查表上的索引数量。二级索引太多的表批量插入的索引维护开销是线性增长的。检查max_allowed_packet单条批量SQL超过限制会直接报错或者被拆分成多次执行。6.3 队列数据堆积现象接口响应快但数据库写入远跟不上队列越积越长。可能原因批量写入时因为死锁回滚消费者线程不断重试卡在循环里。单批次数据太大SQL执行慢消费速率下降。数据库连接池太小批量任务拿不到连接。解决办法给批量任务单独配置一个连接池不要和业务接口共用同一个连接池否则高峰期业务把连接都占了批量任务饿死。消费线程做“退避重试”失败后先等几秒再继续不要疯狂重试。队列设上限满了直接走单条降级防止内存溢出。6.4 自增主键带来的AUTO-INC锁竞争如果表用的是AUTO_INCREMENT主键MySQL在生成自增值时要拿AUTO-INC锁。在innodb_autoinc_lock_mode0传统模式时所有插入都要锁到SQL结束并发插入时这个锁竞争非常严重。建议把innodb_autoinc_lock_mode设置为2交叉模式这样自增值分配过程中不需要持有锁到语句结束多个INSERT可以交叉获取自增值并发插入性能显著提升。需要特别注意在binlog_formatSTATEMENT时innodb_autoinc_lock_mode2可能导致主从自增值不一致。所以请确认你的复制模式。当前MySQL默认的复制方式基本都是ROW格式在ROW格式下设为2非常安全。6.5 死锁不可避免时怎么办批量插入内部多条记录之间本身没有依赖顺序按理说不会死锁。但如果多个批量任务并发执行批与批之间的数据存在跨行更新就可能出现死锁。比如两个批次的任务第一批先插用户A再插用户B第二批先插用户B再插用户A就可能互相等锁。规避方法很简单批量任务统一用一个消费者线程串行执行从根源上消除并发锁冲突。如果必须多线程消费所有线程都按user_id哈希后分片处理保证同一个用户的数据只落在同一个线程里。死锁发生时MySQL会自动回滚其中一个事务应用捕获Deadlock found when trying to get lock异常后把当前批次的数据重新入队稍后重试即可。6.6 补充general.log突然膨胀有同学在排查写入问题时开了general_log结果整个库被日志拖垮。这个日志记录所有SQL语句非常吃IO。排查模式下建议这样操作确认用完后立刻关闭且只把日志写到本地临时目录不要长期保存在数据盘上。SET GLOBAL general_log OFF; SET GLOBAL general_log_file /tmp/mysql_general.log;调试完成后记得清理临时文件。7. 这个方案还能怎么扩展聊完了具体实践再说几个可以继续深挖的方向。如果队列缓冲已经做了但批量插入在极端高并发下还是吃紧可以考虑升级为“分库分表”方案。把单表写入分散到多个物理库/物理表每个分片内部再走批量插入整个系统的写吞吐上限会成倍放大。但注意分库分表会引入分布式事务、跨分片查询等问题改动面很大要在批量插入方案确实撑不住时才考虑。另一个方向是把“同步写”彻底改成“异步化”。业务接口接收到请求后直接把数据发到消息队列返回“受理成功”后台消费者异步批量落库。这种模式下接口响应时间几乎不随数据库压力变化体验是最好的。代价是需要接受最终一致性并且要处理重复消息、消息丢失等分布式问题。还有一个小技巧如果表数据量极大超过千万级批量插入前可以先ALTER TABLE ... DISABLE KEYS临时禁用非唯一索引插入完成后重新启用。这个操作在MyISAM表上效果显著但在InnoDB上禁用非唯一索引的效果有限需要实测确认不能照搬经验。最后再分享一个我自己踩坑后的体会这类优化的核心不是“用一个高大上的组件解决问题”而是先把写入路径搞明白——数据从哪里来、经过几层、最终怎么落库。队列缓冲和批量插入之所以有效是因为它们精准击中了“事务过多导致锁竞争”这个要害。做技术选型时能用内存队列就别上MQ能调参解决的就别改架构让每一层尽量简单系统的稳定性反而更高。根据我个人经验批量插入的批次大小不要一上来就定死先用压测工具跑几轮观察Innodb_row_lock_waits和TPS两个指标找到自己业务曲线的最优点。每个项目的表结构、索引数量、机器配置都不一样照搬网上参数不一定合适靠数据说话才是稳妥的做法。