美文网首页程序员
聊聊业务项目如何主动感知mysql是否存活

聊聊业务项目如何主动感知mysql是否存活

作者: linyb极客之路 | 来源:发表于2023-01-02 10:50 被阅读0次

    前言

    先前写过一篇文章聊聊如何利用redis实现多级缓存同步,里面讲到业务部门因数据库宕机,有技术提出当数据库宕机,切换到redis,今天我们就来聊聊如何触发这个切换动作?

    1、方案一:利用异常机制

    伪代码如下:

    image

    首先这个方案是不可行的,因为每次请求,还是先走到数据库逻辑,然后等抛出异常,这个时间会挺长的,业务上是无法接受的

    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

    相关文章

      网友评论

        本文标题:聊聊业务项目如何主动感知mysql是否存活

        本文链接:https://www.haomeiwen.com/subject/ufbhcdtx.html