MyBatis使用Zookeeper保存数据库的配置可动态刷新的实现代码

核心关键点: 封装一个DataSource, 重写 getConnection 就可以实现

我们一步一步来看.

环境: Spring Cloud + MyBatis

MyBatis常规方式下配置数据源: 使用Spring的Configuration

package com.cnscud.cavedemo.fundmain.config;

import com.cnscud.xpower.dbn.SimpleDBNDataSourceFactory;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

/**
 * Database Config 多数据源配置: 主数据源.
 *
 * @author Felix Zhang 2021-08-02 17:30
 * @version 1.0.0
 */
@Configuration
@MapperScan(basePackages = {"com.cnscud.cavedemo.fundmain.dao"},
        sqlSessionFactoryRef = "sqlSessionFactoryMainDataSource")
public class MainDataSourceConfig {

    //常规配置: 使用application.yml里面的配置.
    @Primary
    @Bean(name = "mainDataSource")
    @ConfigurationProperties("spring.datasource.main")
    public DataSource mainDataSource() throws Exception {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "sqlSessionFactoryMainDataSource")
    public SqlSessionFactory sqlSessionFactoryMainDataSource(@Qualifier("mainDataSource") DataSource mainDataSource) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        //org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        //configuration.setMapUnderscoreToCamelCase(true);
        //factoryBean.setConfiguration(configuration);
        factoryBean.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));

        // 使用mainDataSource数据源, 连接mainDataSource库
        factoryBean.setDataSource(mainDataSource);

        //下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加
        //指定entity和mapper xml的路径
        //factoryBean.setTypeAliasesPackage("com.cnscud.cavedemo.fundmain.model");
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:com/cnscud/cavedemo/fundmain/mapper/*.xml"));
        return factoryBean.getObject();
    }

    @Primary
    @Bean
    public SqlSessionTemplate sqlSessionTemplateMainDataSource(@Qualifier("sqlSessionFactoryMainDataSource") SqlSessionFactory sqlSessionTemplateMainDataSource) throws Exception {

        //使用注解中配置的Factory
        return new SqlSessionTemplate(sqlSessionTemplateMainDataSource);
    }

    @Primary
    @Bean
    public PlatformTransactionManager mainTransactionManager(@Qualifier("mainDataSource") DataSource prodDataSource) {
        return new DataSourceTransactionManager(prodDataSource);
    }
}

这里面获取数据源的关键函数是 mainDataSource, 我们自己来实现就好了:

因为这个是一次性的工作, 所以我们无法修改DataSource的指向, 只能在DataSource内部做文章, 所以我们需要自己实现一个DataSource.

其中的步骤比较多, 我们来看看最终结果:

最终的DataSourceWrapper

它完全封装了一个DataSource, 自己并没有任何DataSource的功能:

package com.cnscud.xpower.dbn;

import javax.sql.DataSource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

/**
 * Datasource wrapper, 为了方便动态创建DataSource.
 *
 * @author Felix Zhang 2021-08-05 14:14
 * @version 1.0.0
 */
public class DynamicByZookeeperDataSourceWrapper implements DataSource {

    protected SimpleDBNConnectionPool simpleDBNConnectionPool;
    protected String bizName;

    public DynamicByZookeeperDataSourceWrapper(SimpleDBNConnectionPool simpleDBNConnectionPool, String bizName) {
        this.simpleDBNConnectionPool = simpleDBNConnectionPool;
        this.bizName = bizName;
    }

    protected DataSource pickDataSource() throws SQLException{
        return simpleDBNConnectionPool.getDataSource(bizName);
    }

    @Override
    public Connection getConnection() throws SQLException {
        return pickDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return pickDataSource().getConnection(username, password);
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return pickDataSource().unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return pickDataSource().isWrapperFor(iface);
    }

    @Override
    public PrintWriter getLogWriter() throws SQLException {
        return pickDataSource().getLogWriter();
    }

    @Override
    public void setLogWriter(PrintWriter out) throws SQLException {
        pickDataSource().setLogWriter(out);
    }

