【事故现象】 一个多步骤数据库查询任务在执行至第三步时失败。
【具体报错】 应用日志抛出 java.sql.SQLException: No operations allowed after statement closed 错误。
【根因推测】 该异常通常意味着数据库连接池中的连接已被服务器端(如MySQL)主动关闭,但客户端(应用)连接池未能及时检测到此连接已失效(无效连接未被清除)。当应用尝试复用这个无效连接执行第三次查询时,服务器拒绝操作并报错。
【常见诱因】 1. 数据库侧因wait_timeout超时断开了空闲连接;2. 客户端连接池配置不当(如未开启测活验证)3. 外部资源限制(数据库缓存不足)引发的连锁性能故障。

详细报错信息如下:

[36mcom.alibaba.druid.filter.stat.StatFilter[0;39m : slow sql 1429 millis. select
        count(1)
        FROM chat_session cs
        LEFT JOIN chat_question cq ON cs.id = cq.session_id AND cq.is_first = 1
         WHERE cq.user_id =?
                AND cs.app_type =?
                AND cs.client_type in (?, ?, ?, ?, ...,?)
        order by cq.create_time desc, cq.id desc

[36mcom.alibaba.druid.filter.stat.StatFilter[0;39m : slow sql 1006 millis. select
            cq.id as id,
            cq.question question,
            cq.create_time createTime
        FROM chat_question cq
         WHERE session_id in (?, ?, ?, ?, ...,?)
[36mcom.alibaba.druid.filter.stat.StatFilter[0;39m : slow sql 8772 millis. select
        count(1)
        from chat_answer ca
        where
            ca.is_collect = 1
            AND ca.question_id in (?, ?, ?, ?, ...,?)
### Cause: java.sql.SQLException: No operations allowed after statement closed.
; No operations allowed after statement closed.; nested exception is java.sql.SQLException: No operations allowed after statement closed.
org.springframework.dao.TransientDataAccessResourceException: 
### Error querying database.  Cause: java.sql.SQLException: No operations allowed after statement closed.
### The error may exist in class path resource [mapper/ChatAnswerMapper.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: select count(1)         from chat_answer ca         where             ca.is_collect = 1             AND ca.question_id in (?, ?, ?, ?, ...,?)

本次原因定位:正常情况下(缓存充足)可以通过索引快速执行的查询,因为数据库缓冲池(Buffer Pool)太小,导致无法有效缓存索引和数据页,从而被迫进行大量的物理磁盘I/O,最终导致查询时间从毫秒级暴跌至秒级(10秒),并且拖累了整个数据库的稳定性。

下面我们分步拆解这个问题:

1. 正常情况下(缓存充足)是如何工作的?

以SELECT COUNT(1) FROM table WHERE queId IN (…) 为例:

1.1 解析与计划:MySQL收到查询后,分析SQL,发现queId字段上有索引(假设是二级索引)。优化器认为使用这个索引来查找IN列表中的值是最优方案。
1.2 索引查找(在内存中):

首先,数据库会从索引中快速定位到每一个queId值对应的记录的主键ID(或直接拿到数据,如果索引是覆盖索引的话)。这个过程主要是随机I/O。

关键点:由于缓冲池(Buffer Pool)足够大,整个索引树(或至少经常被访问的部分)和IN查询涉及到的数据页,都已经被缓存到了内存中。

因此,这些查找操作几乎都在内存中完成,速度极快(微秒级)。

1.3 计数:数据库引擎只需要对找到的记录进行计数,然后将结果返回。
1.4 结果:整个查询可能在几十毫秒内完成。

2. 缓存配置过低时发生了什么?

当 innodb_buffer_pool_size 设置得太小,远小于你的活跃数据集(经常被访问的数据和索引的总大小)时,问题就出现了:

2.1 缓存失效(Cache Thrashing):

缓冲池就像一个“工作台”,空间太小,而你要操作的工具(索引页和数据页)太多。

为了执行你的COUNT查询,数据库需要将相关的索引页和数据页从磁盘加载到缓冲池中。

但由于缓冲池太小,它无法容纳查询所需的所有页。当新页被加载进来时,不得不将一些旧的、可能很快又会被用到的“热”页(Hot Pages)淘汰出去(LRU算法)。

2.2 大量物理磁盘I/O:

你的查询IN列表中可能包含很多值,这意味着它需要访问索引中和表中很多分散在不同数据页上的记录。

由于这些页不在内存中,数据库就必须发起大量的随机磁盘读操作(Random Disk Reads) 来获取它们。

机械硬盘(HDD)的随机I/O性能极差(通常每秒只能完成几十到几百次),这正是速度从毫秒降到秒级的根本原因。即使用SSD会好很多,但大量随机I/O仍然是瓶颈。

2.3 “正常的索引失效”:

这里的“失效”并不是指索引本身被数据库优化器抛弃了。优化器依然会选择使用那个索引,因为从理论上讲它仍然是最优路径。

“失效”指的是索引的性能优势荡然无存。索引的优势在于高效的内存查找。当索引页本身都无法被缓存,每次索引查找都伴随着磁盘I/O时,其性能优势就完全丧失了。此时的查询速度甚至可能不如全表扫描(如果全表扫描是顺序I/O的话),但优化器通常不会在这种情况下选择全表扫描。

2.4 数据库不稳定:

资源竞争:这一个慢查询会长时间占用磁盘I/O资源。其他本来很快的查询,也因为需要读磁盘而排队等待I/O,导致它们的响应时间也变长了。这就是所谓的“一颗老鼠屎坏了一锅粥”。

连接数堆积:慢查询执行时间过长,会长时间占用数据库连接。如果应用并发较高,很快就能耗光数据库的连接池,导致新的请求无法建立连接,出现“数据库连接超时”的错误。

CPU和内存压力:虽然根源是I/O,但大量的I/O等待会导致CPU利用率看似不高(因为CPU在等I/O),而系统负载(Load Average)却很高。同时,频繁的缓存淘汰也增加了内存管理的开销。

3. 解决方案与优化思路

短期应急(治标)

3.1.1 扩容缓冲池(Buffer Pool):这是最直接有效的方法。如果服务器还有空闲内存,立即调整 innodb_buffer_pool_size 参数,将其设置为可用物理内存的 50% - 70%(避免使用Swap)。
-- 查看当前设置
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

-- 在配置文件中修改(需要重启)
innodb_buffer_pool_size = 4G # 例如,设置为4GB
3.1.2 优化SQL:

检查IN列表中的值是否过多。如果非常多(例如上千个),可以考虑分批次查询,或者将其写入临时表然后做表关联。

如果业务允许,使用queId BETWEEN … AND … 代替 IN,可能使访问的数据更连续,减少随机I/O。

长期治本

3.2.1 硬件升级:最彻底的解决方案。将数据库服务器内存扩容,使得缓冲池足以容纳绝大部分的“热数据”。将机械硬盘(HDD)升级为固态硬盘(SSD),能极大提升随机I/O的能力,即使缓存不命中,查询也不会慢得如此离谱。
3.3.2 架构优化:

引入缓存层:在应用层和数据库之间加入Redis或Memcached等缓存服务。对于COUNT(*)这类查询,其结果可以直接缓存起来,避免每次都对数据库进行暴力计数。

读写分离:将这类统计类的查询路由到只读从库上去执行,避免影响主库的稳定性。

分库分表:如果单表数据量确实巨大(亿级以上),需要考虑水平拆分,从根本上减少每次查询需要扫描的数据量。

3.2.3 深度数据库优化:

使用更高效的存储引擎(如 RocksDB)或数据库版本(如 Percona Server、MySQL 8.0 对性能有诸多改进)。

定期进行数据库性能剖析(Profiling),使用 slow query log 找出所有慢查询并逐一优化。

总结
这个问题是一个经典的性能瓶颈传导链:
配置问题(缓存太小) → 引发资源瓶颈(磁盘I/O) → 导致核心优势失效(索引查询变慢) → 拖累整体系统(数据库不稳定)。

解决思路也很清晰: 首先扩大缓存(内存)和升级磁盘(SSD)来缓解I/O压力,其次通过架构手段(缓存、读写分离)减少对数据库的直接压力,最后对SQL和数据库本身进行深度优化。

Logo

加入社区!打开量化的大门,首批课程上线啦!

更多推荐