├── .gitignore ├── README.md ├── note ├── connect.md ├── images │ ├── AuthenticationPlugin.jpg │ ├── Connection.jpg │ ├── ConnectionProperty.jpg │ ├── Driver.jpg │ ├── ExceptionInterceptor.jpg │ ├── LRUCache.jpg │ ├── PerConnectionLRUFactory.jpg │ ├── PreparedStatement.jpg │ ├── ProfilerEvent.jpg │ ├── ProfilerEventHandler.jpg │ ├── ReadAheadInputStream.jpg │ ├── ResultSetMetaData.jpg │ ├── SocketFactory.jpg │ ├── StatementInterceptor.jpg │ ├── info.png │ ├── interactive.png │ ├── lv.png │ ├── parse_url.PNG │ ├── parseinfo_staticsql.PNG │ ├── query_packet_pattern.PNG │ ├── query_result_field_pattern.PNG │ └── query_result_pattern.PNG ├── mysql.uml └── query.md ├── pom.xml └── src └── test └── java ├── client └── Client.java └── something └── Something.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | /.idea/ 24 | /target/ 25 | .DS_Store 26 | /mysql-driver.iml 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 传送门 2 | 3 | [连接](https://github.com/seaswalker/mysql-driver/blob/master/note/connect.md) 4 | 5 | [Select](https://github.com/seaswalker/mysql-driver/blob/master/note/query.md) 6 | -------------------------------------------------------------------------------- /note/connect.md: -------------------------------------------------------------------------------- 1 | 我们仍以常用的方式建立数据库连接,如下代码所示: 2 | 3 | ```java 4 | @Before 5 | public void init() throws ClassNotFoundException, SQLException { 6 | Class.forName("com.mysql.jdbc.Driver"); 7 | connection = DriverManager.getConnection( 8 | "jdbc:mysql://localhost:3306/test", "tiger", "tiger"); 9 | } 10 | ``` 11 | 12 | # 驱动注册 13 | 14 | 当mysql驱动类被加载时,会向java.sql.DriverManager进行注册,Driver静态初始化源码: 15 | 16 | ```java 17 | static { 18 | java.sql.DriverManager.registerDriver(new Driver()); 19 | } 20 | ``` 21 | 22 | DriverManager.registerDriver: 23 | 24 | ```java 25 | public static synchronized void registerDriver(java.sql.Driver driver, 26 | DriverAction da) { 27 | /* Register the driver if it has not already been added to our list */ 28 | if(driver != null) { 29 | registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); 30 | } 31 | } 32 | ``` 33 | 34 | registeredDrivers其实是一个CopyOnWriteArrayList类型,DriverAction用于当驱动被取消注册时被调用,DriverInfo是DriverManager的内部类,其实就是对Driver和DriverAction对象进行了一次包装,并没有其它的作用。 35 | 36 | 接下来看一下驱动接口Driver的定义,位于java.sql包下,类图: 37 | 38 | ![Driver](images/Driver.jpg) 39 | 40 | DriverManager.getConnection方法调用了NonRegisteringDriver的connect方法: 41 | 42 | ```java 43 | public java.sql.Connection connect(String url, Properties info) { 44 | if (url != null) { 45 | if (StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX)) { 46 | return connectLoadBalanced(url, info); 47 | } else if (StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) { 48 | return connectReplicationConnection(url, info); 49 | } 50 | } 51 | Properties props = null; 52 | if ((props = parseURL(url, info)) == null) { 53 | return null; 54 | } 55 | if (!"1".equals(props.getProperty(NUM_HOSTS_PROPERTY_KEY))) { 56 | return connectFailover(url, info); 57 | } 58 | Connection newConn = com.mysql.jdbc.ConnectionImpl. 59 | getInstance(host(props), port(props), props, database(props), url); 60 | return newConn; 61 | } 62 | ``` 63 | 64 | 从源码中可以看出,系统针对URL的不同采用了不同的连接策略,对于以jdbc:mysql:loadbalance://开头的URL,便以Master/Slave的架构进行连接,对于以jdbc:mysql:replication://开头的URL便按照双主的架构进行连接,如果就是我们使用的普通的URL,那么检测URL中节点的数量,如果大于1,那么使用failOver的方式,最后才是我们的测试代码中单节点的连接方式。 65 | 66 | 关于以上提到的Mysql两种集群模式,可以参考: 67 | 68 | [Mysql之主从架构的复制原理及主从/双主配置详解(二)](http://blog.csdn.net/sz_bdqn/article/details/46277831) 69 | 70 | parseURL方法用于解析URL中的各个属性,下面是对于默认URL解析之后得到的结果截图: 71 | 72 | ![URL解析](images/parse_url.PNG) 73 | 74 | ConnectionImpl.getInstance方法对当前jdbc版本进行了区分: 75 | 76 | ```java 77 | protected static Connection getInstance(String hostToConnectTo, int portToConnectTo, 78 | Properties info, String databaseToConnectTo, String url){ 79 | if (!Util.isJdbc4()) { 80 | return new ConnectionImpl(hostToConnectTo, portToConnectTo, info, databaseToConnectTo, url); 81 | } 82 | return (Connection) Util.handleNewInstance(JDBC_4_CONNECTION_CTOR, 83 | new Object[] { hostToConnectTo, Integer.valueOf(portToConnectTo), 84 | info, databaseToConnectTo, url }, null); 85 | } 86 | ``` 87 | 88 | 驱动通过判断当前classpath下是否存在java.sql.NClob来决定是否是jdbc4版本,子jdk6开始自带的便是jdbc4版本,各个版本之间的区别在于高版本提供更多的接口实现,接下来都以jdbc4版本进行说明。 89 | 90 | handleNewInstance方法所做的其实就是利用反射的方法构造了一个com.mysql.jdbc.JDBC4Connection的对象,后面的Object数组便是构造器的参数。 91 | 92 | 这里就涉及到了jdbc里的另一个核心接口: Connection: 93 | 94 | ![Connection](images/Connection.jpg) 95 | 96 | 核心连接逻辑位于ConnectionImpl的构造器中,其核心逻辑(简略版源码)如下: 97 | 98 | ```java 99 | public ConnectionImpl(...) { 100 | initializeDriverProperties(info); 101 | initializeSafeStatementInterceptors(); 102 | createNewIO(false); 103 | unSafeStatementInterceptors(); 104 | } 105 | ``` 106 | 107 | 下面分部分对其进行说明。 108 | 109 | # 属性解析 110 | 111 | info是一个Properties对象,由jdbc连接url解析而来,Mysql的url允许我们进行参数的传递,对于我们普通的没有参数的url: jdbc:mysql://localhost:3306/test,解析得到的属性对象如下图: 112 | 113 | ![URL属性](images/info.png) 114 | 115 | 从上面类图中可以看出,ConnectionImpl其实是ConnectionPropertiesImpl的子类,而**ConnectionPropertiesImpl正是连接参数的载体**,所以initializeDriverProperties方法的目的可以总结如下: 116 | 117 | - 将我们通过URL传入的参数设置到ConnectionPropertiesImpl的相应Field中去,以待后续进行连接时使用。 118 | - 根据我们传入的以及默认的参数对相应的数据结构进行初始化。 119 | 120 | initializeDriverProperties首先调用了父类的initializeProperties方法,用以实现第一个目的,简略版源码: 121 | 122 | ```java 123 | protected void initializeProperties(Properties info) throws SQLException { 124 | if (info != null) { 125 | Properties infoCopy = (Properties) info.clone(); 126 | int numPropertiesToSet = PROPERTY_LIST.size(); 127 | for (int i = 0; i < numPropertiesToSet; i++) { 128 | java.lang.reflect.Field propertyField = PROPERTY_LIST.get(i); 129 | ConnectionProperty propToSet = (ConnectionProperty) propertyField.get(this); 130 | propToSet.initializeFrom(infoCopy, getExceptionInterceptor()); 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | ConnectionPropertiesImpl中的配置字段其实都是ConnectionProperty(定义在其内部)类型: 137 | 138 | ![ConnectionProperty](images/ConnectionProperty.jpg) 139 | 140 | 下面是这种属性的典型定义方式: 141 | 142 | ```java 143 | private IntegerConnectionProperty loadBalanceAutoCommitStatementThreshold = new IntegerConnectionProperty 144 | ("loadBalanceAutoCommitStatementThreshold", 0, 0, 145 | Integer.MAX_VALUE, Messages.getString("ConnectionProperties.loadBalanceAutoCommitStatementThreshold"), 146 | "5.1.15", MISC_CATEGORY, Integer.MIN_VALUE); 147 | ``` 148 | 149 | PROPERTY_LIST其实就是用反射的方法得到的ConnectionPropertiesImpl中所有ConnectionProperty类型Field集合,定义以及初始化源码如下: 150 | 151 | ```java 152 | private static final ArrayList PROPERTY_LIST = new ArrayList<>(); 153 | static { 154 | java.lang.reflect.Field[] declaredFields = ConnectionPropertiesImpl.class.getDeclaredFields(); 155 | for (int i = 0; i < declaredFields.length; i++) { 156 | if (ConnectionPropertiesImpl.ConnectionProperty.class.isAssignableFrom(declaredFields[i].getType())) { 157 | PROPERTY_LIST.add(declaredFields[i]); 158 | } 159 | } 160 | } 161 | ``` 162 | 163 | initializeFromfan方法所做的正如其方法名,就是从属性对象中检测有没有和自己相匹配的设置项,如果有,那么更新为我们设置的值,否则使用默认值。 164 | 165 | ConnectionProperty.initializeFrom: 166 | 167 | ```java 168 | void initializeFrom(Properties extractFrom, ExceptionInterceptor exceptionInterceptor) { 169 | String extractedValue = extractFrom.getProperty(getPropertyName()); 170 | extractFrom.remove(getPropertyName()); 171 | initializeFrom(extractedValue, exceptionInterceptor); 172 | } 173 | ``` 174 | 175 | 以StringConnectionProperty为例,接收(String, ExceptionInterceptor)参数的initializeFrom方法实现如下: 176 | 177 | ```java 178 | @Override 179 | void initializeFrom(String extractedValue, ExceptionInterceptor exceptionInterceptor) { 180 | if (extractedValue != null) { 181 | validateStringValues(extractedValue, exceptionInterceptor); 182 | this.valueAsObject = extractedValue; 183 | } else { 184 | //使用默认值 185 | this.valueAsObject = this.defaultValue; 186 | } 187 | this.updateCount++; 188 | } 189 | ``` 190 | 191 | 从上面类图中可以看到,ConnectionProperty中有一个allowableValues字段,对于StringConnectionProperty来说validateStringValues的逻辑很简单,就是依次遍历整个allowableValues数组,检查给定的设置值是否在允许的范围内,核心源码如下: 192 | 193 | ```java 194 | for (int i = 0; i < validateAgainst.length; i++) { 195 | if ((validateAgainst[i] != null) && validateAgainst[i].equalsIgnoreCase(valueToValidate)) { 196 | //检查通过 197 | return; 198 | } 199 | } 200 | ``` 201 | 202 | # 异常拦截器 203 | 204 | initializeDriverProperties方法相关源码: 205 | 206 | ```java 207 | String exceptionInterceptorClasses = getExceptionInterceptors(); 208 | if (exceptionInterceptorClasses != null && !"".equals(exceptionInterceptorClasses)) { 209 | this.exceptionInterceptor = new ExceptionInterceptorChain(exceptionInterceptorClasses); 210 | } 211 | ``` 212 | 213 | 很容易想到,getExceptionInterceptors方法获取的其实是父类中定义的exceptionInterceptors属性: 214 | 215 | ```java 216 | private StringConnectionProperty exceptionInterceptors = new StringConnectionProperty("exceptionInterceptors", null, 217 | Messages.getString("ConnectionProperties.exceptionInterceptors"), "5.1.8", MISC_CATEGORY, Integer.MIN_VALUE); 218 | ``` 219 | 220 | 也就是说我们可以通过给URL传入exceptionInterceptors参数以定义我们自己的异常处理器,并且是从Mysql 5.1.8版本才开始支持,这其实为我们留下了一个扩展点: 可以不修改业务代码从而对Mysql驱动的运行状态进行监控。只找到了下面一篇简单的介绍文章: 221 | 222 | [Connector/J extension points – exception interceptors](http://mysqlblog.fivefarmers.com/2011/11/21/connectorj-extension-points-%E2%80%93-exception-interceptors/) 223 | 224 | 所有的异常拦截器必须实现ExceptionInterceptor接口,这里要吐槽一下,这个接口竟然一行注释也没有! 225 | 226 | ![ExceptionInterceptor](images/ExceptionInterceptor.jpg) 227 | 228 | ExceptionInterceptorChain其实是装饰模式的体现,内部有一个拦截器列表: 229 | 230 | ```java 231 | List interceptors; 232 | ``` 233 | 234 | 其interceptException方法便是遍历此列表依次调用所有拦截器的interceptException方法。 235 | 236 | ## 初始化 237 | 238 | 由ExceptionInterceptorChain的构造器调用Util.loadExtensions方法完成: 239 | 240 | ```java 241 | public static List loadExtensions(Connection conn, Properties props, String extensionClassNames, String errorMessageKey, 242 | ExceptionInterceptor exceptionInterceptor) { 243 | List extensionList = new LinkedList(); 244 | List interceptorsToCreate = StringUtils.split(extensionClassNames, ",", true); 245 | String className = null; 246 | for (int i = 0, s = interceptorsToCreate.size(); i < s; i++) { 247 | className = interceptorsToCreate.get(i); 248 | Extension extensionInstance = (Extension) Class.forName(className).newInstance(); 249 | extensionInstance.init(conn, props); 250 | extensionList.add(extensionInstance); 251 | } 252 | return extensionList; 253 | } 254 | ``` 255 | 256 | 从这里可以看出以下几点: 257 | 258 | - exceptionInterceptors参数可以同时指定多个拦截器,之间以逗号分隔。 259 | - 拦截器指定时必须用完整的类名。 260 | - 按照我们传入的参数的顺序进行调用。 261 | 262 | # 国际化 263 | 264 | 从上面配置项的定义可以看出,Mysql使用了Messages类对消息进行了处理,这里Messages其实是对jdk国际化支持ResourceBundle的一层包装,下面是其简略版源码: 265 | 266 | ```java 267 | public class Messages { 268 | private static final String BUNDLE_NAME = "com.mysql.jdbc.LocalizedErrorMessages"; 269 | private static final ResourceBundle RESOURCE_BUNDLE; 270 | public static String getString(String key) { 271 | return RESOURCE_BUNDLE.getString(key); 272 | } 273 | } 274 | ``` 275 | 276 | 这里省略了资源加载的过程,什么是国际化,问度娘就好了。 277 | 278 | # 日志记录 279 | 280 | 对应initializeDriverProperties方法的下列源码: 281 | 282 | ```java 283 | if (getProfileSql() || getUseUsageAdvisor()) { 284 | this.eventSink = ProfilerEventHandlerFactory.getInstance(getMultiHostSafeProxy()); 285 | } 286 | ``` 287 | 288 | getProfileSql方法对应URL的profileSQL参数,getUseUsageAdvisor对应useUsageAdvisor参数,两个参数默认为false,即关闭,profileSQL如果打开,那么Mysql将会把sql执行的相应日志记录下来,用于性能分析,参见: 289 | 290 | [Enabling MySQL general query log with JDBC](https://stackoverflow.com/questions/10903206/enabling-mysql-general-query-log-with-jdbc) 291 | 292 | useUsageAdvisor参数用以记录Mysql认为性能不高的查询操作。 293 | 294 | eventSink是一个ProfilerEventHandler对象: 295 | 296 | ![ProfilerEventHandler](images/ProfilerEventHandler.jpg) 297 | 298 | getMultiHostSafeProxy方法用于在负载均衡的情况下获得代理对象,如果使用了负载均衡,那么必定有多台真实的Mysql物理机,所以在这种情况下连接就变成一个逻辑上的概念。 299 | 300 | 下面看一下ProfilerEventHandlerFactory.getInstance的实现: 301 | 302 | ```java 303 | public static synchronized ProfilerEventHandler getInstance(MySQLConnection conn) throws SQLException { 304 | //这里获取的就是ConnectionImpl内部的eventSink,第一次当然为null 305 | ProfilerEventHandler handler = conn.getProfilerEventHandlerInstance(); 306 | if (handler == null) { 307 | handler = (ProfilerEventHandler) Util.getInstance(conn.getProfilerEventHandler(), 308 | new Class[0], new Object[0], conn.getExceptionInterceptor()); 309 | conn.initializeExtension(handler); 310 | conn.setProfilerEventHandlerInstance(handler); 311 | } 312 | return handler; 313 | } 314 | ``` 315 | 316 | Util.getInstance方法根据传入的完整类名用反射的方式初始化一个对象并返回,完整类名便是conn.getProfilerEventHandler()方法获得的在ConnectionPropertiesImpl中定义的profilerEventHandler参数,默认值便是com.mysql.jdbc.profiler.LoggingProfilerEventHandler,不过从这里我们也可以看出,Mysql为我们留下了扩展的机会。 317 | 318 | LoggingProfilerEventHandler的实现非常简单: 319 | 320 | ```java 321 | public class LoggingProfilerEventHandler implements ProfilerEventHandler { 322 | private Log log; 323 | public void consumeEvent(ProfilerEvent evt) { 324 | if (evt.eventType == ProfilerEvent.TYPE_WARN) { 325 | this.log.logWarn(evt); 326 | } else { 327 | this.log.logInfo(evt); 328 | } 329 | } 330 | public void destroy() { 331 | this.log = null; 332 | } 333 | //被上面的initializeExtension方法调用 334 | public void init(Connection conn, Properties props) throws SQLException { 335 | this.log = conn.getLog(); 336 | } 337 | } 338 | ``` 339 | 340 | ## ProfilerEvent 341 | 342 | ![ProfilerEvent](images/ProfilerEvent.jpg) 343 | 344 | 推测: 此类必定是检测事件对象的序列化与反序列化的载体。 345 | 346 | # 预编译缓存 347 | 348 | 所谓的"预编译"指的便是jdbc标准里面的PreparedStatement: 349 | 350 | ![PreparedStatement](images/PreparedStatement.jpg) 351 | 352 | 注意,Statement和PreparedStatement的类图并未画全,否则实在是太长了。:cry: 353 | 354 | Statement在jdbc里代表的便是一条sql语句的执行,而这里的编译指的是什么将在后面提到。参数cachePrepStmts 如果设为true,那么jdbc便会将编译之后得到的PreparedStatement对象缓存起来,**当在一个连接内**多次针对同一条sql语句调用`connection.prepareStatement(sql)`方法时返回的实际上是一个PreparedStatement对象。这在数据库连接池中非常有用,默认是关闭的。 355 | 356 | 预编译的好处共有两点: 357 | 358 | - 减轻Mysql服务的负担,这不废话么。 359 | - **抵御SQL注入攻击**, 360 | 361 | 如果我们开启了此参数,那么Mysql jdbc将为其建立相应的缓存数据结构,initializeDriverProperties方法相应源码: 362 | 363 | ```java 364 | if (getCachePreparedStatements()) { 365 | createPreparedStatementCaches(); 366 | } 367 | ``` 368 | 369 | createPreparedStatementCaches简略版源码: 370 | 371 | ```java 372 | private void createPreparedStatementCaches() throws SQLException { 373 | synchronized (getConnectionMutex()) { 374 | //默认25 375 | int cacheSize = getPreparedStatementCacheSize(); 376 | //1. 377 | Class factoryClass = Class.forName(getParseInfoCacheFactory()); 378 | CacheAdapterFactory cacheFactory = ((CacheAdapterFactory) factoryClass.newInstance()); 379 | this.cachedPreparedStatementParams = cacheFactory.getInstance(this, this.myURL, getPreparedStatementCacheSize(), 380 | getPreparedStatementCacheSqlLimit(), this.props); 381 | //2. 382 | if (getUseServerPreparedStmts()) { 383 | this.serverSideStatementCheckCache = new LRUCache(cacheSize); 384 | this.serverSideStatementCache = new LRUCache(cacheSize) { 385 | private static final long serialVersionUID = 7692318650375988114L; 386 | @Override 387 | protected boolean removeEldestEntry(java.util.Map.Entry eldest) { 388 | if (this.maxElements <= 1) { 389 | return false; 390 | } 391 | boolean removeIt = super.removeEldestEntry(eldest); 392 | if (removeIt) { 393 | ServerPreparedStatement ps = (ServerPreparedStatement) eldest.getValue(); 394 | ps.isCached = false; 395 | ps.setClosed(false); 396 | 397 | try { 398 | ps.close(); 399 | } catch (SQLException sqlEx) { 400 | // punt 401 | } 402 | } 403 | return removeIt; 404 | } 405 | }; 406 | } 407 | } 408 | } 409 | ``` 410 | 411 | 整个方法的逻辑明显可以分为2部分,第一部分的效果就是创建了一个cachedPreparedStatementParams并保存在ConnectionImpl内部,定义如下: 412 | 413 | ```java 414 | /** A cache of SQL to parsed prepared statement parameters. */ 415 | private CacheAdapter cachedPreparedStatementParams; 416 | ``` 417 | 418 | factoryClass默认为PerConnectionLRUFactory: 419 | 420 | ![PerConnectionLRUFactory](images/PerConnectionLRUFactory.jpg) 421 | 422 | 为什么缓存接口名为CacheAdapter呢? 423 | 424 | Mysql允许我们使用不同的CacheAdapterFactory实现,可以通过参数parseInfoCacheFactory传入,加入我们想用Guava cache代替默认的缓存实现,那么我们只需要编写一个类并实现CacheAdapter接口,内部委托给Guava cache,这不就相当于一个Adapter吗,猜的好有道理的样子,逃:)... 425 | 426 | PerConnectionLRU内部完全委托给LRUCache实现: 427 | 428 | ![LRUCache](images/LRUCache.jpg) 429 | 430 | # 服务端编译 431 | 432 | 默认Mysql的jdbc编译是在客户端完成的,我们可以通过参数useServerPrepStmts 将其改为在服务器端编译,不过Mysql官方建议应该非常谨慎(不要)修改这个参数,这两部分内容可以参考: 433 | 434 | [What's the difference between cachePrepStmts and useServerPrepStmts in MySQL JDBC Driver](https://stackoverflow.com/questions/32286518/whats-the-difference-between-cacheprepstmts-and-useserverprepstmts-in-mysql-jdb) 435 | 436 | # 存储过程缓存 437 | 438 | jdbc标准里CallableStatement负责对存储过程的调用执行,而cacheCallableStmts参数正是用于开启对存储过程调用的缓存,initializeDriverProperties方法相关源码: 439 | 440 | ```java 441 | if (getCacheCallableStatements()) { 442 | this.parsedCallableStatementCache = new LRUCache(getCallableStatementCacheSize()); 443 | } 444 | ``` 445 | 446 | # 元数据缓存 447 | 448 | 众所周知我们在进行select操作时,jdbc将返回ResultSet对象作为结果集,调用其getMetaData方法可以获得一个ResultSetMetaData对象,这就代表了结果集的元数据信息: 449 | 450 | ![ResultSetMetaData](images/ResultSetMetaData.jpg) 451 | 452 | 从中我们可以获得列数,列的相关信息等。initializeDriverProperties方法相关源码: 453 | 454 | ```java 455 | if (getCacheResultSetMetadata()) { 456 | this.resultSetMetadataCache = new LRUCache(getMetadataCacheSize()); 457 | } 458 | ``` 459 | 460 | 关于此属性可进一步参考: 461 | 462 | [What metadata is cached when using cacheResultSetMetadata=true with MySQL JDBC connector?](https://stackoverflow.com/questions/23817312/what-metadata-is-cached-when-using-cacheresultsetmetadata-true-with-mysql-jdbc-c) 463 | 464 | # 批量查询 465 | 466 | 如果我们开启了allowMultiQueries参数,那么便可以这样写SQL语句交给Mysql执行: 467 | 468 | ```sql 469 | select * from student;select name from student; 470 | ``` 471 | 472 | 一次写多条,中间以分号分割。 473 | 474 | 不过在目前的Mysql驱动实现中,此选项和元数据缓存是冲突的,即如果开启了此选项,元数据缓存将会被禁用,即使cacheResultSetMetadata设为true: 475 | 476 | ```java 477 | if (getAllowMultiQueries()) { 478 | setCacheResultSetMetadata(false); // we don't handle this yet 479 | } 480 | ``` 481 | 482 | # StatementInterceptor 483 | 484 | Mysql驱动允许我们通过参数statementInterceptors指定一组StatementInterceptor: 485 | 486 | ![StatementInterceptor](images/StatementInterceptor.jpg) 487 | 488 | 就像Netty、Tomcat一样,这里又是链式调用的实现,preProcess和postProcess方法会在每个语句执行的前后分别被调用。 initializeSafeStatementInterceptors方法会检测并初始化我们指定的拦截器: 489 | 490 | ```java 491 | public void initializeSafeStatementInterceptors() throws SQLException { 492 | this.isClosed = false; 493 | //反射初始化 494 | List unwrappedInterceptors = Util.loadExtensions(this, this.props, getStatementInterceptors(), , ); 495 | this.statementInterceptors = new ArrayList(unwrappedInterceptors.size()); 496 | for (int i = 0; i < unwrappedInterceptors.size(); i++) { 497 | Extension interceptor = unwrappedInterceptors.get(i); 498 | if (interceptor instanceof StatementInterceptor) { 499 | if (ReflectiveStatementInterceptorAdapter.getV2PostProcessMethod(interceptor.getClass()) != null) { 500 | this.statementInterceptors.add( 501 | new NoSubInterceptorWrapper(new ReflectiveStatementInterceptorAdapter((StatementInterceptor) interceptor))); 502 | } else { 503 | this.statementInterceptors.add( 504 | new NoSubInterceptorWrapper(new V1toV2StatementInterceptorAdapter((StatementInterceptor) interceptor))); 505 | } 506 | } else { 507 | this.statementInterceptors.add(new NoSubInterceptorWrapper((StatementInterceptorV2) interceptor)); 508 | } 509 | } 510 | } 511 | ``` 512 | 513 | 从源码中可以看出两点: 514 | 515 | - StatementInterceptorV2接口应该是StatementInterceptor的新(替代)版本,源码中使用Adapter将老版本适配为新版本。 516 | 517 | - NoSubInterceptorWrapper相当于一个装饰器,我们来看一下其preProcess方法的实现便知其目的: 518 | 519 | ```java 520 | public ResultSetInternalMethods preProcess(String sql, Statement interceptedStatement, Connection connection) { 521 | this.underlyingInterceptor.preProcess(sql, interceptedStatement, connection); 522 | return null; // don't allow result set substitution 523 | } 524 | ``` 525 | 526 | underlyingInterceptor为被装饰者,根据StatementInterceptor(V2)语义,如果preProcess或postProcess方法的返回值非空,那么驱动便会将此值作为结果集返回给调用者,而不是真正的数据库查询结果。所以包装为NoSubInterceptorWrapper的目的便是**在驱动启动(初始化)时禁用这一特性**。 527 | 528 | # 连接 529 | 530 | ConnectionImpl.createNewIO: 531 | 532 | ```java 533 | public void createNewIO(boolean isForReconnect) throws SQLException { 534 | synchronized (getConnectionMutex()) { 535 | Properties mergedProps = exposeAsProperties(this.props); 536 | if (!getHighAvailability()) { 537 | connectOneTryOnly(isForReconnect, mergedProps); 538 | return; 539 | } 540 | connectWithRetries(isForReconnect, mergedProps); 541 | } 542 | } 543 | ``` 544 | 545 | 源码说的"HighAvailability"是啥? 546 | 547 | ```java 548 | protected boolean getHighAvailability() { 549 | return this.highAvailabilityAsBoolean; 550 | } 551 | ``` 552 | 553 | highAvailabilityAsBoolean在ConnectionPropertiesImpl.postInitialization方法中被设置: 554 | 555 | ```java 556 | this.highAvailabilityAsBoolean = this.autoReconnect.getValueAsBoolean(); 557 | ``` 558 | 559 | 其实就是一个自动重连而已,默认为false。:joy_cat: 560 | 561 | connectOneTryOnly简略版源码: 562 | 563 | ```java 564 | private void connectOneTryOnly(boolean isForReconnect, Properties mergedProps){ 565 | coreConnect(mergedProps); 566 | this.connectionId = this.io.getThreadId(); 567 | this.isClosed = false; 568 | this.io.setStatementInterceptors(this.statementInterceptors); 569 | // Server properties might be different from previous connection, so initialize again... 570 | initializePropsFromServer(); 571 | return; 572 | } 573 | ``` 574 | 575 | coreConnect方法简略版源码: 576 | 577 | ```java 578 | private void coreConnect(Properties mergedProps) { 579 | this.io = new MysqlIO(newHost, newPort, mergedProps, getSocketFactoryClassName(), getProxy(), getSocketTimeout(), 580 | this.largeRowSizeThreshold.getValueAsInt()); 581 | this.io.doHandshake(this.user, this.password, this.database); 582 | if (versionMeetsMinimum(5, 5, 0)) { 583 | // error messages are returned according to character_set_results which, at this point, is set from the response packet 584 | this.errorMessageEncoding = this.io.getEncodingForHandshake(); 585 | } 586 | } 587 | ``` 588 | 589 | MysqlIO类用于驱动与服务器的交互,其构造器源码精简如下: 590 | 591 | ```java 592 | public MysqlIO(String host, int port, Properties props, String socketFactoryClassName, MySQLConnection conn, int socketTimeout, 593 | int useBufferRowSizeThreshold) { 594 | this.socketFactory = createSocketFactory(); 595 | this.mysqlConnection = this.socketFactory.connect(this.host, this.port, props); 596 | if (socketTimeout != 0) { 597 | try { 598 | this.mysqlConnection.setSoTimeout(socketTimeout); 599 | } catch (Exception ex) { 600 | /* Ignore if the platform does not support it */ 601 | } 602 | } 603 | //意义不大,跳过 604 | this.mysqlConnection = this.socketFactory.beforeHandshake(); 605 | //input 606 | if (this.connection.getUseReadAheadInput()) { 607 | this.mysqlInput = new ReadAheadInputStream(this.mysqlConnection.getInputStream(), 16384, this.connection.getTraceProtocol(), 608 | this.connection.getLog()); 609 | } else if (this.connection.useUnbufferedInput()) { 610 | this.mysqlInput = this.mysqlConnection.getInputStream(); 611 | } else { 612 | this.mysqlInput = new BufferedInputStream(this.mysqlConnection.getInputStream(), 16384); 613 | } 614 | this.mysqlOutput = new BufferedOutputStream(this.mysqlConnection.getOutputStream(), 16384); 615 | } 616 | ``` 617 | 618 | ## SocketFactory 619 | 620 | 驱动使用SocketFactory接口完成Socket的创建与连接,这里仍然是策略模式的体现,默认实现是com.mysql.jdbc.StandardSocketFactory. 621 | 622 | ![SocketFactory](images/SocketFactory.jpg) 623 | 624 | StandardSocketFactory的connect方法实现其实就是创建Socket并连接到给定的地址,但是有一个细节值得注意: 625 | 626 | ```java 627 | InetAddress[] possibleAddresses = InetAddress.getAllByName(this.host); 628 | for (int i = 0; i < possibleAddresses.length; i++) { 629 | this.rawSocket = createSocket(props); 630 | configureSocket(this.rawSocket, props); 631 | InetSocketAddress sockAddr = new InetSocketAddress(possibleAddresses[i], this.port); 632 | this.rawSocket.connect(sockAddr, getRealTimeout(connectTimeout)); 633 | break; 634 | } 635 | ``` 636 | 637 | InetAddress.getAllByName方法将会返回给定的hostname的所有的IP地址,比如如果hostname是localhost,那么返回的结果是: 638 | 639 | [localhost/127.0.0.1, localhost/0:0:0:0:0:0:0:1] 640 | 641 | 一个是IPV4地址,另一个是IPV6地址。如果hostname为www.baidu.com: 642 | 643 | [www.baidu.com/119.75.213.61, www.baidu.com/119.75.216.20] 644 | 645 | 原理其实就是DNS查找,Mysql驱动将会遍历IP数组,只要有一个IP连接成功即结束遍历。 646 | 647 | ## 输入流 648 | 649 | MysqlIO构造器源码input标记处决定了驱动使用何种输入流。有三种选项: 650 | 651 | - Mysql的ReadAheadInputStream,useReadAheadInput参数控制,默认true. 652 | - jdk BufferedInputStream. 653 | - 最原始的输入流,即SocketInputsream. 654 | 655 | 在这里重点关注ReadAheadInputStream和BufferedInputStream的区别以及它的好处到底在哪里。类图: 656 | 657 | ![ReadAheadInputStream](images/ReadAheadInputStream.jpg) 658 | 659 | 通过对两个类源码的详细阅读以及对比,得出结论: **ReadAheadInputStream就是去掉了mark(), reset()方法支持的BufferedInputStream**,其它的核心逻辑的实现完全一样,markSupported方法源码: 660 | 661 | ```java 662 | @Override 663 | public boolean markSupported() { 664 | return false; 665 | } 666 | ``` 667 | 668 | ## 输出流 669 | 670 | 就是原生BufferedOutputStream,没什么好说的。 671 | 672 | ## 握手 673 | 674 | 握手所做的可以概括为以下两个方面: 675 | 676 | - 接收Mysql服务器发送而来的版本等信息,客户端(jdbc)根据这些信息判断驱动和服务器的版本是否可以相互支持,以及决定哪些特性可以使用。 677 | - 向服务器发送用户名密码等认证信息完成登录。 678 | 679 | 相应的源码实现需要根据各个版本信息进行复杂的条件判断,这里不再贴出。整个交互的流程可如下图进行表示: 680 | 681 | ![交互](images/interactive.png) 682 | 683 | Mysql底层在进行消息的发送与接收时,使用的是类似于TLV的结构,不过这里没有T,只有L和V,以服务端信息阶段为例,服务器发送过来的消息的格式大体如下: 684 | 685 | ![消息格式](images/lv.png) 686 | 687 | head(消息头)由4个字节组成,前三个字节为长度字段,小端序,假设其采用的是无符号数(没有理由使用有符号数),那么可以表示的最大消息体长度为16MB,其实在驱动内部将消息体的最大长度限制在1MB,如果超过此值那么将会抛出异常。S表示一字节的包序列号。 688 | 689 | 消息内容以一个人为追加的空字符(0)作为结尾,事实上在驱动内部读取字符串类型时均以遇到字节0作为此次读取的结束。P表示消息版本号,V表示服务器的版本,字符串类型,示例: "5.5.6";ID表示此次连接的ID。 690 | 691 | ### 认证: 可插拔 692 | 693 | 这里的认证指的便是客户端(驱动)向服务器发送用户名、密码完成登录的过程,Mysql自5.5.7版本(这里使用的是5.7.18)开始引入了可插拔登录的概念,主要目的有两点: 694 | 695 | - 引入扩展登录方式支持,比如利用Windows ID、Kerberos进行认证。传统的认证方式是**查询Mysql的mysql.user表**。 696 | - 支持"代理"用户。 697 | 698 | 具体可以参考Mysql官方文档: [6.3.6 Pluggable Authentication](https://dev.mysql.com/doc/refman/5.5/en/pluggable-authentication.html) 699 | 700 | 反映到代码层次上就是驱动将密码的转换抽象为插件的形式,密码的转换以默认的插件进行说明: 假定我们的密码为"1234",Mysql中实际存储的必定不是简单的字符串1234,而是经过摘要/加密得到的串,那么这个过程便称为"转换"。"插件"的类图如下: 701 | 702 | ![认证插件](images/AuthenticationPlugin.jpg) 703 | 704 | AuthenticationPlugin的每一个实现类都是Mysql**内建支持**的认证插件,我们可以通过参数defaultAuthenticationPlugin指定使用的插件,默认为MysqlNativePasswordPlugin,关于此插件的说明可以参考官方文档: 705 | 706 | [6.5.1.1 Native Pluggable Authentication](https://dev.mysql.com/doc/refman/5.5/en/native-pluggable-authentication.html) 707 | 708 | 其对明文密码进行处理的核心逻辑位于方法nextAuthenticationStep: 709 | 710 | ```java 711 | bresp = new Buffer(Security.scramble411(pwd, fromServer.readString(), this.connection.getPasswordCharacterEncoding())); 712 | ``` 713 | 714 | 驱动中加载插件、利用插件进行认证的入口位于MysqlIO的proceedHandshakeWithPluggableAuthentication方法。 715 | 716 | 从上面的内容也可以看出,Mysql进行认证时通过网络进行传输的并不是明文,如果是,那就丢人了。 -------------------------------------------------------------------------------- /note/images/AuthenticationPlugin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/AuthenticationPlugin.jpg -------------------------------------------------------------------------------- /note/images/Connection.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/Connection.jpg -------------------------------------------------------------------------------- /note/images/ConnectionProperty.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/ConnectionProperty.jpg -------------------------------------------------------------------------------- /note/images/Driver.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/Driver.jpg -------------------------------------------------------------------------------- /note/images/ExceptionInterceptor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/ExceptionInterceptor.jpg -------------------------------------------------------------------------------- /note/images/LRUCache.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/LRUCache.jpg -------------------------------------------------------------------------------- /note/images/PerConnectionLRUFactory.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/PerConnectionLRUFactory.jpg -------------------------------------------------------------------------------- /note/images/PreparedStatement.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/PreparedStatement.jpg -------------------------------------------------------------------------------- /note/images/ProfilerEvent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/ProfilerEvent.jpg -------------------------------------------------------------------------------- /note/images/ProfilerEventHandler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/ProfilerEventHandler.jpg -------------------------------------------------------------------------------- /note/images/ReadAheadInputStream.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/ReadAheadInputStream.jpg -------------------------------------------------------------------------------- /note/images/ResultSetMetaData.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/ResultSetMetaData.jpg -------------------------------------------------------------------------------- /note/images/SocketFactory.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/SocketFactory.jpg -------------------------------------------------------------------------------- /note/images/StatementInterceptor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/StatementInterceptor.jpg -------------------------------------------------------------------------------- /note/images/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/info.png -------------------------------------------------------------------------------- /note/images/interactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/interactive.png -------------------------------------------------------------------------------- /note/images/lv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/lv.png -------------------------------------------------------------------------------- /note/images/parse_url.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/parse_url.PNG -------------------------------------------------------------------------------- /note/images/parseinfo_staticsql.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/parseinfo_staticsql.PNG -------------------------------------------------------------------------------- /note/images/query_packet_pattern.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/query_packet_pattern.PNG -------------------------------------------------------------------------------- /note/images/query_result_field_pattern.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/query_result_field_pattern.PNG -------------------------------------------------------------------------------- /note/images/query_result_pattern.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seaswalker/mysql-driver/ccbb2c091f0e70ace330dac985f3f0ebb75c49ac/note/images/query_result_pattern.PNG -------------------------------------------------------------------------------- /note/query.md: -------------------------------------------------------------------------------- 1 | 以下列源码为例进行说明: 2 | 3 | ```java 4 | @Test 5 | public void query() throws SQLException { 6 | final String sql = "select * from student"; 7 | PreparedStatement ps = connection.prepareStatement(sql); 8 | ResultSet rs = ps.executeQuery(); 9 | while (rs.next()) { 10 | System.out.println("User: " + rs.getString("name") + "."); 11 | } 12 | } 13 | ``` 14 | 15 | prepareStatement方法的实现位于ConnectionImpl: 16 | 17 | ```java 18 | public java.sql.PreparedStatement prepareStatement(String sql) throws SQLException { 19 | return prepareStatement(sql, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY); 20 | } 21 | ``` 22 | 23 | DEFAULT_RESULT_SET_TYPE的定义如下: 24 | 25 | ```java 26 | private static final int DEFAULT_RESULT_SET_TYPE = ResultSet.TYPE_FORWARD_ONLY; 27 | ``` 28 | 29 | 含义为通过此种类型结果集得到的cursor只能单向向后进行遍历。 30 | 31 | DEFAULT_RESULT_SET_CONCURRENCY定义: 32 | 33 | ```java 34 | private static final int DEFAULT_RESULT_SET_CONCURRENCY = ResultSet.CONCUR_READ_ONLY; 35 | ``` 36 | 37 | prepareStatement方法的源码实现较长,这里分部分进行说明。 38 | 39 | # 线程安全 40 | 41 | prepareStatement方法所有的逻辑均在锁的保护下执行: 42 | 43 | ```java 44 | synchronized (getConnectionMutex()) { 45 | //... 46 | } 47 | ``` 48 | 49 | getConnectionMutex方法获得的其实就是连接对象本身,为什么要加锁呢,因为一个连接对象完全有可能在多个线程中被使用。 50 | 51 | # PrepareStatement创建 52 | 53 | 我们以客户端编译同时开启PrepareStatement缓存为例,ConnectionImpl.clientPrepareStatement方法相关源码: 54 | 55 | ```java 56 | if (getCachePreparedStatements()) { 57 | PreparedStatement.ParseInfo pStmtInfo = this.cachedPreparedStatementParams.get(nativeSql); 58 | if (pStmtInfo == null) { 59 | //反射创建PreparedStatement对象 60 | pStmt = com.mysql.jdbc.PreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database); 61 | this.cachedPreparedStatementParams.put(nativeSql, pStmt.getParseInfo()); 62 | } else { 63 | pStmt = new com.mysql.jdbc.PreparedStatement(getMultiHostSafeProxy(), nativeSql, this.database, pStmtInfo); 64 | } 65 | } 66 | ``` 67 | 68 | 核心的缓存数据结构cachedPreparedStatementParams其实就是一个继承自LinkedHashMap实现的LRU缓存,可以看出,缓存的并不是PreparedStatement,而是ParseInfo对象,缓存的key就是我们的SQL语句。 69 | 70 | ParseInfo代表着一条SQL语句在客户端"编译"的结果,对于SQL的编译的入口位于PreparedStatement的构造器: 71 | 72 | ```java 73 | this.parseInfo = new ParseInfo(sql, this.connection, this.dbmd, this.charEncoding, this.charConverter); 74 | ``` 75 | 76 | ParseInfo类在其构造器中完成对SQL的编译,其本身就是PreparedStatement的嵌套类,那么这里的编译指的是什么呢?我们将测试用的SQL语句稍作改造: 77 | 78 | ```java 79 | final String sql = "select * from student where name = ? and age = ?"; 80 | PreparedStatement ps = connection.prepareStatement(sql); 81 | ps.setString(1, "skywalker"); 82 | ps.setInt(2, 22); 83 | ``` 84 | 85 | ParseInfo内部有一个关键的属性: 86 | 87 | ```java 88 | byte[][] staticSql = null 89 | ``` 90 | 91 | 通过调试可以发现,解析之后此属性的值变成了: 92 | 93 | ![staticSql](images/parseinfo_staticsql.PNG) 94 | 95 | 可以推测: 在对PreparedStatement进行参数设置时,必定是在数组的各个元素之间插入,至于为什么要使用byte数组而不是String数组,猜测是为了便于后续利用网络进行传输。 96 | 97 | 所以这里可以得出结论: 对PreparedStatement的编译其实就是**将SQL语句按照占位符进行分割**,对ParseInfo进行缓存而不是PreparedStatement的原因便是**PreparedStatement必定要保存具体的参数值**。 98 | 99 | # 参数设置 100 | 101 | 我们以方法: 102 | 103 | ```java 104 | ps.setString(1, "skywalker"); 105 | ``` 106 | 107 | 为例,具体实现位于com.mysql.jdbc.PreparedStatement中。 108 | 109 | ## 字符串包装 110 | 111 | 对于字符串类型,驱动会对其用单引号包装,相应源码: 112 | 113 | ```java 114 | if (needsQuoted) { 115 | parameterAsBytes = StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charConverter, this.charEncoding, 116 | this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), getExceptionInterceptor()); 117 | } 118 | ``` 119 | 120 | ## 设置 121 | 122 | PreparedStatement.setInternal方法: 123 | 124 | ```java 125 | protected final void setInternal(int paramIndex, byte[] val) { 126 | synchronized (checkClosed().getConnectionMutex()) { 127 | int parameterIndexOffset = getParameterIndexOffset(); 128 | checkBounds(paramIndex, parameterIndexOffset); 129 | this.isStream[paramIndex - 1 + parameterIndexOffset] = false; 130 | this.isNull[paramIndex - 1 + parameterIndexOffset] = false; 131 | this.parameterStreams[paramIndex - 1 + parameterIndexOffset] = null; 132 | this.parameterValues[paramIndex - 1 + parameterIndexOffset] = val; 133 | } 134 | } 135 | ``` 136 | 137 | 从中我们可以看出几点: 138 | 139 | 1. PreparedStatement的setXXX方法的序号是从1开始的。 140 | 2. PreparedStatement允许我们以输入流/Reader的形式作为值传入。 141 | 3. parameterValues是一个byte二维数组。 142 | 143 | ## 参数类型保存 144 | 145 | PreparedStatement.setString相关源码: 146 | 147 | ```java 148 | this.parameterTypes[parameterIndex - 1 + getParameterIndexOffset()] = Types.VARCHAR; 149 | ``` 150 | 151 | parameterTypes是一个int数组,依次保存了所有参数的类型。 152 | 153 | # 查询 154 | 155 | 入口位于com.mysql.jdbc.PreparedStatement的executeQuery方法,依然是在加锁的情况下执行的。 156 | 157 | ## 流式查询 158 | 159 | ```java 160 | boolean doStreaming = createStreamingResultSet(); 161 | ``` 162 | 163 | 驱动支持流式的从数据库服务获得查询结果而不是一次性全部将结果取回,但默认是没有开启的: 164 | 165 | ```java 166 | protected boolean createStreamingResultSet() { 167 | synchronized (checkClosed().getConnectionMutex()) { 168 | return ((this.resultSetType == java.sql.ResultSet.TYPE_FORWARD_ONLY) && 169 | (this.resultSetConcurrency == java.sql.ResultSet.CONCUR_READ_ONLY) && (this.fetchSize == Integer.MIN_VALUE)); 170 | } 171 | } 172 | ``` 173 | 174 | TYPE_FORWARD_ONLY指结果集只能单向向后移动,CONCUR_READ_ONLY指结果集只读,不满足的是最后一个条件,默认情况下fetchSize为0,我们可以通过将参数defaultFetchSize设为int最小值以支持这一特性。 175 | 176 | ## 查询Packet创建 177 | 178 | fillSendPacket方法: 179 | 180 | ```java 181 | protected Buffer fillSendPacket() throws SQLException { 182 | synchronized (checkClosed().getConnectionMutex()) { 183 | return fillSendPacket(this.parameterValues, this.parameterStreams, this.isStream, this.streamLengths); 184 | } 185 | } 186 | ``` 187 | 188 | 所谓的Packet其实就是向数据库服务发送的一个byte数组,所以这里我们重点关注一下驱动是如何组织消息格式的。 189 | 190 | ### 缓冲区 191 | 192 | Packet的载体获取方式如下: 193 | 194 | ```java 195 | Buffer sendPacket = this.connection.getIO().getSharedSendPacket(); 196 | ``` 197 | 198 | Buffer是驱动自己实现的、基于byte数组的一个简单缓冲区,可以看出,**一个连接的所有查询操作(也可能含其它操作)公用一个缓冲区**,线程安全性由连接唯一的锁保证。 199 | 200 | ### 格式 201 | 202 | 对于查询来说,格式如下: 203 | 204 | ![查询格式](images/query_packet_pattern.PNG) 205 | 206 | query是一个单字节的指示位,定义如下: 207 | 208 | ```java 209 | static final int QUERY = 3; 210 | ``` 211 | 212 | 驱动允许我们为连接的所有statement指定一个通用/全局的注释,我们可以通过com.mysql.jdbc.Connection的setStatementComment方法进行设置,注意我们设置的应该仅仅包含注释,驱动会自动为我们用`/*`和`*/`(共6个字符,包括两个空格)包围。 213 | 214 | static sql和argument便印证了之前对预编译和参数设置行为的猜测。这部分源码如下: 215 | 216 | ```java 217 | for (int i = 0; i < batchedParameterStrings.length; i++) { 218 | sendPacket.writeBytesNoNull(this.staticSqlStrings[i]); 219 | if (batchedIsStream[i]) { 220 | streamToBytes(sendPacket, batchedParameterStreams[i], true, batchedStreamLengths[i], useStreamLengths); 221 | } else { 222 | sendPacket.writeBytesNoNull(batchedParameterStrings[i]); 223 | } 224 | } 225 | sendPacket.writeBytesNoNull(this.staticSqlStrings[batchedParameterStrings.length]); 226 | ``` 227 | 228 | batchedParameterStrings便是parameterValues二维数组。 229 | 230 | 注意,这里有一个细节: 231 | 232 | **驱动并未将参数的类型信息发送给数据库服务**,这一点可以从parameterTypes的定义上可以得到印证: 233 | 234 | ```java 235 | /** 236 | * Only used by statement interceptors at the moment to 237 | * provide introspection of bound values 238 | */ 239 | protected int[] parameterTypes = null; 240 | ``` 241 | 242 | 那么数据库服务如何得知类型信息呢?猜测: 243 | 244 | 服务端会对SQL进行语法分析,必然可以结合表定义得到每个字段的类型信息,然后Mysql会对参数byte数组进行转换,转换失败也就报错了。 245 | 246 | ## 交互 247 | 248 | 与数据库服务交互的核心逻辑位于MysqlIO的sqlQueryDirect方法,简略版源码: 249 | 250 | ```java 251 | final ResultSetInternalMethods sqlQueryDirect(StatementImpl callingStatement, String query, 252 | String characterEncoding, Buffer queryPacket, int maxRows, 253 | int resultSetType, int resultSetConcurrency, boolean streamResults, String catalog, Field[] cachedMetadata) { 254 | 255 | //前置拦截方法调用 256 | if (this.statementInterceptors != null) { 257 | ResultSetInternalMethods interceptedResults = invokeStatementInterceptorsPre(query, callingStatement, false); 258 | //拦截器返回结果不为null,不进行实际的查询 259 | if (interceptedResults != null) { 260 | return interceptedResults; 261 | } 262 | } 263 | Buffer resultPacket = sendCommand(MysqlDefs.QUERY, null, queryPacket, false, null, 0); 264 | ResultSetInternalMethods rs = readAllResults(callingStatement, maxRows, resultSetType, resultSetConcurrency, 265 | streamResults, catalog, resultPacket, false, -1L, cachedMetadata); 266 | 267 | //拦截器后置方法调用 268 | if (this.statementInterceptors != null) { 269 | ResultSetInternalMethods interceptedResults = invokeStatementInterceptorsPost(query, callingStatement, rs, false, null); 270 | if (interceptedResults != null) { 271 | rs = interceptedResults; 272 | } 273 | } 274 | return rs; 275 | } 276 | ``` 277 | 278 | ### 结果集读取 279 | 280 | 核心逻辑位于MysqlIO的readResultsForQueryOrUpdate方法,简略版源码: 281 | 282 | ```java 283 | protected final ResultSetImpl readResultsForQueryOrUpdate(StatementImpl callingStatement, 284 | int maxRows, int resultSetType, int resultSetConcurrency, 285 | boolean streamResults, String catalog, Buffer resultPacket, boolean isBinaryEncoded, 286 | long preSentColumnCount, Field[] metadataFromCache) { 287 | //栏位数 288 | long columnCount = resultPacket.readFieldLength(); 289 | if (columnCount == 0) { 290 | return buildResultSetWithUpdates(callingStatement, resultPacket); 291 | } else if (columnCount == Buffer.NULL_LENGTH) { 292 | //... 293 | } else { 294 | com.mysql.jdbc.ResultSetImpl results = getResultSet(callingStatement, columnCount, 295 | maxRows, resultSetType, resultSetConcurrency, streamResults, catalog, isBinaryEncoded, metadataFromCache); 296 | return results; 297 | } 298 | } 299 | ``` 300 | 301 | #### 栏位数/字段长度 302 | 303 | readFieldLength方法用于读取返回一个字段的长度: 304 | 305 | ```java 306 | final long readFieldLength() { 307 | int sw = this.byteBuffer[this.position++] & 0xff; 308 | switch (sw) { 309 | case 251: 310 | return NULL_LENGTH; 311 | case 252: 312 | return readInt(); 313 | case 253: 314 | return readLongInt(); 315 | case 254: 316 | return readLongLong(); 317 | default: 318 | return sw; 319 | } 320 | } 321 | ``` 322 | 323 | 这里使用了一个小小的优化策略: 采用不定长的字节数存储,当数值较小时,一个字节就够了,这样可以降低网络带宽的占用。对于我们测试用的student表,有id、name和age三个字段,所以返回3. 324 | 325 | #### 格式 326 | 327 | 包含栏位数,最终返回的byte数组的格式大致如下: 328 | 329 | ![结果格式](images/query_result_pattern.PNG) 330 | 331 | **栏位值的个数应该是栏位信息数的整数倍**,两者的比值应该就是结果集的行数,这里只是猜测,没有经过源码上的验证。 332 | 333 | 其中栏位信息又包括多个字段(栏位的描述信息,个数是是固定的),每个字段的格式如下: 334 | 335 | ![字段格式](images/query_result_field_pattern.PNG) 336 | 337 | 主要字段及其意义如下: 338 | 339 | 1. catalogName 340 | 2. databaseName 341 | 3. tableName 342 | 4. name 343 | 5. colLength 344 | 6. colType 345 | 7. colFlag 346 | 8. colDecimals 347 | 348 | 分别表示数据库名、表明、字段名,字段类型等信息。 349 | 350 | # 超时 351 | 352 | 这部分是受《亿级流量》一书的启发,即:**MySQL会为每一个连接创建一个Timer线程用于SQL执行超时控制**。下面来从源码的角度进行证实。 353 | 354 | 关键点位于PreparedStatement的executeInternal方法,相关源码如下: 355 | 356 | ```java 357 | protected ResultSetInternalMethods executeInternal(...) { 358 | if (locallyScopedConnection.getEnableQueryTimeouts() 359 | && this.timeoutInMillis != 0 360 | && locallyScopedConnection.versionMeetsMinimum(5, 0, 0) 361 | ) { 362 | timeoutTask = new CancelTask(this); 363 | locallyScopedConnection 364 | .getCancelTimer().schedule(timeoutTask, this.timeoutInMillis); 365 | } 366 | } 367 | ``` 368 | 369 | 可以看出,启用超时检测的前提条件是我们开启了查询超时机制并且超时时间不为零。查询超时机制是默认启用的,超时时间需要我们手动去设置。 370 | 371 | locallyScopedConnection其实就是当前的数据库连接,下面看一下getCancelTimer方法实现: 372 | 373 | ```java 374 | public Timer getCancelTimer() { 375 | synchronized (getConnectionMutex()) { 376 | if (this.cancelTimer == null) { 377 | this.cancelTimer = new Timer(true); 378 | } 379 | return this.cancelTimer; 380 | } 381 | } 382 | ``` 383 | 384 | 从这里可以看出,如果我们的数据库连接池设置的最大连接数过大,在极限的情况下,连接池将会至少占用 385 | 386 | 连接数 * 1MB的内存,记住这一点。 387 | 388 | ## 两种超时 389 | 390 | 还有一个有意思的问题,MySQL的queryTimeout和socketTimeout究竟有什么区别? 391 | 392 | 参考这篇美团的文章: 393 | 394 | [深入分析JDBC超时机制](https://blog.csdn.net/a837199685/article/details/75796891) 395 | 396 | 简而言之,最好这两个超时都设置一下,**并且queryTimeout应比socketTimeout要小**。 397 | 398 | 399 | 400 | 401 | 402 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | skywalker 8 | mysql-driver 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | 14 | org.apache.maven.plugins 15 | maven-compiler-plugin 16 | 17 | 1.8 18 | 1.8 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | mysql 27 | mysql-connector-java 28 | 5.1.35 29 | 30 | 31 | 32 | junit 33 | junit 34 | 4.12 35 | test 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/test/java/client/Client.java: -------------------------------------------------------------------------------- 1 | package client; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import java.sql.*; 8 | 9 | /** 10 | * Mysql客户端. 11 | * 12 | * @author skywalker 13 | */ 14 | public class Client { 15 | 16 | private Connection connection; 17 | 18 | @Before 19 | public void init() throws ClassNotFoundException, SQLException { 20 | Class.forName("com.mysql.jdbc.Driver"); 21 | connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?cachePrepStmts=true&useServerPrepStmts=false", "root", "1234"); 22 | } 23 | 24 | @Test 25 | public void query() throws SQLException { 26 | final String sql = "select * from student where name = ? and age = ?"; 27 | PreparedStatement ps = connection.prepareStatement(sql); 28 | ps.setString(1, "skywalker"); 29 | ps.setInt(2, 22); 30 | ResultSet rs = ps.executeQuery(); 31 | while (rs.next()) { 32 | System.out.println("User: " + rs.getString("name") + "."); 33 | } 34 | } 35 | 36 | @After 37 | public void close() { 38 | if (connection != null) { 39 | try { 40 | connection.close(); 41 | } catch (SQLException ignore) { 42 | } 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/something/Something.java: -------------------------------------------------------------------------------- 1 | package something; 2 | 3 | import org.junit.Test; 4 | 5 | import java.net.InetAddress; 6 | import java.net.UnknownHostException; 7 | import java.util.Arrays; 8 | 9 | /** 10 | * Test something, test anything... 11 | * 12 | * @author skywalker. 13 | */ 14 | public class Something { 15 | 16 | /** 17 | * 测试{@link java.net.InetAddress#getAllByName(String)}. 18 | */ 19 | @Test 20 | public void allNames() throws UnknownHostException { 21 | InetAddress[] addresses = InetAddress.getAllByName("localhost"); 22 | System.out.println(Arrays.toString(addresses)); 23 | } 24 | 25 | } 26 | --------------------------------------------------------------------------------