Druid作为“数据库连接池”的使用以及数据库的动态切换

本文介绍Druid数据库连接池的配置方法及其监控功能,包括如何设置连接池参数、实现读写分离及数据库动态切换,同时展示了如何配置监控以优化数据库性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Druid简介

    Druid是一个JDBC组件库,包括数据库连接池、SQL Parser等组件。DruidDataSource是最好的数据库连接池。

    

    Druid是一个JDBC组件,它包括三部分: 

  • DruidDriver 代理Driver,能够提供基于Filter-Chain模式的插件体系。 

  • DruidDataSource 高效可管理的数据库连接池。 

  • SQLParser 


    Druid可以做什么? 

     1) 可以监控数据库访问性能,Druid内置提供了一个功能强大的StatFilter插件,能够详细统计SQL的执行性能,这对于线上分析数据库访问性能有帮助。 

      2) 替换DBCPC3P0。Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。 

     3) 数据库密码加密。直接把数据库密码写在配置文件中,这是不好的行为,容易导致安全问题。DruidDruiver和DruidDataSource都支持PasswordCallback。 

       4) SQL执行日志,Druid提供了不同的LogFilter,能够支持Common-LoggingLog4j和JdkLog,你可以按需要选择相应的LogFilter,监控你应用的数据库访问情况。 

扩展JDBC,如果你要对JDBC层有编程的需求,可以通过Druid提供的Filter-Chain机制,很方便编写JDBC层的扩展插件。


  下载地址

      maven central repository http://repo1.maven.org/maven2/com/alibaba/druid/

   Maven坐标
       <dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>druid</artifactId>
       <version>1.1.5</version>
  </dependency>
  常见问题汇总

      https://github.com/alibaba/druid/wiki/常见问题


