MySQL连接与SQL查询优化:从握手到执行计划的完整排查指南
干后端这几年MySQL 的日常工作中我听到最多的两句话一句是“连不上数据库了”另一句是“这条 SQL 怎么这么慢”。表面上看这是两个独立的问题一个发生在连接阶段一个发生在查询阶段但如果你把从连接数据库到查询返回这条完整链路摊开看会发现很多看似诡异的报错其实都对应链路中某一个很具体的环节。今天这篇我就用自己实际排查和测试积累的经验把这个全过程从头到尾拆一遍保证讲的都是你平时能直接用上的东西。这篇文章适合谁看刚入门的朋友可以把它当成一条主线把零散的 MySQL 知识点串起来写过一两年 SQL 的同学则可以用它来对照自己的知识体系看看连接、认证、优化器、执行器这些环节里还有哪些细节是你之前没注意到的。我尽量不堆概念每到一个关键点都会说清楚“为什么”也会把我踩过的坑直接摆出来。1. 连接之前先把 MySQL 的“门牌号”和“钥匙”准备好1.1 端口、地址、监听确认 MySQL 到底在哪里等你很多人第一次装 MySQL装完就急着写代码结果程序一跑直接报 2003 Cant connect。这个错误绝大多数情况下不是用户名密码的问题而是客户端压根没找到服务器。MySQL 默认监听在 3306 端口这是它的“门牌号”。你可以用一条命令确认它有没有在听netstat -tlnp | grep 3306能看到0.0.0.0:3306或者::3306这样的输出说明 MySQL 正在所有网卡上监听外部机器有权限的话就能连进来。如果只看到127.0.0.1:3306那说明它只允许本机连接外部连不上是正常的。如果你想让它对局域网开放需要去改配置文件里的bind-address参数bind-address 0.0.0.0改完重启 MySQL 服务。这里要提醒一句bind-address设为0.0.0.0和 MySQL 的用户权限是两套体系前者决定“端口开不开放”后者决定“谁能用哪个账号从哪个 IP 登录”两个都得配好才行。另外 MySQL 还有一个走 Unix Socket 的本地连接方式通常在配置里有socket /var/run/mysqld/mysqld.sock这样的路径。命令行不带-h参数登录时用的往往就是 Socket 而不是 TCP这也是很多新手搞不明白“为什么本地能连远程不能连”的原因之一。1.2 账号与权限连得上不代表你有资格查确认端口通之后下一个坑就是权限。MySQL 的账号是由user和host共同决定的比如app%和applocalhost是两个完全不同的账号。%表示任意主机但不包括localhost这是新手很容易踩的坑明明创建了app%本地用mysql -uapp -p登录却提示 Access denied因为本地那条连接实际匹配的是applocalhost而你没建这个账号。创建账号和授权的标准姿势我建议分两步走CREATE USER app% IDENTIFIED BY StrongPssw0rd; GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO app%;这里只给了业务上需要的DML权限如果你是开发环境可以再考虑加CREATE、ALTER这类DDL权限但生产环境一定要克制。授权之后不需要刻意执行FLUSH PRIVILEGES因为用CREATE USER/GRANT语句修改权限表时MySQL 会自动重新加载权限这条命令是从老版本流传下来的习惯在 5.7 之后基本用不上了。还有一个容易被忽略的点MySQL 8.0 默认的认证插件是caching_sha2_password而 5.7 时代很多客户端默认使用的是mysql_native_password。你用 5.7 时代的老客户端去连 8.0 库经常会报Authentication plugin caching_sha2_password cannot be loaded。解决办法当然是把客户端升级到支持新插件的版本而不是反过来把数据库的认证方式降级除非你有非常特殊的历史原因否则别为了省事破坏默认安全配置。2. 三种最常见的连接方式从命令行到驱动2.1 命令行客户端排查问题时的最后底线我在定位线上问题的时候不管手边有没有图形工具第一反应永远是先用命令行连一下。因为命令行最接近协议本身报错信息也最原始。基本用法如下mysql -h10.0.0.5 -P3306 -uapp -p-h是主机地址-P是端口-u是用户-p表示需要交互式输入密码。如果你还想指定数据库直接在命令末尾加上库名mysql -h10.0.0.5 -P3306 -uapp -p mydb连接成功后我习惯先执行几条“体检”命令SELECT VERSION(); SELECT CURRENT_USER(); SHOW DATABASES;CURRENT_USER()会告诉你当前实际匹配到的账号是appxxx这对排查那种“我以为我是用 A 账号登录实际命中的是 B 账号”的问题非常有帮助。另外提醒一下命令行连接时密码直接跟在-p后面如-p123456是极不安全的做法因为进程列表里能直接看到密码一定要用交互式输入或者用MYSQL_PWD环境变量兜底连这个我都不推荐直接用-p回车后输入最稳妥。2.2 图形化工具Navicat、DBeaver 的连接配置图形工具是日常开发效率神器Navicat 和 DBeaver 我都用过配置逻辑大同小异连接名随意填主机、端口、用户名、密码必填数据库名可以等连上再选。第一次连接时如果勾选“保存密码”相当于把密码以可逆加密的形式存在本地公网机器上我建议不要勾。这类工具在连接时还有一个隐藏参数经常被忽略就是字符集。很多老项目用的库表还是latin1编码而新工具默认请求utf8mb4连接上去后查中文可能会乱码。正确的做法是先执行SHOW VARIABLES LIKE character_set_database;看清库的实际编码再把工具连接里的编码设成一致。千万别在不知道库编码的情况下直接改表结构那不是修乱码是给自己挖坑。图形工具还有一个好处就是能可视化看执行计划。点一下“解释当前语句”就能看到EXPLAIN的输出比命令行敲EXPLAIN再人工对齐列舒服很多。不过建议你不要过分依赖工具还是要能手动读懂执行计划的每一列这样排查问题才不受工具限制。2.3 代码里的连接串JDBC 和 PyMySQL 的经典配置写 Java 的同学对 JDBC URL 应该很熟它的通用格式是jdbc:mysql://10.0.0.5:3306/mydb?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8这里面有三个参数我建议每一次都显式写上useSSL、serverTimezone、characterEncoding。不写serverTimezone在 JDBC 8.x 下通常会报时区相关的异常因为驱动拿不到 JVM 默认时区不写characterEncoding则可能出现中文乱码。还有两个容易被忽略但线上很有用的参数connectTimeout3000socketTimeout5000connectTimeout是 TCP 建立连接的最大等待时间socketTimeout是连接建立后等待数据返回的最大时间。不配这两个参数一旦数据库负载过高或者网络抖动你的应用线程会一直卡在数据库调用上最后把线程池打满。我见过不止一次线上事故就是“数据库慢应用线程全部堆积”最后数据库恢复了应用也恢复不了。Python 这边我用得比较多的是PyMySQL典型写法import pymysql conn pymysql.connect( host10.0.0.5, port3306, userapp, passwordStrongPssw0rd, databasemydb, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor )注意charset传utf8mb4而不是utf8utf8在 MySQL 里其实是utf8mb3遇到 emoji 这类四字节字符会直接报错或者存成乱码。Django 项目如果在 Python 3 下用 PyMySQL还必须在__init__.py里加一行pymysql.install_as_MySQLdb()否则 ORM 会找不到 MySQLdb 模块。2.4 连接串里的隐藏参数SSL、时区、字符集一个都不能省前面提到useSSL这里展开说一下。MySQL 8.0 服务端默认是支持 SSL 的但很多老项目的客户端默认不启用如果你在 JDBC 连接串里什么都不写驱动通常会告警但不会强拦。真正麻烦的是另一种情况你显式设置了useSSLtrue但客户端没有信任服务端证书这时候会直接报SSL connection error。解决思路有两种第一种是正经办法给客户端配置信任证书第二种是排障时临时用verifyServerCertificatefalse跳过证书校验但这是测试环境干的事生产环境别学。从实践角度我建议内网环境直接useSSLfalse走明文字符流因为内网本身有网络隔离和防火墙策略保护公网或跨机房传输必须开启 SSL而且要配证书校验。盲目关 SSL 或者盲目开 SSL 都可能出问题关键是搞清楚自己的传输链路有没有被嗅探的风险。另外characterEncodingutf8和charsetutf8mb4是两码事前者是 Java 驱动生成的字符编码后者是连接握手时告诉服务端“我用什么字符集交互”两边要搭配对才能避免中文显示不出来。3. 连接这一步数据库内部是怎么接待你的3.1 TCP 握手和 MySQL 协议握手包别看命令行敲一下回车就出现Enter password这一瞬间底层已经经历了好几个阶段。首先是 TCP 三次握手这和其他网络应用没有区别。三次握手完成后MySQL 服务端会主动发一个握手包包含协议版本号、服务端版本号、连接 ID、认证插件名、密码盐值等信息。客户端拿到这些信息后会根据认证插件计算出加密后的密码摘要再回传给服务端。这里有个有趣的点就是 MySQL 默认的 3306 端口实际上是“协议端口”不是“HTTP 端口”所以你不能用浏览器去访问数据库。连接建立后双方还会协商能力标志位比如是否支持 SSL、是否支持压缩传输、字符集是哪种。如果你在客户端指定了utf8mb4就是在这个阶段告诉服务端的。连接 ID 也很重要它就是你在SHOW PROCESSLIST里看到的Id列以后排查“这个连接是谁、在干什么、卡了多久”全靠它。3.2 身份认证从 mysql_native_password 到 caching_sha2_passwordMySQL 5.7 最常用的是mysql_native_password它的原理是用SHA1对密码做两次摘要然后和服务端存储的摘要比对。MySQL 8.0 换成了caching_sha2_password从名字就能看出它引入了缓存机制安全性更高但代价是老客户端不兼容。实际排查中我会先用这条命令看当前账号用的什么插件SELECT user, host, plugin FROM mysql.user;如果输出显示caching_sha2_password而客户端报Authentication plugin相关错误那就是客户端版本太老。这一类问题我推荐先升级驱动或客户端而不是立刻执行ALTER USER ... IDENTIFIED WITH mysql_native_password;去降级。因为降级影响的是整个账号的安全性你为了一台老机器妥协等于让所有新机器都承担旧协议的风险。我见过不少团队因为图省事把生产库所有账号都降级了后来做安全审计时不得不匆匆改回中间还引发了业务抖动真不划算。另外SSL 在这里的作用也体现出来了。caching_sha2_password在首次认证时需要一条安全通道来传输密码明文如果连接没走 SSL它需要通过 RSA 公钥加密来传。这也是为什么在某些特殊网络环境下MySQL 8 首次连接会特别慢或者报 RSA 相关的错误。如果你确实没法开 SSL可以在客户端开启allowPublicKeyRetrievaltrueJDBC 参数但前提是你确认网络环境安全。3.3 连接建立后的会话配置以及为什么需要连接池连接认证通过后MySQL 会为这个连接分配一个线程同时设置一堆会话级变量。这些变量只对当前连接生效比如wait_timeout、transaction_isolation、sql_mode。每个新连接都是“干净”的不会继承上一个连接改过的变量。这也是为什么在生产环境里“连接池里的连接如果被某个请求改了隔离级别必须记得复位”的原因否则下一个请求复用这个连接时可能拿到完全不符合预期的隔离级别。说到连接池我多说两句。每建立一个 MySQL 连接背后都涉及 TCP 握手、认证、权限检查、线程创建代价很高。如果你的应用是请求来了就新建连接、请求结束就断开高并发下数据库会被“连接风暴”直接打垮。所以 Java 应用几乎都会用 HikariCP 或 DruidPython 那边也用 DBUtils。连接池的核心参数就那么几个maximumPoolSize最大连接数、minimumIdle最小空闲数、connectionTimeout获取连接的等待时间。连接池大小不是越大越好我踩过这个坑。曾有一台小机器我配了 200 个最大连接数据库瞬间打满CPU 和内存双双报警。比较稳妥的估算公式是(CPU核心数 * 2) 磁盘数比如四核机器配 8~12 个连接通常够用。连接池还有一个关键职责是“保活”它会定期执行轻量级查询比如SELECT 1来确认连接还活着避免数据库主动断开后应用拿到一个失效连接。你在日志里看到偶尔的SELECT 1别慌那是连接池在探活。4. 从 SQL 语句到结果集查询的全过程拆解4.1 客户端发送 SQL协议层做了什么连接建立好接下来就是发查询。MySQL 客户端发送一条 SQL 时最常用的是COM_QUERY命令也就是直接把 SQL 文本发给服务端。如果用预编译语句则是COM_STMT_PREPARE先发送 SQL 模板服务端解析后返回一个语句 ID之后每次执行用COM_STMT_EXECUTE传参数。这也是为什么 JDBC 的PreparedStatement能防止 SQL 注入的核心原因模板和参数是分离的参数根本不会拼进 SQL 里。这里有个容易被忽略的性能点如果一条 SQL 只是参数不同但整体结构相同用预编译语句可以省去重复解析的开销。不过 MySQL 自身有查询缓存相关机制只是在 8.0 里已经彻底移除了查询缓存因为它的命中率低且全局竞争严重。所以别指望“同样的 SQL 跑第二次就快”在 MySQL 8.0 里每次查询都是真正的执行。4.2 词法分析和语法解析把字符串变成内部数据结构服务端收到 SQL 文本后第一步是词法分析。它会把字符串拆成一个个“单词”区分出关键字SELECT、FROM、WHERE、标识符表名、列名、常量字符串、数字和运算符。拆完之后再做语法解析把这些单词按照 MySQL 的语法规则组装成一棵“语法树”。这一步如果出错会提示You have an error in your SQL syntax后面的语法高亮和报错位置就是从这里来的。很多人写 SQL 时不太在意语法树但理解它对排查问题很有用。比如你写了WHERE status active如果status列是整数类型MySQL 在词法解析后做语义分析时会把字符串active转成数字转换失败就变成 0然后给你返回一堆意料之外的行。这类“逻辑错误”不是语法错误它报错都不会报只能靠你经验判断。4.3 预处理与权限核实表存在不代表你能查语法树生成后MySQL 还要做语义检查也就是预处理阶段。它会检查 SQL 里引用的表是否存在、列是否存在、列名有没有歧义还会解析*到底代表哪些列。如果视图涉及底层表也是在这个阶段展开的。也就是说你写错列名时MySQL 不是等到执行才报错而是在预处理阶段就拦下来了。紧接着是权限验证。MySQL 会根据当前用户对库、表、列、存储过程等对象的权限判断这次查询是否被允许。这里要注意MySQL 的权限检查是“分步”的比如SELECT时需要表级别的SELECT权限但如果查询涉及存储函数可能还需要EXECUTE权限。权限检查失败会报SELECT command denied to user此时别急着改密码先想清楚当前账号到底被授予了什么。4.4 查询优化器怎么选索引、怎么定执行计划预处理通过后就轮到优化器做主了。MySQL 优化器的目标是找到一个“代价最低”的执行计划。它会分析语法树、统计信息、索引分布计算各种访问路径的代价包括走哪个索引、是否做排序、怎么关联多张表、是否要临时表。我们平时执行EXPLAIN看到的输出就是优化器最终选定的执行计划。举一个最常见的场景SELECT * FROM orders WHERE user_id 123 ORDER BY create_time DESC。如果user_id有索引create_time没有索引优化器可能选择走user_id索引拿到数据再对create_time做一次文件排序如果user_id选择性不高可能直接选择全表扫描反而更快。优化器到底怎么选跟你统计信息是否准确非常相关。表数据量变化大时ANALYZE TABLE重新收集统计信息往往能解决“ SQL 一样但这两天突然变慢”的问题。优化器不是万能的它也会犯傻。比如它可能低估了一个大IN列表的代价或者对关联顺序判断失误。这时候就需要人工介入用STRAIGHT_JOIN强制关联顺序或者给优化器提示USE INDEX/FORCE INDEX。但这些手段是最后的应急方案不要一开始就用否则表结构一变这些硬编码很可能变成负优化。还有一个老生常谈的点EXPLAIN并不代表语句真正执行时的所有细节比如它不会告诉你运行时的行锁等待、临时表落盘等等但它仍然是入门诊断的第一工具。4.5 执行器与存储引擎最后真正干活的角色拿到执行计划后执行器才开始真正干活。你可以把优化器理解成“制定作战计划”执行器是“前线指挥”存储引擎是“真正跑腿的士兵”。执行器会通过存储引擎的接口逐行读取数据然后进行条件过滤、投影、排序、聚合、联表等操作。如果涉及事务行锁、MVCC、undo log 这些也是在存储引擎层完成的。InnoDB 是绝大部分业务表的默认引擎所以网上讨论存储引擎时基本都在说 InnoDB。执行过程中有几个细节值得了解。比如全表扫描时执行器会一行一行调用存储引擎接口所以 InnoDB 也有“预读”机制一次从磁盘读入一批页到缓冲池减少 IO 次数。MySQL 8.0 还加入了“并行扫描”的能力不过默认并不总是启用。再比如如果ORDER BY或者GROUP BY无法用到索引执行器会把中间结果写到临时表数据量大时临时表会从内存转到磁盘也就是Using temporary; Using filesort。看到EXPLAIN的Extra列里有这个就意味着这条查询的排序或分组有优化空间。返回结果集也不是一次全给完的。执行器会把结果逐条写入网络发送缓冲区然后由服务端陆续发给客户端。客户端收到多少取决于网络包大小如果单行数据过大超了max_allowed_packet就会报Packet too large。所以如果你查大字段比如TEXT、BLOB时遇到这个报错优先考虑是不是这个参数太小而不是数据库坏了。对应用来说读取大结果集时也要注意分批处理别一次性fetch几十万行到内存里。5. 连接和查询中最常遇到的拦路虎附排查顺序5.1 连不上数据库4 个错误码一次说清我做了一个比较全面的故障速查表按我自己的排查顺序排的遇到连不上问题可以直接对着看错误码错误含义常见原因排查方向2003Cant connect to MySQL server网络不通、端口未监听、防火墙拦截ping主机、telnet ip 3306、检查服务状态1045Access denied for user用户名、密码错误或账号不存在确认CURRENT_USER()检查mysql.user1130Host is not allowed to connect账号的 host 限制包含不了当前来源 IP改用user%或user具体IP2059Authentication plugin 错误客户端版本与 8.0 认证插件不匹配升级客户端或暂时降级认证插件最典型的场景是 2003 和 1130 搞混2003 是“找不到服务器”1130 是“找到服务器但你不被允许进”。我从日志里看到过很多次用户明明改的是账号权限却一直在调防火墙。正确顺序一定是先确认网络通不通再看账号 host最后看密码。别一上来就重置密码那样会把问题搞得更乱。SSL 相关报错我也把它归到连接阶段的疑难杂症里。它通常表现为“某些客户端能连、某些不能连”或者在公网环境连接极慢。原因绝大多数是证书链不被信任、主机名和证书不匹配、TLS 版本协商失败。排障时先看客户端日志里有没有SSL字样再用mysql --ssl-modeREQUIRED测试服务端 SSL 是否可用一步步缩小范围。5.2 连上了但查询很慢先从 EXPLAIN 下手连接没问题SQL 却慢这是我日常花时间最多的地方。我处理这类问题的固定套路是“三段式”先看SHOW PROCESSLIST确认语句在做什么再执行EXPLAIN看执行计划最后看慢查询日志确认耗时趋势。SHOW PROCESSLIST的State列非常关键。常见状态包括Sending data、Sorting result、Waiting for table metadata lock、Waiting for handler commit等。如果你看到大量线程处于Sending data里时间很长多半是执行器确实在扫描大量行如果是Waiting for table metadata lock说明有DDL语句卡住了后面的所有查询这时候要找持有锁的会话。Waiting for handler commit则大概率是磁盘 IO 慢或者 binlog 刷盘慢。EXPLAIN里我最先看的是type列它从好到差大致是const、eq_ref、ref、range、index、ALL。如果出现ALL意味着全表扫描需要确认是否能走索引再看possible_keys和key前者代表可能用到的索引后者代表实际用的索引两列不一致时就要思考优化器为什么没选你预期的索引。Extra里的Using filesort、Using temporary也需要重点关注。我看过一条典型的慢查询SELECT * FROM order_detail WHERE order_id IN (SELECT id FROM orders WHERE user_id 123456)。这条 SQL 的执行计划里子查询可能被改写成semi-join如果优化器没选好效率会很低。这个场景下EXISTS和IN的结果很多时候是等价的但代价可能完全不同不能用“哪个关键字快”一概而论一定要结合执行计划判断。同样地去重也想提醒一下SELECT DISTINCT和GROUP BY在某些情况下可以互相替代但如果DISTINCT导致Using temporary那就要考虑是不是可以通过索引来消除排序。5.3 索引失效几个让优化器“眼瞎”的典型写法索引建了却没走很多人第一个反应是“MySQL 优化器太笨”其实大部分情况是写法有问题。我归纳了几个最常见的失效场景全部是我真实遇到过的对索引列做函数运算比如WHERE DATE(create_time) 2025-01-01这时索引列被函数包裹优化器无法按索引范围扫描。正确写法是WHERE create_time 2025-01-01 00:00:00 AND create_time 2025-01-02 00:00:00。隐式类型转换比如WHERE phone 13800138000而phone是 VARCHAR。MySQL 会把字符串列转成数字导致无法走索引。左前置模糊匹配LIKE %abc无法使用索引LIKE abc%可以。模糊查询如果非用前置匹配不可建议考虑全文索引或者搜索引擎。多列索引时没有遵循最左前缀原则比如在(a, b, c)联合索引上只查c索引当然用不上。要想验证是不是索引问题我推荐一个习惯写EXPLAIN看执行计划时顺便看key_len列。key_len表示优化器实际使用的索引长度同样是联合索引(a, b, c)如果key_len比预期短说明只用了部分列那后面的列其实没有参与过滤这时候就要检查查询条件是否满足最左前缀。5.4 存储过程和批量更新对连接的影响有些同学会把复杂业务封装到存储过程里然后应用层像调远程函数一样CALL。我建议先想清楚一个问题存储过程虽然能减少网络往返但它在数据库里执行意味着把 CPU 压力放在数据库上而数据库恰恰是横向扩展最困难的组件。应用服务器不够了可以随便加但数据库瓶颈就不那么好解决。所以存储过程适合那些强一致、需要事务边界包裹的批量逻辑不适合当“万能函数库”来用。批量更新场景也常和连接、事务绑在一起。默认情况下InnoDB 行锁是“加锁即生效事务结束才释放”如果一条更新语句涉及很多行持锁时间会很长。曾经有人用一个循环每次更新查一次数据库把 10 万条数据逐条更新结果全程跑了二十分钟。后来我改成批量拼接UPDATE ... CASE WHEN或者分批次提交时间降到了几十秒。这里的关键不是 SQL 本身多华丽而是减少事务持锁时间避免把其他查询全堵住。5.5 系统表的排查姿势processlist 和 performance_schema排查连接和查询问题时光靠EXPLAIN还不够你得会用系统表。SHOW PROCESSLIST看的是瞬时状态而performance_schema有历史累计数据。比如我想看哪种 SQL 是吃 CPU 大户可以查SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;这条语句能按“归一化后的 SQL 摘要”统计各类语句的总执行时间秒级定位到最耗时的 SQL 类别。相比慢查询日志它的数据是内存表查询更快也不会产生大量落盘日志。当然慢查询日志也有不可替代的作用尤其是你想看某条 SQL 在不同时间点的耗时曲线时日志里每一行都有时间戳。performance_schema里我还经常用events_waits_summary_global_by_event_name看 IO 等待用table_io_waits_summary_by_table看哪张表的读写最频繁。这些数据在排障时是“证据链”能帮你把“数据库慢”这个模糊描述转化成一个可执行的具体问题比如“某张表的 scan 行数过大需要加索引”或者“某类 SQL 的锁等待时间太长需要缩短事务”。6. 个人保留的几点经验写在最后这条链路走完你会发现自己再遇到“数据库慢”“连不上”这类问题时脑海里会有一个清晰的坐标问题出在连接阶段、解析阶段、优化器阶段还是执行器阶段这比抓瞎翻日志要高效太多。分享两个我一直坚持的小习惯。第一所有代码里的连接串必须显式配置超时时间和字符集不依赖默认值。默认值在当前版本下或许没问题但数据库一升级、网络拓扑一调整问题就会毫无预兆地爆发。第二每写完一条有一定复杂度的 SQL至少跑一次EXPLAIN看一眼type、key、rows这前三列。这个习惯坚持半年你对“索引到底怎么生效”的理解会远超大多数同事。最后补一个我踩过很多次才长记性的点不要在事务里做远程调用或者等待外部接口返回。曾经有人在一个事务里调用支付回调接口一等就是几秒结果数据库连接被占住不放连接池被迅速耗尽整个应用跟着崩。数据库事务是宝贵资源里面只放数据库操作任何外部 IOHTTP、Redis、MQ都要拿到事务外面去做。这条原则理解了比我前面讲的很多参数都管用。