来源:一树一溪
前面一篇文章《MySQL 客户端 Ctrl + C,服务端会发生什么?》介绍的场景是 MySQL 客户端主动触发中断 SQL 执行,服务端进行的一系列操作。
今天我们来介绍另一种场景:MySQL 客户端给服务端发送一条 SQL 之后,服务端执行 SQL 的过程中,客户端没有任何通知,就直接断开了连接。
这种情况下,服务端会怎么办?
本文内容基于 MySQL 8.0.32 源码,涉及存储引擎为 InnoDB。
目录
1. 两种场景对比
2. 客户端不辞而别
3. 服务端怎么办?
3.1 先提交事务再回滚
3.2 回滚事务
4. 总结
5. 遗留问题
正文
1. 两种场景对比
为了和前一篇文章介绍的场景区分开,我们用两个虚构小故事把两种场景放在一起作个对比。
场景一:MySQL 客户端 Ctrl + C,服务端会发生什么?
张三(MySQL 客户端
)和李四(服务端
)是好朋友,它送给了李四一个礼物(发送了一条 DML/DDL SQL
)。
有一天,张三和李四闹别扭,它后悔送礼物给李四了,于是它对李四说:把我送你的礼物还给我(Ctrl + C 要求服务端中断 SQL 执行
)。
李四要先把张三送给它的礼物找出来,才能还给张三。
如果礼物还在(事务还没有提交
),李四就能把礼物还给张三(中断执行,回滚事务
);如果礼物不在了(事务已经提交了
),也就没法还了。
场景二:MySQL 客户端不辞而别,服务端怎么办?
张三(MySQL 客户端
)和李四(服务端
)是好朋友,它送给了李四一个礼物(发送了一条 DML/DDL SQL
)。
有一天,李四因为一件事情把张三惹毛了。
张三心里很不爽,它要跟李四绝交(直接断开了连接
),但对它而言,送出去的礼物就是泼出去的水,它不想收回。
李四的性格大大咧咧,它不知道自己把张三惹毛了,还在美滋滋的欣赏张三送给它的礼物(执行 SQL
)。
等它回过头来想找张三的时候,发现找不着了,它才回想起来,可能自己把张三惹毛了,朋友没得做了。
此时,李四要怎么对待张三送给它的礼物呢?
接下来,我们跳出虚构,回归现实,来捋一下场景二的流程。
这种场景只会出现在通过程序连接 MySQL 服务端,程序没有关闭数据库连接就执行结束或者崩溃了
2. 客户端不辞而别
MySQL 客户端发送一条 DML/DDL SQL 给服务端,服务端收到之后,就开始吭哧吭哧地执行。
SQL 执行完成之前,客户端再没有给服务端发送任何消息,就直接断开连接了。
SQL 执行过程中,服务端并不能感知到连接已经断开了,它还会一直卖力地执行 SQL。
SQL 执行完成之后,问题来了。
3. 服务端怎么办?
3.1 先提交事务再回滚
如果服务端执行的是 DDL
语句,一条 SQL 执行完成之后,会自动提交事务。
如果服务端执行的是 DML
语句,并且系统变量 auto_commit = on
,一条 SQL 执行完成之后,也会自动提交事务。
因为服务端不知道客户端已经断开连接了,事务提交之后,它会把 SQL 执行结果发送给客户端。
把结果发送给客户端,执行的是 send()
系统调用,send() 有两种行为:
行为一,如果 send() 把 SQL 执行结果写入 socket 缓冲区,会返回写入成功,此时,服务端还不会感知到
客户端已经断开连接。
send() 执行成功之后,服务端认为这条 SQL 就告一段落了,会等待客户端发送下一条命令。
由于客户端已经断开连接,从当前连接读取下一条命令时会出错。
此时,服务端会感知到客户端已经断开连接了。
行为二,如果 send() 直接把 SQL 执行结果发送给客户端,服务端就能马上感知
到客户端已经断开连接了。
不管 send() 发生哪种行为,服务端都会感知到客户端已经断开连接了,无非早一点晚一点而已。
感知到连接断开之后,服务端会回滚事务
。
但是,由于执行 send() 之前,服务端已经把事务提交了,这里回滚事务并不会
生效。
那么,最终结果就是:服务端对于数据的修改会被持久化,永久生效。
3.2 回滚事务
如果服务端执行的是 DML
语句,并且用 begin
或 start transaction
显式开启了事务,一条 SQL 执行完成之后,不会自动提交事务,而是会等待客户端发送下一条命令。
读取下一条命令之前,服务端会执行 send()
系统调用,把当前 SQL 的执行结果发送给客户端。
send() 有两种行为:
行为一,如果 send() 把 SQL 执行结果写入 socket 缓冲区,会返回写入成功,此时,服务端还不会感知
到客户端已经断开连接。
send() 执行成功之后,服务端认为这条 SQL 就告一段落了,会等待客户端发送下一条命令。
由于客户端已经断开连接,从当前连接读取下一条命令时会出错。
此时,服务端会感知到
客户端已经断开连接了。
行为二,如果 send() 直接把 SQL 执行结果发送给客户端,服务端就能马上感知
到客户端已经断开连接了。
不管 send() 发生哪种行为,服务端都会感知到客户端已经断开连接了,无非早一点晚一点而已。
感知到连接断开之后,服务端会回滚事务
,由于执行 send() 之前,并没有提交过事务,这里回滚事务会生效
。
那么,最终结果就是:服务端对于数据的修改会被回滚,相当于没有执行过 DML 操作。
4. 总结
前面展开介绍了 MySQL 客户端不辞而别之后,服务端进行的一系列操作,总结起来就 3 条:
第 1 条:如果服务端执行的是 DDL
语句,DDL 会执行成功。
第 2 条:如果服务端执行的是 DML
语句,并且系统变量 auto_commit = on
,DML 也会执行成功。
第 3 条:如果服务端执行的是 DML
语句,并且用 begin、start transaction 显式开启了事务或者系统变量 auto_commit = off
,事务会被回滚,DML 相当于没有执行。
5. 遗留问题
前面介绍到 send() 有两种行为:
先把 SQL 执行结果写入 socket 缓冲区,缓冲区满再发送给客户端。 直接把 SQL 执行结果发送给客户端。
这两种行为由操作系统内核决定,目前对于这个机制还没有完全研究清楚,留待后续。