首先看一段代码:
try (Connection connection = DriverManager.getConnection("jdbc:mysql://remote01:3306/test");) {
ExecutorService executor = ThreadUtil.newExecutor();
for (int i = 0; i < 1000; i++) {
int finalI = i;
// 使用线程池,并发执行下面语句
executor.submit(() -> {
try {
// 通过连接创建 Statement
PreparedStatement statement = connection.prepareStatement("select * from user limit ?,100");
statement.setInt(1, finalI * 100);
// 执行查询操作
statement.execute();
// 获取返回结果
ResultSet resultSet = statement.getResultSet();
while (resultSet.next()) {
System.out.println(resultSet.getString("name"));
}
} catch (SQLException e) {
e.printStackTrace();
}
});
}
} catch (SQLException e) {
e.printStackTrace();
}
上面这段代码中,出现了线程池中多个线程共用一个 SQL 连接 的现象。一般我们会使用一些连接池来管理 SQL 连接
,而这些连接池通常会为每个线程单独分配一个连接,来确保线程安全的问题。也就是说,如果出现了多个线程共用一个 Sql 连接,正常理解来说会出现线程安全问题,那么真的是这样的吗?
ExecutorService executor = ThreadUtil.newExecutor();
for (int i = 0; i < 1000; i++) {
// 使用线程池,并发执行下面语句
final int finalI = i;
executor.submit(() -> {
List<Map<String, Object>> resultSet = jdbcTemplate.queryForList("select * from user limit ?,100", finalI * 100);
// 获取返回结果
resultSet.forEach(res -> System.out.println(res.get("name")));
});
}
上面这段代码使用了 jdbcTemplate
来执行 SQL 语句,这种情况下SQL 的连接管理就统一交由连接池去做了,jdbcTempalte 会在执行SQL前从连接池中拿到一个SQL 连接去执行SQL,连接池会根据当前线程名称为我们分配一个固定的SQL连接,确保一个线程对应一个SQL连接,而 jdbcTemplate 会在SQL执行完成后会将SQL连接放回至连接池中以备稍后使用,而不是直接关掉。从而提高SQL连接的利用率。
结论
非事务场景
Connection 没有开启事务的情况下,如单纯的 Select 查询语句,或开启了 AUTO_COMMIT
,是线程安全的。但是由于内部使用了 (synchronized),同步锁,在多线程访问一个 Connection 时,会导致执行请求的串行化。
事务场景
如果在开启事务的情况下,会导致多个线程共用一个事务,也就是说如果其中一个线程对事务进行了提交或者回滚操作,会同时影响其他线程执行的SQL。
上代码
从结论可以得到,SQL连接虽然总体上非线程安全,但是在非事务场景中还是保证了线程安全的,那么 Connection
是怎么保证这一点的呢?
通常我们使用 Connection
来执行查询,通过Connection
获取一个 Statement
对象 ,然后调用Statement
中的 execute 方法来执行真正的 sql。我们可以通过解析 Statement
实现中的 execute
方法来得到 SQL 执行的具体逻辑。
// com.mysql.jdbc.StatementImpl#execute(java.lang.String, boolean)
private boolean execute(String sql, boolean returnGeneratedKeys) throws SQLException {
MySQLConnection locallyScopedConn = checkClosed();
// 使用同步代码块对单次查询加锁
synchronized (locallyScopedConn.getConnectionMutex()) {
// 重置并且检查当前连接的状态
this.retrieveGeneratedKeys = returnGeneratedKeys;
lastQueryIsOnDupKeyUpdate = false;
if (returnGeneratedKeys)
lastQueryIsOnDupKeyUpdate = containsOnDuplicateKeyInString(sql);
resetCancelledState();
checkNullOrEmptyQuery(sql);
checkClosed();
char firstNonWsChar = StringUtils.firstAlphaCharUc(sql, findStartOfStatement(sql));
boolean isSelect = true;
if (firstNonWsChar != 'S') {
// 检查状态
}
boolean doStreaming = createStreamingResultSet();
try {
// 执行查询,并且处理返回结果
...
} finally {
this.statementExecuting.set(false);
}
}
}
上面是 mysql 驱动中 Statement 执行具体 SQL 的逻辑实现,可以看到在执行开始,就通过当前连接设置了一个同步代码块。这样的话,当前 Connection 所创建的所有 Statement 同时执行 execute 方法时,实际是串行执行的。
// 提交多线程并发执行任务
executor.submit(() -> {
try {
// 每个线程执行时会单独创建一个 Statement
PreparedStatement statement = connection.prepareStatement("select * from user limit ?,100");
statement.setInt(1, finalI * 100);
// 执行该方法时,由于 execute 方法中的同步代码块
// 实际查询在并发场景中是串行执行的
statement.execute();
ResultSet resultSet = statement.getResultSet();
while (resultSet.next()) {
System.out.println(resultSet.getString("name"));
}
} catch (SQLException e) {
e.printStackTrace();
}
});
}
现在我们了解了,单个连接创建的多个SQL查询,实际是通过代码块的方式实现了线程安全,也就是在单个连接在多线程并发执行查询的场景下,不同的查询之间是互不干扰的。但由于同步代码块的存在,多个并发查询实际是串行执行的,所以使用单个连接并发查询会导致最终查询的效果还是串行的,并没有因为使用并发而提高效率。
事务场景
当然上面的分析是基于非事务场景下,查询之间是没有联系的。或者我们开启了自动提交(AUTO_COMMIT),这样每个SQL之间的操作也是互不关联的。但是在事务场景中,如果多个线程共用一个 SQL 连接去执行 SQL 的话,会导致多个线程共用同一个事务,如果某个线程回滚了一个事务,则会导致会将其他线程在事务期间执行的SQL一起给回滚了,而其他线程却对此一无所知,最终导致数据不一致。
如上图,线程A 开启了一个事务并执行相应的SQL操作。此时线程B共用同一个连接执行SQL的话,线程B是不知道它此时正处于线程A的事务当中,这样当线程A在请求回滚事务时,MySQL 会将线程B 在事务过程中提交的SQL一起给回滚掉。但所有的操作线程B 是无感知的。