    @Override
    public void setLoginTimeout(int seconds) throws SQLException {
        pickDataSource().setLoginTimeout(seconds);
    }

    @Override
    public int getLoginTimeout() throws SQLException {
        return pickDataSource().getLoginTimeout();
    }

    @Override
    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
        throw new SQLFeatureNotSupportedException();
    }
}

SimpleDBNConnectionPool

支持多个数据源的暂存池, 可以根据name获取不同的数据库DataSource实例:

这个类负责创建DataSource, 保存在Map里. 并且能监听Zookeeper的变化, 一旦侦听到变化, 就close现有的DataSource.

package com.cnscud.xpower.dbn;

import com.github.zkclient.IZkDataListener;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import static java.lang.String.format;

/**
 * The simple datasource pool.
 *
 * 根据名字存放多个数据库的DataSource, 并且会监听Zookeeper配置, 动态重建.
 *
 * @author adyliu (imxylz@gmail.com)
 * @since 2011-7-27
 */
public class SimpleDBNConnectionPool {

    final Logger logger = LoggerFactory.getLogger(getClass());

    private Map<String, DataSource> instances = new ConcurrentHashMap<>();
    private final Set<String> watcherSchema = new HashSet<String>();

    public DataSource getInstance(String bizName) {
        try {
            return findDbInstance(bizName);
        }
        catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }

    public Connection getConnection(String bizName) throws SQLException {
        DataSource ds = getDataSource(bizName);
        return ds.getConnection();
    }

    public DataSource getDataSource(String bizName) throws SQLException {
        return findDbInstance(bizName);
    }

    protected void destroyInstance(final String bizName) {
        synchronized (instances) {
            DataSource oldInstanceIf = instances.remove(bizName);
            logger.warn(format("destoryInstance %s and %s", bizName, oldInstanceIf != null ? "close datasource" : "do nothing"));
            if (oldInstanceIf != null) {
                closeDataSource(oldInstanceIf);
            }
        }
    }

    protected void closeDataSource(DataSource ds) {
        if (ds instanceof HikariDataSource) {
            try {
                ((HikariDataSource) ds).close();
            }
            catch (Exception e) {
                logger.error("Close datasource failed. ", e);
            }
        }
    }

    private DataSource createInstance(Map<String, String> dbcfg) {
        return new SimpleDataSourceBuilder().buildDataSource(dbcfg);
    }

    private DataSource findDbInstance(final String bizName) throws SQLException {
        DataSource ins = instances.get(bizName);
        if (ins != null) {
            return ins;
        }
        synchronized (instances) {// 同步操作
            ins = instances.get(bizName);
            if (ins != null) {
                return ins;
            }
            boolean success = false;
            try {
                Map<String, String> dbcfg = SchemeNodeHelper.getInstance(bizName);
                if (dbcfg == null) {
                    throw new SQLException("No such datasouce: " + bizName);
                }
                ins = createInstance(dbcfg);
                //log.warn("ins put "+ins);
                instances.put(bizName, ins);

                if (watcherSchema.add(bizName)) {
                    SchemeNodeHelper.watchInstance(bizName, new IZkDataListener() {

                        public void handleDataDeleted(String dataPath) throws Exception {
                            logger.warn(dataPath + " was deleted, so destroy the bizName " + bizName);
                            destroyInstance(bizName);
                        }

                        public void handleDataChange(String dataPath, byte[] data) throws Exception {
                            logger.warn(dataPath + " was changed, so destroy the bizName " + bizName);
                            destroyInstance(bizName);
                        }
                    });
                }
                success = true;
            }
            catch (SQLException e) {
                throw e;
            }
            catch (Throwable t) {
                throw new SQLException("cannot build datasource for bizName: " + bizName, t);
            }
            finally {
                if (!success) {
                    instances.remove(bizName);
                }
            }
        }
        return ins;
    }

}

真正创建DataSource的代码:

package com.cnscud.xpower.dbn;

import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.lang.StringUtils;

import java.util.Map;

