Java連接proxysql管理端口
爲什麼不使用JDBC
總所周知,proxysql管理端口實現了mysql協議,可以使用mysql client連接並使用類似SQL的語法查詢和修改配置。很多人基於此就大膽地說proxysql管理端口可以使用諸如navicat等工具去連接並管理,我試過之後嚴重懷疑他們並沒有真的用naviacat操作過,因爲根本就無法執行sql,只能嘗試連接而已:
確實可連接,但是操作就別想了,因爲任何展示和執行的操作都會報錯[Err] 1045 - ProxySQL Admin Error: near "SHOW": syntax error
,問題很明顯:navicat做了一些額外操作,除了執行正真的sql,還會執行額外的sql來查詢一些展示需要的信息,但是proxysql admin是基於sqlite的,很多語法都不支持。
同樣地,如果使用JDBC連接proxysql admin,一樣會報錯:[Err] 1045 - ProxySQL Admin Error: near "@": syntax error
,因爲JDBC會額外查詢一些系統變量(比如SELECT @@report_host
),這種語法一樣也不是sqllite支持的,導致JDBC createConnection的時候報錯。
解決方案
如果有一個純的java mysql client就能解決這個問題了,它的功能只需要像mysql shell那樣簡單,不要任何額外的增強功能。自己google了一陣兒,github也搜索過一陣兒,但是都不理想。最後一個同事推薦了vertx社區使用的異步非阻塞式mysql client——jasync-sql,試用了一下效果很好。
代碼示例:proxysqlclient
package github.clyoudu.proxysqlclient;
import com.alibaba.fastjson.JSON;
import com.github.jasync.sql.db.Connection;
import com.github.jasync.sql.db.QueryResult;
import com.github.jasync.sql.db.ResultSet;
import com.github.jasync.sql.db.RowData;
import com.github.jasync.sql.db.mysql.MySQLConnectionBuilder;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Create by IntelliJ IDEA
*
* @author chenlei
* @dateTime 2019/7/26 17:39
* @description ProxysqlClient
*/
public class ProxysqlClient {
private final static Logger log = LoggerFactory.getLogger(ProxysqlClient.class);
/**
* mysql 默認連接的schema
* proxysql 雖然沒有這個schema,但並不會報錯
*/
public static final String DEFAULT_SCHEMA = "information_schema";
private static final String EXECUTE_SQL_ERROR = "執行sql[%s]異常";
private static final String EXECUTE_SQL_TIMEOUT = "執行sql[%s]超時";
/**
* 禁止用new實例化
*/
private ProxysqlClient() {
}
/**
* 執行proxysql語句,但不需要執行結果
* 可以多條語句順序執行,所有語句在同一個事務中執行
* 注意:proxysql不支持事務,但並不會報錯
* 測試通過:mysql、proxysql
*
* @param ip 必填,服務IP
* @param port 必填,服務端口
* @param database 選填,數據庫名稱,默認 information_schema
* @param user 必填,用戶名
* @param pass 必填,密碼
* @param sqlList 必填,待執行的sql列表
* @param timeout 選填,超時時間,單位s,默認不設超時時間
* @param transaction 是否開啓事務
* @return 成功or失敗
* @throws ProxysqlException 所有本方法可處理的異常都包裝成 github.clyoudu.proxysqlclient.ProxysqlException,外層可單獨catch此異常做處理
*/
public static boolean execute(String ip, Integer port, String database, String user, String pass, List<String> sqlList, Integer timeout, boolean transaction) {
//參數檢查
if (StringUtils.isBlank(ip) || port == null || StringUtils.isBlank(user) || StringUtils.isBlank(pass)) {
throw new ProxysqlException("參數錯誤:ip/port/user/pass不能爲空");
}
//默認連接information_schema
if (StringUtils.isBlank(database)) {
database = DEFAULT_SCHEMA;
}
if (sqlList == null || sqlList.isEmpty()) {
throw new ProxysqlException("執行sql爲空");
}
Connection connection = MySQLConnectionBuilder.createConnectionPool(
String.format("jdbc:mysql://%s:%s/%s?user=%s&password=%s", ip, port, database, user, pass)
);
CompletableFuture<QueryResult> result = null;
if (transaction) {
result = connection.inTransaction(c -> {
CompletableFuture<QueryResult> future = null;
for (String sql : sqlList) {
if (future == null) {
future = c.sendQuery(sql);
} else {
future = future.thenCompose(r -> c.sendQuery(sql));
}
}
return future;
});
} else {
for (String sql : sqlList) {
if (result == null) {
result = connection.sendQuery(sql);
} else {
result = result.thenCompose(r -> connection.sendQuery(sql));
}
}
}
if (result == null) {
log.error("執行sql失敗!");
return false;
}
try {
if (timeout != null && timeout > 0) {
result.get(timeout, TimeUnit.SECONDS);
} else {
result.get();
}
} catch (InterruptedException e) {
log.error(String.format(EXECUTE_SQL_ERROR, StringUtils.join(sqlList, ";")), e);
Thread.currentThread().interrupt();
throw new ProxysqlException(String.format(EXECUTE_SQL_ERROR, StringUtils.join(sqlList, ";")) + ": InterruptedException");
} catch (ExecutionException e) {
log.error(String.format(EXECUTE_SQL_ERROR, StringUtils.join(sqlList, ";")), e);
throw new ProxysqlException(String.format(EXECUTE_SQL_ERROR, StringUtils.join(sqlList, ";")) + ": " + e.getMessage());
} catch (TimeoutException e) {
log.error(String.format(EXECUTE_SQL_ERROR, StringUtils.join(sqlList, ";")), e);
throw new ProxysqlException(String.format(EXECUTE_SQL_TIMEOUT, StringUtils.join(sqlList, ";")) + ":" + timeout + "s");
}
connection.disconnect();
return true;
}
/**
* 執行proxysql語句,返回執行結果
*
* @param ip 必填,服務IP
* @param port 必填,服務端口
* @param database 選填,數據庫名稱,默認 information_schema
* @param user 必填,用戶名
* @param pass 必填,密碼
* @param sqlString 必填,待執行的sql
* @param timeout 選填,超時時間,單位s,默認不設超時時間
* @return List<Map> 所有字段名均爲數據庫原始字段名
* @throws ProxysqlException 所有本方法可處理的異常都包裝成 github.clyoudu.proxysqlclient.ProxysqlException,外層可單獨catch此異常做處理
*/
public static List<Map<String, Object>> executeQuery(String ip, Integer port, String database, String user, String pass, String sqlString, Integer timeout) {
//參數檢查
if (StringUtils.isBlank(ip) || port == null || StringUtils.isBlank(user) || StringUtils.isBlank(pass)) {
throw new ProxysqlException("參數錯誤:ip/port/user/pass不能爲空");
}
//默認連接information_schema
if (StringUtils.isBlank(database)) {
database = DEFAULT_SCHEMA;
}
if (StringUtils.isBlank(sqlString)) {
throw new ProxysqlException("執行sql爲空");
}
Connection connection = MySQLConnectionBuilder.createConnectionPool(
String.format("jdbc:mysql://%s:%s/%s?user=%s&password=%s", ip, port, database, user, pass)
);
CompletableFuture<QueryResult> future = connection.sendQuery(sqlString);
QueryResult queryResult;
try {
if (timeout == null || timeout <= 0) {
queryResult = future.get();
} else {
queryResult = future.get(timeout, TimeUnit.SECONDS);
}
} catch (InterruptedException e) {
log.error(String.format(EXECUTE_SQL_ERROR, sqlString), e);
Thread.currentThread().interrupt();
throw new ProxysqlException(String.format(EXECUTE_SQL_ERROR, sqlString) + ": InterruptedException");
} catch (ExecutionException e) {
log.error(String.format(EXECUTE_SQL_ERROR, sqlString), e);
throw new ProxysqlException(String.format(EXECUTE_SQL_ERROR, sqlString) + ": " + e.getMessage());
} catch (TimeoutException e) {
log.error(String.format(EXECUTE_SQL_ERROR, sqlString), e);
throw new ProxysqlException(String.format(EXECUTE_SQL_TIMEOUT, sqlString) + ":" + timeout + "s");
}
ResultSet resultSet = queryResult.getRows();
List<String> names = resultSet.columnNames();
List<Map<String, Object>> resultList = new ArrayList<>();
for (RowData rowData : resultSet) {
Map<String, Object> map = new HashMap<>(16);
for (String name : names) {
map.put(name, rowData.get(name));
}
resultList.add(map);
}
connection.disconnect();
return resultList;
}
public static void main(String[] args) {
log.info(JSON.toJSONString(executeQuery("localhost", 3306, "mysql", "root", "root",
"select * from user",
null)));
}
}
使用方式:
public static void main(String[] args) {
//select
log.info(JSON.toJSONString(executeQuery("localhost", 6032, "main", "root", "root",
"select * from mysql_users",
null)));
//ddl & batch execute
log.info(JSON.toJSONString(execute("localhost", 6032, "main", "root", "root",
Arrays.asList("INSERT INTO mysql_users(username,password,default_hostgroup) VALUES ('app','pass',2)", "load mysql user to runtime", "save mysql user to disk"),
null, false)));
}
當然,jasync-sql不僅支持proxysql,任何符合mysql協議的server都可以使用jasync-sql作爲client,因爲它沒有任何額外的增強。
支持批量執行,支持事務。