一次偶然的机会debug代码瞅了一眼Mysql客户端的代码(Java版本),最引发我兴趣的是这一句:
execSQL是一个非常重要的方法,所有SQL语句的最终都是交由这个代码来实现的;这个方法被一个connectionMutex锁(这个锁其实就是Connection对象自己,Mysql客户端通过使用了“反射”所以不能直接使用this)包裹着这就意味着——在同一时刻一个Mysql连接只能执行一条SQL语句。
MySQL客户端的“锁”
带着疑问我走读了这段代码,画一幅图
MysqlIO是负责网络通讯的底层类,使用的是Java是BIO通讯属于“标准”的TCP客户端写法。它内部有三个重要的成员变量
- mysqlConnection是一个Socket类型
- mysqlOutput是一个BufferedOutputStream类型
- InputStream是一个InputStream类型(Mysql做了一点封装理解成BufferedInputStream也没什么问题)
这三个成员变量的初始化是在MySqlIO的构造函数完成的,当执行SQL语句的时相当于:
- 锁住Socket,此时只有当前连接可以使用这个Socket对象(实际的锁范围更大)
- mysqlOutput写入数据包,发送Command(内部称客户端->服务器的请求为Command)
- mysqlInput读取数据包,解析返回结果
如果有多条SQL语句递交给一个连接因为Socket被“锁”所以会变成串行执行。
这不是bug,是特性
带着疑问我翻看了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的效率)。
技术就是这么奇妙,很多东西都是你“借鉴我”,我“借鉴你”。相同的问题相同的解决办法,如果用心其实可以汇编成一本书,比如我们上面讲的或许就可以叫——“多路复用的协议设计模式”吧。
【本文是51CTO专栏作者邢森的原创文章,转载请联系作者本人获取授权】