/**
 * Hikari DataSource.
 *
 * 思考: 可以根据参数里面的类型来使用不同的库创建DataSource, 例如Druid. (默认为HikariDataSource)
 *
 *
 * @author Felix Zhang 2021-08-05 11:14
 * @version 1.0.0
 */
public class SimpleDataSourceBuilder {

    public HikariDataSource buildDataSource(Map<String, String> args) {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(getUrl(args));
        config.setUsername(args.get("username"));
        config.setPassword(args.get("password"));
        config.setDriverClassName(getDriverClassName(args));

        String maximumPoolSizeKey = "maximum-pool-size";
        int maximumPoolSize = 30;
        if(StringUtils.isNotEmpty(args.get(maximumPoolSizeKey))){
            maximumPoolSize = Integer.parseInt(args.get(maximumPoolSizeKey));
        }

        config.addDataSourceProperty("cachePrepStmts", "true"); //是否自定义配置,为true时下面两个参数才生效
        config.addDataSourceProperty("prepStmtCacheSize", maximumPoolSize); //连接池大小默认25,官方推荐250-500
        config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); //单条语句最大长度默认256,官方推荐2048
        config.addDataSourceProperty("useServerPrepStmts", "true"); //新版本MySQL支持服务器端准备,开启能够得到显著性能提升
        config.addDataSourceProperty("useLocalSessionState", "true");
        config.addDataSourceProperty("useLocalTransactionState", "true");
        config.addDataSourceProperty("rewriteBatchedStatements", "true");
        config.addDataSourceProperty("cacheResultSetMetadata", "true");
        config.addDataSourceProperty("cacheServerConfiguration", "true");
        config.addDataSourceProperty("elideSetAutoCommits", "true");
        config.addDataSourceProperty("maintainTimeStats", "false");

        config.setMaximumPoolSize(maximumPoolSize); //
        config.setMinimumIdle(10);//最小闲置连接数,默认为0
        config.setMaxLifetime(600000);//最大生存时间
        config.setConnectionTimeout(30000);//超时时间30秒
        config.setIdleTimeout(60000);

        config.setConnectionTestQuery("select 1");

        return new HikariDataSource(config);
    }

    private String getDriverClassName(Map<String, String> args) {
        return args.get("driver-class-name");
    }

    private String getUrl(Map<String, String> args) {
        return args.get("jdbc-url") == null ? args.get("url"): args.get("jdbc-url");
    }
}

为了方便读取Zookeeper节点, 还有个SchemeNodeHelper:

支持两种配置文件的方式 json或者Properties格式:

package com.cnscud.xpower.dbn;

import com.cnscud.xpower.configcenter.ConfigCenter;
import com.cnscud.xpower.utils.Jsons;
import com.github.zkclient.IZkDataListener;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * 从Zookeeper的 /xpower/dbn节点下读取数据库配置.
 * 内容支持两种格式: json或者properties格式.
 *
 * JSON格式如下:
 * {
 *   "jdbc-url": "jdbc:mysql://127.0.0.1:3306/cavedemo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC",
 *   "username": "dbuser",
 *   "password": "yourpassword",
 *   "driver-class-name": "com.mysql.cj.jdbc.Driver"
 * }
 *
 * Properties格式如下:
 *  jdbc-url: jdbc:mysql://127.0.0.1:3306/cavedemo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
 *  username: dbuser
 *  password: password
 *  driver-class-name: com.mysql.cj.jdbc.Driver
 *
 * @author Felix Zhang
 * @since 2021-8-5
 */
public class SchemeNodeHelper {

    static final Logger logger = LoggerFactory.getLogger(SchemeNodeHelper.class);