以上是Druid作者温绍对druid的介绍说明 ,接下来,我们在项目中实践一下


    
那么首先,既然说到Druid是“数据库连接池”,那么我们就需要写配置文件来连接到数据库,直接贴码我的配置文件,参数含义就不赘述了,代码中我都写了注释
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--注入properties文件-->
    <context:property-placeholder location="classpath:dataSource.properties" ignore-unresolvable="true" />
    <!--spring相关注解都需要声明Bean,包括@Autowired,@Resource等等,单个声明太过繁琐,spring就提供了这种简便的方式
    注:由于<context:component-scan base-package=”xx.xx”/>也包含了自动注入上述Bean的功能,所以<context:annotation-config/> 可以省略。
    如果两者都进行了配置,则只有前者有效。
    同时<context:component-scan>除了具有<context:annotation-config>的功能之外,<context:component-scan>还可以在指定的package下扫描以及注册javabean 。-->
    <context:annotation-config />

    <!--配置数据库-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" >
        <property name="driverClassName" value="${jdbc.driver.class.name}" />
        <!--druid开启日志 value是日志间隔时间,单位是ss-->
        <property name="timeBetweenLogStatsMillis" value="300000" />
        <!--初始连接数-->
        <property name="initialSize" value="${jdbc.initialSize}"/>
        <!--连接池中可同时连接的最大的连接数           一些版本maxActive配置选项已重命名为maxTotal-->
        <property name="maxActive" value="${jdbc.maxActive}"/>
        <!--最小连接池数量    maxIdle已不再使用,配置了也没效果-->
        <property name="minIdle" value="${jdbc.minIdle}"/>
        <!--最大等待毫秒数, 单位为 ms, 超过时间会出错误信息-->
        <property name="maxWait" value="${jdbc.maxWait}"/>
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000" />
        <!-- 配置一个连接在池中最小生存的时间,最小可被驱逐时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000"/>
        <!--验证数据库连接的有效性
                Oracle : select 1 from dual           Mysql : select 1-->
        <property name="validationQuery" value="select 1"/>
        <!-- testOnBorrow和testOnReturn在生产环境一般是不开启的,主要是性能考虑。
        失效连接主要通过testWhileIdle保证,如果获取到了不可用的数据库连接,一般由应用处理异常。-->
        <property name="testWhileIdle" value="true" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="proxyFilters" >
            <list>
                <ref bean="log-filter"/>
                <ref bean="stat-filter"/>
            </list>
        </property>
        <!--属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:
            监控统计用的filter:stat
            日志用的filter:log4j
            防御sql注入的filter:wall-->
        <property name="filters" value="stat,wall" />

    </bean>
    <!--druid监控-->
    <bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter">
        <!--慢查询-->
        <property name="mergeSql" value="true"/>
        <property name="slowSqlMillis" value="1000"/>
        <property name="logSlowSql" value="true"/>
    </bean>
    <!--将监控到的数据持久化到日志-->
    <bean id="log-filter" class="com.alibaba.druid.filter.logging.Slf4jLogFilter">
        <!--表示是否显示SQL语句-->
        <!--<property name="statementExecutableSqlLogEnable" value="true"/>-->
        <!--表示是否显示结果集-->
        <property name="resultSetLogEnabled" value="false"/>
        <!--所有DataSource相关的日志-->
        <property name="dataSourceLogEnabled" value="true" />
        <!--所有statement相关的日志-->
        <property name="statementLogEnabled" value="true" />
        <!--所有连接相关的日志-->
        <property name="connectionLogEnabled" value="true" />
    </bean>
    <!--主库-->
    <bean id="masterDateSource" parent="dataSource" init-method="init" destroy-method="close" >
        <property name="url" value="${master.jdbc.url}" />
        <property name="username" value="${master.jdbc.username}" />
        <property name="password" value="${master.jdbc.password}" />
    </bean>
    <!--从库-->
    <bean id="slaveDateSource" parent="dataSource" init-method="init" destroy-method="close" >
        <property name="url" value="${slave.jdbc.url}" />
        <property name="username" value="${slave.jdbc.username}" />
        <property name="password" value="${slave.jdbc.password}" />
    </bean>

    <!--主从库动态切换切面-->
    <bean class="com.abc.framework.db.DataSourceAspect"  id="dataSourceAspect"/>
    <aop:config>
        <aop:aspect ref="dataSourceAspect" >
            <aop:pointcut  id="dbPonitCut" expression="execution(* com.abc.service.*.*(..))" />
            <aop:before method="before" pointcut-ref="dbPonitCut" />
            <aop:around method="doAround" pointcut-ref="dbPonitCut"/>
            <aop:after method="doAfter" pointcut-ref="dbPonitCut"/>
        </aop:aspect>
    </aop:config>
    <!--整合数据库-->
    <bean id="dynamicDataSource" class="com.abc.framework.db.DynamicDataSource" >
        <property name="defaultDataSourceName" value="slaveDateSource"/>
        <property name="masterDataSourceName" value="masterDateSource"/>
        <property name="slaveDataSourceNames" >
            <list>
                <value>slaveDateSource</value>
            </list>
        </property>
        <property name="targetDataSources" >
            <map key-type="java.lang.String">
                <entry key="slaveDateSource" value-ref="slaveDateSource" />
                <entry key="masterDateSource" value-ref="masterDateSource"/>
            </map>
        </property>
    </bean>
    <!--spring管理事物-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" c:dataSource-ref="dynamicDataSource" />
    <!--启动对事物注解的支持-->
    <tx:annotation-driven transaction-manager="txManager"/>


    <!-- 配置sqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="configLocation" value="classpath:context/mybaties/page-plugin.xml"/>
        <property name="dataSource" ref="dynamicDataSource" />
        <property name="mapperLocations">
            <list>
                <value>classpath:mapper/*Mapper.xml</value>
            </list>
        </property>
    </bean>

    <!-- 通过扫描的模式,自动注入bean -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        <property name="basePackage" value="com.abc.dao" />
    </bean>
</beans>

同时,我们还需要再web中注入提供Druid视图显示的service

<servlet>
        <servlet-name>DruidStatView</servlet-name>
        <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
        <init-param>
            <param-name>loginUsername</param-name>
            <param-value>admin</param-value>
        </init-param>
        <init-param>
            <param-name>loginPassword</param-name>
            <param-value>admin</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>DruidStatView</servlet-name>
        <url-pattern>/druid/*</url-pattern>
    </servlet-mapping>

要查看内置监控,访问路径是/druid/index.html

StatViewSerlvet展示出来的监控信息比较敏感,是系统运行的内部情况,如果你需要做访问控制,可以配置allow和deny这两个参数。比如:

  <servlet>
      <servlet-name>DruidStatView</servlet-name>
      <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
  	<init-param>
  		<param-name>allow</param-name>
  		<param-value>128.242.127.1/24,128.242.128.1</param-value>
  	</init-param>
  	<init-param>
  		<param-name>deny</param-name>
  		<param-value>128.242.127.4</param-value>
  	</init-param>
  </servlet>
  • deny优先于allow,如果在deny列表中,就算在allow列表中,也会被拒绝。
  • 如果allow没有配置或者为空,则允许所有访问

在StatViewSerlvet输出的html页面中,有一个功能是Reset All,执行这个操作之后,会导致所有计数器清零,重新计数。你可以通过配置参数关闭它。

  <servlet>
      <servlet-name>DruidStatView</servlet-name>
      <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
  	<init-param>
  		<param-name>resetEnable</param-name>
  		<param-value>false</param-value>
  	</init-param>
  </servlet>

至此基本配置都完成了  如果还需要使用Web应用、URI监控、Session监控、Spring监控等则还需要继续增加配置。

    按需要配置web和spring的关联监控


相关拓展


        为了提高数据库的性能,降低主库承担的请求数量,我们还要实现读写分离,这就涉及到了根据注解来动态切换数据库,这里我们可以通过自定义注解来实现。这里用到

        

DataSource      是一个注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface DataSource {
    String value();
}

DataSourceAspect      通过spring的aop面向切面编程, 拦截切面中注解(@DataSource)的value值,并将该值提供至

DynamicDataSource,再由DynamicDataSource切换数据源的连接。

/**
 * Created by zhanghe
 * 2018/3/3.
 *
 * 数据库动态切换切面类
 */
