前言
先前寫過一篇文章聊聊如何利用redis實現多級快取同步,裡面講到業務部門因資料庫當機,有技術提出當資料庫當機,切換到redis,今天我們就來聊聊如何觸發這個切換動作?
1、方案一:利用異常機制
虛擬碼如下:
首先這個方案是不可行的,因為每次請求,還是先走到資料庫邏輯,然後等丟擲異常,這個時間會挺長的,業務上是無法接受的
2、方案二:主動進行mysql探活
實現思路: 可以利用資料庫連線池檢測有效連線的思路
實現方案
1、方式一:利用druid連線池的ValidConnectionChecker進行擴充套件
核心邏輯如下
@Slf4j
public class MysqlConnectionCheck extends MySqlValidConnectionChecker {
private ApplicationContext applicationContext;
public MysqlConnectionCheck(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public boolean isValidConnection(Connection conn, String validateQuery, int validationQueryTimeout) throws Exception {
return checkMySQLCommunications(conn,validateQuery,validationQueryTimeout);
}
private boolean checkMySQLCommunications (Connection conn, String validateQuery, int validationQueryTimeout) {
boolean validConnection = false;
try {
validConnection = super.isValidConnection(conn, validateQuery, validationQueryTimeout);
} catch (Exception e) {
}
if(validConnection){
boolean b = MySQLCommunicationsHolder.isMySQLCommunicationsException.compareAndSet(true, false);
if(b){
CommunicationsHealthEvent event = CommunicationsHealthEvent.builder().conn(conn).build();
applicationContext.publishEvent(event);
}
}
return validConnection;
}
}
在yml配置我們自定義的檢測器
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: ${DRIVER_CALSS_NAME:com.mysql.cj.jdbc.Driver}
url: ${DATASOURCE_URL:jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai}
username: ${DATASOURCE_USERNAME:root}
password: ${DATASOURCE_PWD:123456}
druid:
# 指定連線的有效檢查類
valid-connection-checker-class-name: com.github.lybgeek.db.check.test.user.check.MysqlConnectionCheck
這個方案也是不大行的,後面翻了一下druid原始碼。當資料庫丟擲不可恢復的異常時,比如網路抖動,異常斷開,druid會觸發exceptionSorter,拋棄連線。而CreateConnectionThread會檢測是否需要建立連線,如果不需要,他就會進行等待。當連線不夠時,會呼叫
com.alibaba.druid.pool.DruidAbstractDataSource#createPhysicalConnection()
進行建立,同時isValidConnection也是在這個方法裡面進行連線驗證,但這邊就有問題,就是當資料庫當機了,就建立不了連線,因此就進入異常流程,isValidConnection是沒法執行的
2、方式二:參考druid的檢測連線邏輯,額外編寫定時器觸發檢測邏輯
核心程式碼塊:
public class ValidConnectionCheckerAdapter implements ValidConnectionChecker {
private DbCheckProperies dbCheckProperies;
public ValidConnectionCheckerAdapter(DbCheckProperies dbCheckProperies) {
this.dbCheckProperies = dbCheckProperies;
}
@Override
public boolean isValidConnection(Connection conn, String query, int validationQueryTimeout) throws SQLException {
boolean valid = checkConnection(conn, query, validationQueryTimeout);
// unexcepted branch
if (valid && isMysql()) {
long lastPacketReceivedTimeMs = MySqlUtils.getLastPacketReceivedTimeMs(conn);
if (lastPacketReceivedTimeMs > 0) {
long mysqlIdleMillis = System.currentTimeMillis() - lastPacketReceivedTimeMs;
if (lastPacketReceivedTimeMs > 0
&& mysqlIdleMillis >= dbCheckProperies.getTimeBetweenEvictionRunsMillis()) {
return false;
}
}
}
return valid;
}
private boolean checkConnection(Connection conn, String query, int validationQueryTimeout) throws SQLException {
if (query == null || query.length() == 0) {
return true;
}
if(conn == null){
return false;
}
Statement stmt = null;
ResultSet rs = null;
try {
stmt = conn.createStatement();
if (validationQueryTimeout > 0) {
stmt.setQueryTimeout(validationQueryTimeout);
}
rs = stmt.executeQuery(query);
return true;
} finally {
JdbcUtils.close(rs);
JdbcUtils.close(stmt);
}
}
@Override
public void afterPropertiesSet() throws Exception {
executorService.scheduleWithFixedDelay(new DbCheckTask(),0,dbCheckProperies.getTimeBetweenEvictionRunsMillis(), TimeUnit.MILLISECONDS);
}
private class DbCheckTask implements Runnable{
@Override
public void run() {
SQLException sqlException = null;
Connection conn = dbConnManger.getConn();
try {
boolean validConnection = validConnectionChecker.isValidConnection(conn, dbCheckProperies.getValidationQuery(), dbCheckProperies.getValidationQueryTimeout());
if(validConnection){
boolean b = MySQLCommunicationsHolder.isMySQLCommunicationsException.compareAndSet(true, false);
if(b){
CommunicationsHealthEvent event = CommunicationsHealthEvent.builder().conn(conn).build();
applicationContext.publishEvent(event);
}
}else{
sqlException = new SQLException("connection is invalid","10040");
}
} catch (SQLException e) {
log.error("{}",e);
sqlException = e;
dbConnManger.closeConnection();
conn = null;
}
if(sqlException != null){
MySQLCommunicationsHolder.isMySQLCommunicationsException.compareAndSet(false, true);
CommunicationsUnHealthEvent event = CommunicationsUnHealthEvent.builder().sqlException(sqlException).build();
applicationContext.publishEvent(event);
}
}
}
總結
其實mysql的探活實現方式有很多種,本文的實現檢測邏輯是直接套用druid的檢測連線邏輯,之前對druid的使用,基本上就是停留在配置上,沒過多關注。
為了寫這篇文章,特地翻了一下druid的原始碼,主要是因為之前以為擴充套件ValidConnectionChecker就行了,後面發現行不通。就看了一下原始碼,發現druid的設計思路挺好的,有些實現思想是我們在日常開發中,可以借鑑使用的。還有druid裡面有些跟資料庫相關的util,也是可以直接拿過來用的。
demo連結
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-db-check