    //支持两种格式: json, properties
    public static Map<String, String> getInstance(final String instanceName) throws Exception {
        String data = ConfigCenter.getInstance().getDataAsString("/xpower/dbn/" + instanceName);
        if(StringUtils.isEmpty(data)){
            return null;
        }

        data = data.trim();
        if (data.startsWith("{")) {
            //as json
            Map<String, String> swap = Jsons.fromJson(data, Map.class);
            Map<String, String> result = new HashMap<>();

            if (swap != null) {
                for (String name : swap.keySet()) {
                    result.put(name.toLowerCase(), swap.get(name));
                }
            }

            return result;
        }
        else {
            //as properties
            Properties props = new Properties();
            try {
                props.load(new StringReader(data));
            }
            catch (IOException e) {
                logger.error("loading global config failed", e);
            }

            Map<String, String> result = new HashMap<>();

            for (String name : props.stringPropertyNames()) {
                result.put(name.toLowerCase(), props.getProperty(name));
            }

            return result;
        }
    }

    public static void watchInstance(final String bizName, final IZkDataListener listener) {
        final String path = "/xpower/dbn/" + bizName;
        ConfigCenter.getInstance().subscribeDataChanges(path, listener);
    }
}

实际应用

最后在MyBatis项目中, 替换原有MainDataSource代码为:

/**
     * 添加@Primary注解,设置默认数据源,事务管理器.
     * 此处使用了一个可以动态重建的DataSource, 如果Zookeeper配置改变,会动态重建.
     */
    @Primary
    @Bean(name = "mainDataSource")
    public DataSource mainDataSource() throws Exception {
        return SimpleDBNDataSourceFactory.getInstance().getDataSource("cavedemo");
    }

运行项目, 发现可以连上数据库, 并且不重启项目的情况下, 动态修改数据库配置, 能自动重连.

项目代码:

https://github.com/cnscud/xpower/tree/main/xpower-main/src/main/java/com/cnscud/xpower/dbn

其中用到的 ConfigCenter 也在这个项目里, 也可以自己实现, 就可以脱离本项目了.

本文来自博客园,作者:飞云~风之谷,转载请注明原文链接:https://www.cnblogs.com/cnscud/p/15103859.html