public class DataSourceAspect {
    /**
     * DataSourceConf 使用的是阿里的Disconf开源框架,我们用它来维护我们得到配置文件,可以进行热部署
     */
    @Autowired
    private DataSourceConf conf;
    private static Logger logger = LoggerFactory.getLogger(com.abc.framework.db.DataSourceAspect.class);

    public DataSourceAspect() {
    }

    public void before(JoinPoint joinPoint) {
        logger.debug("log begin method: " + joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName());

        try {
            Object e = joinPoint.getTarget();
            String method = joinPoint.getSignature().getName();
            Class[] parameterTypes = ((MethodSignature)joinPoint.getSignature()).getMethod().getParameterTypes();
            //获取注解 @DateSource 的value值
            String dataSourceValue = this.buildDataSource(e, method, parameterTypes);
            //如果注解为 @Transactional ,根据事物是否只读  readOnly 来判断,只读为从库
            dataSourceValue = this.buildTransactional(dataSourceValue, e, method, parameterTypes);
            if(dataSourceValue == null) {
                //如果没注解,那么从Disconf 中获取默认数据库名称
                String defaultDataSource = this.conf.getDefaultDataSourceName();
                defaultDataSource = defaultDataSource == null? com.abc.framework.db.DynamicDataSource.getDefaultDataSourceName():defaultDataSource;
                logger.debug("切换默认数据源:[ " + defaultDataSource + " ]");
                //DynamicDataSource负责真正意义上的切换数据库
                com.abc.framework.db.DynamicDataSource.setDataSourceKey(defaultDataSource);
            } else {
                logger.debug("切换数据源到:[ " + dataSourceValue + " ]");
                com.abc.framework.db.DynamicDataSource.setDataSourceKey(dataSourceValue);
            }
        } catch (Exception var7) {
            logger.error("切换数据源异常", var7);
        }

    }
    //根据java反射机制 获取到 注解@DateSource 的value值
    private String buildDataSource(Object target, String method, Class<?>[] parameterTypes) throws NoSuchMethodException {
        String dataSourceValue = null;
        Class[] classes = target.getClass().getInterfaces();
        Method method1 = classes[0].getMethod(method, parameterTypes);
        if(method1 != null && method1.isAnnotationPresent(DataSource.class)) {
            dataSourceValue = ((DataSource)method1.getAnnotation(DataSource.class)).value();
        }

        return dataSourceValue;
    }
    //如果注解为 @Transactional ,根据事物是否只读  readOnly 来判断,只读为从库
    private String buildTransactional(String dataSourceValue, Object target, String method, Class<?>[] parameterTypes) throws NoSuchMethodException {
        Class clazz = target.getClass();
        Method m = clazz.getMethod(method, parameterTypes);
        if(m.isAnnotationPresent(Transactional.class)) {
            Transactional transactional = (Transactional)m.getAnnotation(Transactional.class);
            if(transactional.readOnly()) {
                dataSourceValue = com.abc.framework.db.DynamicDataSource.getSlaveDataSource();
            } else {
                dataSourceValue = com.abc.framework.db.DynamicDataSource.getMasterDataSourceName();
            }
        }

        return dataSourceValue;
    }
    //性能统计
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        long time = System.currentTimeMillis();
        Object retVal = pjp.proceed();
        time = System.currentTimeMillis() - time;
        logger.debug(pjp.getTarget().getClass().getName() + "." + pjp.getSignature().getName() + "process time: " + time + " ms");
        return retVal;
    }

    public void doAfter(JoinPoint jp) {
        logger.debug("log Ending method: " + jp.getTarget().getClass().getName() + "." + jp.getSignature().getName());
    }
}

