系统突然保存不了数据库,查看日志,有报错

Error: Can't create more than max_prepared_stmt_count statements (current value: 16382)

一脸蒙逼,从没见过该错误啊。

网文最多的解决方法

mysql> show global status like 'com_stmt%';
查看如下3个参数值:
Com_stmt_close             prepare语句关闭的次数
Com_stmt_execute           prepare语句执行的次数
Com_stmt_prepare           prepare语句创建的次数

请确认Com_stmt_close的值是否接近于Com_stmt_prepare。
实际情况是
Com_stmt_prepare=16382
Com_stmt_close=0

也不知道这个参数是做什么的,查看max_prepared_stmt_count的数值用如下命令

mysql> show global variables like 'max_prepared_stmt_count';
+-------------------------+-------+
| Variable_name           | Value |
+-------------------------+-------+
| max_prepared_stmt_count | 16382 |
+-------------------------+-------+
1 row in set (0.01 sec)

然后把max_prepared_stmt_count数值加大

mysql> set global max_prepared_stmt_count=1048576;

或是把写到mysql的配置文件里
max_prepared_stmt_count=1048576

把mysql重启,Com_stmt这些参数全都能归0,也是个临时解决方法

然后再试试程序,是没问题了。但这也不是个办法啊,总不能无限制扩大啊。刚刚解决了没两天,又报相同的错误了。

必须把这个max_prepared_stmt_count搞清楚才是正道。

Prepared Statement 预处理/预编译

刚开始以为max_prepared_stmt_count这个是程序里连接池无限制连接造成的。做了很多试验,发现Com_stmt_close永远是0

查了后才发现,prepared statement在MySQL4.1中新引进的。
预处理的优势:
(1)预处理sql能一定程度上防止sql注入
(2)sql预编译效率更高
(3)二进制包协议让sql预处理更加高效。

既然是预处理了这么多又不关闭,那是不是可以手工关闭。
查了资料是这么写的

qlString = updateSQLString;
conn.execute(sqlString)
        .then(()=>{
          conn.unprepare(sqlString);
          resolve(sqlString);
        })
        .catch((err)=>{
          reject(err);
        });

每次都unprepare一下,那程序改动挺大的,也总感觉还没找到点子上。

继续检查问题

SELECT count(*) AS 重复数, 
GROUP_CONCAT(STATEMENT_ID) as STATEMENT_IDS, 
SQL_TEXT
FROM performance_schema.prepared_statements_instances
GROUP BY SQL_TEXT
ORDER BY 重复数;

发现重复最多的语句

DELETE FROM goods_bom WHERE part_id = ? AND goods_id = ? AND id NOT IN (0,0,0)
DELETE FROM goods_bom WHERE part_id = ? AND goods_id = ? AND id NOT IN (0,0,0,0)
DELETE FROM goods_bom WHERE part_id = ? AND goods_id = ? AND id NOT IN (83,84,85)
......

看到这里,这下有点明白预处理的作用了。就是把这些经常使用的sql语法校验啥的先做好存好,下次再调用,执行前的七七八八事情就不用做了,直接就可以调用执行了。
那问题是为什么会有这么多的相同语句,按道理是应该只有一条的。
程序里是这么写的

await mysql.execute(`DELETE FROM goods_bom WHERE part_id = ? AND goods_id = ? AND id NOT IN (${part.goods_bom.map(v => v.id || 0).join(',') || 0})`, [part.id, id]);

问题就出在IN这段,每次组织好,sql语句都不一样,导致预处理后的语句每次都不一样。这里这么写,主要是之前不知道IN里的占位符该怎么写。那就改IN里的内容,改来改去发现IN是一个大坑。

第一次改成如下:

await mysql.execute(`DELETE FROM goods_bom WHERE part_id = ? AND goods_id = ? AND id NOT IN (?)`, [part.id, id, part.goods_bom.length > 0 ? part.goods_bom.map(v => v.id || 0) : [0]]);

报错:

(node:34895) UnhandledPromiseRejectionWarning: Error: Truncated incorrect DOUBLE value: '["0"]'

按道理是没错的,把拼接后的sql输出看一下

console.log(mysql.format(`DELETE FROM goods_bom WHERE part_id = ? AND goods_id = ? AND id NOT IN (?)`, [part.id, id, part.goods_bom.length > 0 ? part.goods_bom.map(v => v.id || 0) : [0]]));

输出完全正确,没有错误。
再试试把execute改成query

await mysql.query(`DELETE FROM goods_bom WHERE part_id = ? AND goods_id = ? AND id NOT IN (?)`, [part.id, id, part.goods_bom.length > 0 ? part.goods_bom.map(v => v.id || 0) : [0]]);

也完全正确,没错。感觉好像是excute没有把参数正确的解析。

execute()和query()之间的区别:

  1. query是在node装SQL语句,而 execute 则是利用MySQL 的 PreparedStatement 机制来预编译SQL语句
  2. execute 的优势是数据库原生支持的预编译机制,性能更高
  3. query 的优势是更灵活,例如可以用??代替表名、字段、索引名;用?代替数据

想了好一会明白了,execute的时候,数组[1,2,3]会转义成’[1,2,3]‘传给mysql,然后mysql直接就把’[1,2,3]‘替换占位符?了。
query是前端调用,前端会把数组[1,2,3]会转义成(1,2,3)。
之前开发mysql,没细看文档,还以为查询数据用query,例如select;执行语句用execute,例如:update,insert,delete。现在才发现理解有错。query和execute是不是用预处理的区别。

总结

  • execute时,sql语句不要动态拼接,全用占位符
  • 执行sql遇到in时,就query吧,不要用execute,方便
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