到此这篇关于MyBatis使用Zookeeper保存数据库的配置,可动态刷新的文章就介绍到这了,更多相关MyBatis使用Zookeeper保存数据库的配置,可动态刷新内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • dubbo中zookeeper请求超时问题:mybatis+spring连接mysql8.0.15的配置

    这两天准备复习一下java,所以写一个采用dubbo的商场项目练练手,却卡第一个测试上,启动provider服务和Consumer服务,请求接口却始终报zookeeper请求超时错误(dubbo+zookeeper服务端重复调用三次),经过排查,我的问题是出在dao层与数据库连接的问题上(而且provider方还不报错,我也是R-此处省略一万字,其他都是正常的,如果你不是出在dao层连接数据库的问题,此文可能对你帮助不大).dao层采用mybatis+spring连接mysql数据库版本8.0.

  • MyBatis使用Zookeeper保存数据库的配置可动态刷新的实现代码

    核心关键点: 封装一个DataSource, 重写 getConnection 就可以实现 我们一步一步来看. 环境: Spring Cloud + MyBatis MyBatis常规方式下配置数据源: 使用Spring的Configuration package com.cnscud.cavedemo.fundmain.config; import com.cnscud.xpower.dbn.SimpleDBNDataSourceFactory; import org.apache.ibati

  • Spring+Mybatis 实现aop数据库读写分离与多数据库源配置操作

    在数据库层面大都采用读写分离技术,就是一个Master数据库,多个Slave数据库.Master库负责数据更新和实时数据查询,Slave库当然负责非实时数据查询.因为在实际的应用中,数据库都是读多写少(读取数据的频率高,更新数据的频率相对较少),而读取数据通常耗时比较长,占用数据库服务器的CPU较多,从而影响用户体验.我们通常的做法就是把查询从主库中抽取出来,采用多个从库,使用负载均衡,减轻每个从库的查询压力. 废话不多说,多数据源配置和主从数据配置原理一样 1.首先配置  jdbc.prope

  • Spring Boot 与 Kotlin 使用Redis数据库的配置方法

    Spring Boot中除了对常用的关系型数据库提供了优秀的自动化支持之外,对于很多NoSQL数据库一样提供了自动化配置的支持,包括:Redis, MongoDB, Elasticsearch, Solr和Cassandra. 使用Redis Redis是一个开源的使用 ANSI C 语言编写.支持网络.可基于内存亦可持久化的日志型. Key-Value 数据库. Redis官网 Redis中文社区 引入依赖 Spring Boot提供的数据访问框架Spring Data Redis基于Jedi

  • Spring+Mybatis+Mysql搭建分布式数据库访问框架的方法

    一.前言 用Java开发企业应用软件, 经常会采用Spring+MyBatis+Mysql搭建数据库框架.如果数据量很大,一个MYSQL库存储数据访问效率很低,往往会采用分库存储管理的方式.本文讲述如何通过Spring+Mybatis构建多数据库访问的架构,并采用多线程提升数据库的访问效率. 需要说明一下,这种方式只适合数据库数量.名称固定,且不是特别多的情况.针对数据库数量不固定的情况,后面再写一篇处理方案. 二.整体方案 三.开发环境准备 3.1 下载Spring.Mybatis.Mysql

  • 浅谈分布式锁的几种使用方式(redis、zookeeper、数据库)

    Q:一个业务服务器,一个数据库,操作:查询用户当前余额,扣除当前余额的3%作为手续费 synchronized lock dblock Q:两个业务服务器,一个数据库,操作:查询用户当前余额,扣除当前余额的3%作为手续费 分布式锁 我们需要怎么样的分布式锁? 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行. 这把锁要是一把可重入锁(避免死锁) 这把锁最好是一把阻塞锁(根据业务需求考虑要不要这条) 这把锁最好是一把公平锁(根据业务需求考虑要不要这条) 有高可用

  • MongoDB数据库安装配置、基本操作实例详解

    本文实例讲述了MongoDB数据库安装配置.基本操作.分享给大家供大家参考,具体如下: 1.简介 NO SQL:NoSQL(NoSQL = Not Only SQL ),意即"不仅仅是SQL",是对不同于传统的关系数据库管理系统(RDBMS)的统称.NoSQL用于超大规模数据的存储,这些类型的数据存储不需要固定的模式,无需多余操作就可以横向扩展. RDBMS NOSQL - 高度组织化结构化数据 - 结构化查询语言(SQL) - 数据和关系都存储在单独的表中. - 数据操纵语言,数据定

  • Mybatis如何自动生成数据库表的实体类

    第一步引入jar 第二步,配置文本文件 # 数据库驱动jar 路径 本地创库的包 drive.class.path=C:/Users/Administrator/.m2/repository/mysql/mysql-connector-java/5.1.30/mysql-connector-java-5.1.30.jar # 数据库连接参数 jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://192.168.1.188:3306/sxh

  • mac安装mysql数据库及配置环境变量的图文教程

    目录 安装mysql 配置环境变量 安装mysql 下载mysql.我下载的是:mysql-8.0.11-macos10.13-x86_64.dmg 双击打开mysql-8.0.11-macos10.13-x86_64.dmg,然后双击mysql-8.0.11-macos10.13-x86_64.pkg 一路点击继续,傻瓜式安装,没什么好说的 此处选择"Use Legacy Password Encryption",否则使用navicat连接mysql的时候,会报无法加载身份验证的错误

  • mybatis plus generator 根据数据库自动生成实体类的实现示例

    目录 1.添加依赖 2.编写代码生成器 3.运行主程序,输入表名 1.添加依赖 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.3.2</version> </dependency> <dependency> <groupId&g

  • Mybatis实现分包定义数据库的原理与过程

    目录 前言 原理 前提: 猜想 实现 1:定义两个数据源 2:定义两个SqlSessionFactory 3:不同包实现绑定不同的sqlSessionFactory 4:测试 拓展: 总结 前言 业务需求中需要连接两个数据库处理数据,需要用动态数据源.通过了解mybatis的框架,计划 使用分包的方式进行数据源的区分. 原理 前提: 我们使用mybatis都会有四个步骤 1:构建SqlSessionFactory 2:通过SqlSessionFactory 获取到sqlSession 对象 3:

随机推荐