DynamicDataSource      根据DataSourceAspect提供的数据库名称value,切换至对应的数据库

/**
 * Created by zhanghe
 * 2018/3/3.
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    //里面存放的是从注解获取的 当前数据库 名称 @DataSource 的 value值
    private static final ThreadLocal<String> dataSourceKey = new InheritableThreadLocal();
    //从库名称列表
    private static List<String> slaveDataSourceNames;
    //主库名称
    private static String masterDataSourceName;
    //默认数据库名称
    private static String defaultDataSourceName;
    private static Random random = new Random();

    public DynamicDataSource() {
    }

    public static void setDataSourceKey(String dataSource) {
        dataSourceKey.set(dataSource);
    }
    //重写父类的方法,根据dataSourceKey决定当前数据库
    @Override
    protected Object determineCurrentLookupKey() {
        return dataSourceKey.get();
    }

    public static String getSlaveDataSource() {
        //随机获取从库
        return (String)slaveDataSourceNames.get(random.nextInt(slaveDataSourceNames.size()));
    }

    public static void setSlaveDataSourceNames(List<String> slaveDataSourceNames) {
        slaveDataSourceNames = slaveDataSourceNames;
    }

    public static String getDefaultDataSourceName() {
        return defaultDataSourceName;
    }

    public static void setDefaultDataSourceName(String defaultDataSourceName) {
        defaultDataSourceName = defaultDataSourceName;
    }

    public static String getMasterDataSourceName() {
        return masterDataSourceName;
    }

    public static void setMasterDataSourceName(String masterDataSourceName) {
        masterDataSourceName = masterDataSourceName;
    }
}

至于为什么 AbstractRoutingDataSource 这个抽象类可以切换数据库
看看其注释:
翻译:文摘{ @link javax.sql。根据查找键,将{@link #getConnection()}调用路由到多个目标数据源中的一个。后者通常(但不一定)是通过一些线程绑定的事务上下文确定的。
再看看 AbstractRoutingDataSource 的 determineTargetDataSource()方法;
 

可以看出determinCurrentLookupKey()这个方法是关键,那么我们的DynamicDataSource 就重写了这个方法

定义了需要转换的数据库名称。


有不正确的地方欢迎指出,一起探讨 共勉共进。

个人原创,转载需经博主同意且标明出处

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值