一次偶然的机会debug代码瞅了一眼Mysql客户端的代码(Java版本),最引发我兴趣的是这一句:
execSQL
是一个非常重要的方法,所有SQL语句的最终都是交由这个代码来实现的;这个方法被一个 connectionMutex
锁(这个锁其实就是Connection对象自己,Mysql客户端通过使用了“反射”所以不能直接使用 this
)包裹着这就意味着—— 在同一时刻一个Mysql连接只能执行一条SQL语句 。
带着疑问我走读了这段代码,画一幅图
MysqlIO
是负责网络通讯的底层类,使用的是Java是BIO通讯属于“标准”的TCP客户端写法。它内部有三个重要的成员变量
mysqlConnection
是一个Socket类型
mysqlOutput
是一个BufferedOutputStream类型
InputStream
是一个InputStream类型(Mysql做了一点封装理解成BufferedInputStream也没什么问题)
这三个成员变量的初始化是在 MySqlIO
的构造函数完成的,当执行SQL语句的时相当于:
锁住Socket,此时只有当前连接可以使用这个Socket对象(实际的锁范围更大)
mysqlOutput
写入数据包,发送Command(内部称客户端->服务器的请求为Command)
mysqlInput
读取数据包,解析返回结果
如果有多条SQL语句递交给一个 连接
因为Socket被“锁”所以会变成串行执行。
带着疑问我翻看了Mysql的通讯协议,客户端->服务器的标准格式如下:
3字节表示后面“payload”的长度(所以mysql单数据包最大值是4M);
1字节表示“sequence_id”;这个字段似乎没有什么用途
最后一个是变长的“payload”(长度由第一部分决定)
服务器端->客户端(响应数据包)的标准格式是
1字节表示“类型”
根据不同的类型“payload”会有不同的变化,比如返回的错误信息、受影响的行数等
看到这里我明白原因了。
Mysql的数据包中没有办法区分出一个连接中“不同的数据包”。 A、B两条SQL语句,可以通过一个Socket发送到服务器端;服务器端也会返回两个执行结果。问题是——客户端如何区分出哪个是A的执行结果哪个是B的执行结果呢? 仔细观察上面的数据包唯一的方式是sequence_id,A分配一个id,B分配一个;服务器端在返回的时候把相应的sequence_id带回来表示这是某个SQL语句的执行结果。 遗憾的是——sequence_id根本没用到,服务器端不会返回sequence_id信息。(看Mysql的响应数据包)
正是因为这个原因导致了所有的Mysql客户端 Connection
对象都不是线程安全的。如果想要同时执行多条SQL语句就只能构造多个 Connection
。 有意思的是MongoDB的协议格式几乎和Mysql的一样,messageLength,requestID,opCode;但是响应数据包“修正”了这个bug,messageLength,responseID,opCode。 这就意味着mongodb的conneciton是不需要阻塞的而且根本不需要“线程池”。 (根据我观察 connectionsPerHost
似乎是没有用到,难道这个是为了给大家“安全感”?——我们带线程池,放心用吧)
我想到了更多东西,HTTP 2.0的多路复用(Multiplexing)。在HTTP1.0中每个HTTP请求都是一个TCP请求,浏览器载入页面的时候会大量加载css、js、图片、html短时间内会发起大量的TCP请求。在HTTP2.0中同时向一台主机发起css、js、图片可以被承载在同一个TCP连接中。 AMQP中也有相似的设计,叫Channel;一个TCP连接可以被多个线程同时使用。比如用一个TCP连接可以同时实现“订阅”和“发布”消息。 微软RDP协议中也有相同的设计,区分图片、声音、鼠标、键盘操作(Citrix的ICA设计号称的32个通道就是这个意思)。 SSH协议中有一个叫 Channel Mechanism
的东西,它也是为了实现“多路复用”的(这意味着提高了ansible的效率)。
技术就是这么奇妙,很多东西都是你“借鉴我”,我“借鉴你”。相同的问题相同的解决办法,如果 用心其实可以汇编成一本书 ,比如我们上面讲的或许就可以叫——“多路复用的协议设计模式”吧。
欢迎关注公众账号了解更多信息“写程序的康德——思考、批判、理性”