Druid简介
Druid是一个JDBC组件库,包括数据库连接池、SQL Parser等组件。DruidDataSource是最好的数据库连接池。
Druid是一个JDBC组件,它包括三部分:
DruidDriver 代理Driver,能够提供基于Filter-Chain模式的插件体系。
DruidDataSource 高效可管理的数据库连接池。
SQLParser
Druid可以做什么?
1) 可以监控数据库访问性能,Druid内置提供了一个功能强大的StatFilter插件,能够详细统计SQL的执行性能,这对于线上分析数据库访问性能有帮助。
2) 替换DBCP和C3P0。Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。
3) 数据库密码加密。直接把数据库密码写在配置文件中,这是不好的行为,容易导致安全问题。DruidDruiver和DruidDataSource都支持PasswordCallback。
4) SQL执行日志,Druid提供了不同的LogFilter,能够支持Common-Logging、Log4j和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的介绍说明 ,接下来,我们在项目中实践一下
<?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的关联监控
- Web关联监控配置
https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_%E9%85%8D%E7%BD%AEWebStatFilter - Spring关联监控配置
https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE_Druid%E5%92%8CSpring%E5%85%B3%E8%81%94%E7%9B%91%E6%8E%A7%E9%85%8D%E7%BD%AE
相关拓展
为了提高数据库的性能,降低主库承担的请求数量,我们还要实现读写分离,这就涉及到了根据注解来动态切换数据库,这里我们可以通过自定义注解来实现。这里用到
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 就重写了这个方法
定义了需要转换的数据库名称。
有不正确的地方欢迎指出,一起探讨 共勉共进。
个人原创,转载需经博主同意且标明出处