赞
踩
mysql客户端与mysql服务建立连接后如果太长时间没动静,mysql服务端连接器就会自动将它断开。这个时间是由参数 wait_timeout控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候如果你要继续,就需要重连,然后再执行请求了。
那么应用程序中 一般就是通过连接池来管理Mysql连接。如果一个应用与mysql建立连接后,长时间不用,理论也会出现这个问题。那么如果想一直有连接 ,数据库连接池一定帮我们做什么, 下面来看下应用中常用连接池DruidDataSource是 如何保持与mysql 连接的?
connection是如何被回收利用的?
先来看下官网推荐的DruidDataSource连接池配置
连接阿里云AnalyticDB参考配置
使用Druid连接池连接阿里云AnalyticDB 建议配置keepAlive=true ,并使用1.1.16 之后的版本
官方地址
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <!-- 基本属性 url、user、password --> <property name="url" value="${jdbc_url}" /> <property name="username" value="${jdbc_user}" /> <property name="password" value="${jdbc_password}" /> <!-- 配置初始化大小、最小、最大 --> <property name="initialSize" value="5" /> <property name="minIdle" value="10" /> <property name="maxActive" value="20" /> <!-- 配置获取连接等待超时的时间 获取连接时最大等待时间,单位毫秒。 配置了maxWait之后,缺省启用公平锁,并发效率会有所下降, 如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 --> <property name="maxWait" value="6000" /> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="2000" /> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="600000" /> <property name="maxEvictableIdleTimeMillis" value="900000" /> <property name="validationQuery" value="select 1" /> <property name="testWhileIdle" value="true" /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> <property name="keepAlive" value="true" /> <property name="phyMaxUseCount" value="1000" /> <!-- 配置监控统计拦截的filters --> <property name="filters" value="stat" /> </bean>
DruidDataSource整体上就生产者,消费者模式。
生产者:线程 生产一个数据库连接放入 连接池中。
消费者:应用程序从连接池中获取一个线程。
共享资源:存放连接的 连接池(这个连接池实际上就是 存放连接的数组)。
共享资源需要互斥:那么涉及到生产者 消费者 需要互斥访问 共享资源。
生产者 消费者需要协同合作:
数组已满,那么生产者 需要等待,当消费者消费一个 连接池,那么唤醒等待 的生产者
数组已空,消费者需要等待,同时唤醒生产者 产生一个连接。当连接创建好后,唤醒等待的 消费者。

| 线程 | 说明 |
|---|---|
| CreateConnectionThread | 创建连接,做为生产者,满足消费者对连接的需求。 |
| DestroyConnectionThread | 销毁连接,将空闲、不健康的连接回收。将连接池维持在最小连接数。 |
| LogStatsThread | 打印日志,定期打印连接池的状态。 |
public DruidPooledConnection getConnection(long maxWaitMillis) throws SQLException {
//初始化参数:数据库连接、用户名、密码。初始化3个数组
init();
if (filters.size() > 0) {
FilterChainImpl filterChain = new FilterChainImpl(this);
return filterChain.dataSource_connect(this, maxWaitMillis);
} else {
//从池中 获取连接返回
return getConnectionDirect(maxWaitMillis);
}
}
//连接池中可用的连接(未被拿走), //内部会维护一个poolingCount值代表队列中剩余可用的连接, //每次从末尾拿走连接 connections = new DruidConnectionHolder[maxActive]; //失效、过期的连接,会暂时放在这个数组里面 evictConnections = new DruidConnectionHolder[maxActive]; //销毁线程会检测线程,如果keepalive=true,检测存活的线程放暂时放在这里,如果connections大小<于maxActive 会被重新回收 keepAliveConnections = new DruidConnectionHolder[maxActive]; /** 初始化必须的线程 */ // 开启创建连接的线程,如果线程池createScheduler为null, //则开启单个创建连接的线程 createAndStartCreatorThread(); // 开启销毁过期连接的线程 createAndStartDestroyThread(); // keepAlive为true时,并且createScheduler不为null,则初始化minIdle个线程用于创建连接 if (keepAlive) { // async fill to minIdle if (createScheduler != null) { for (int i = 0; i < minIdle; ++i) { createTaskCount++; CreateConnectionTask task = new CreateConnectionTask(); this.createSchedulerFuture = createScheduler.submit(task); } } else { this.emptySignal(); } }
调用getConnectionInternal获取经过各种包装的Connection
主要方法pollLast(nanos)
创建好的连接 放入 connection数组中。
唤醒等待在 获取连接方法 的线程。
应用程序调用Connection#close(),实际上调用的是DruidDataSource的recyle(DruidPooledConnection conn),我们直接分析recyle的实现
DestroyConnectionThread线程会定期执行一次清理动作,默认是60000ms执行一次,可以指定timeBetweenEvictionRunsMillis控制清理的频率,主要逻辑在于DestroyTask,首先会执行shrink对过期时间进行处理,然后根据removeAbandoned的值判断是否需要进行清理abandoned的连接。shrink只针对连接池的连接进行清理,而removeAbandoned会对从连接池外的连接进行清理
shrink只会清理连接池内的连接。如果回收之后小于最小空闲连接,那么唤醒创建连接的线程,去创建连接。
//获得锁 lock.lockInterruptibly(); //计算removeCount evictCount keepAliveCount等 //如果evictCount大于0 关闭连接 DruidConnectionHolder item = evictConnections[i]; Connection connection = item.getConnection(); JdbcUtils.close(connection); //如果回收之后小于最小空闲连接 if (activeCount + poolingCount <= minIdle) { //通知可以创建新连接了 empty.signal(); } //解锁 lock.unlock();
DruidDataSource并没有和mysql保持连接,而是定时去清理无效连接。
通过启动一个线程,定时遍历connection数组,查看连接是否无效 ,如果无效那么 移除这个连接。 再判断activeCount + poolingCount <= minIdle 池外和池内连接是否小于 最小空闲连接,如果小于那么唤醒创建连接线程创建线程。
我们都知道Mybatis是可以防止sql注入的。 比如用 #{}这样的占位符传参可以防止sql注入。而${}这样的占位符不能防止sql注入。原因是什么?看下面2个sql分别和#{},${}
select
* from <include refid="table_name"/> as u
where u.user_name like '${userName}'
当传的参数为aaaa' or 1=1; --。 那么$占位符的时候, 就会查出所有数据。查看下打印的日志:
--为mysql单行注释,直接注释掉了后 mybaits后面加上的'号,这样整个sql就变成了 where user_name like 'aaa' or 1=1。后面多了一个永真条件,这样数据就能全部被查出来。如果这条是delete 语句呢? 那么就删除了表里所有数据。
[main] [DEBUG] c.dao.UserInfoDao.test 159 -- []
==> Preparing: select * from USER_INFO as u where u.user_name like 'aaaa' or 1=1; -- '
select
* from <include refid="table_name"/> as u
where u.user_name like #{userName}
生成的sql语句如下:
[main] [DEBUG] c.dao.UserInfoDao.test 159 -- []
==> Preparing: select * from USER_INFO as u where u.user_name = ?
[main] [DEBUG] c.dao.UserInfoDao.test 159 -- []
==> Parameters: aaaa' or 1=1; -- (String)
[main] [DEBUG] c.dao.UserInfoDao.test 159 -- []
<== Total: 0
#{}底层mybatis调用的jdbc的PreparedStatement,会预编译,因此不会产生SQL注入问题;
${}匹配的是真实传递的值,传递过后,会与sql语句进行字符串拼接。${}会与其他sql进行字符串拼接,不能预防sql注入问题。
?里面到底是什么?jdbc的PreparedStatement到底把我们传进去的参数改成了什么形式,使得sql没有防止注入实际上可以先猜想一下。比如,无论怎么传参 想理的情况是 把%aaaa' or 1=1; --% 一部分当成参数,而不是和原sql组合成新的sql去执行。 之所有 会组装是因为 参数包含了 一部分sql的语法。那么此时让 mysql当成 参数。只要把 敏感字符转义不就行了。
MySQL驱动的源码一看究竟;
打开PreparedStatement类的setString()方法(MyBatis在#{}传递参数时,是借助setString()方法来完成,${}则不是):
先打断点看下 参数会变成啥样:

'aaaa\' or 1=1; -- ',在我们传的参数'前,加了一个转义字符\。那么' 自然就不能成为参数结束符,后面的字符无法成为条件,于是就防止的sql 注入。
查看com.mysql.cj.ClientPreparedQueryBindings#setString方法,主要是下面这段:

那么即便${}这样的占位符,传的参数是aaaa' or 1=1; --也不会sql注入,而是出现下面这样的错误。是的,如果你不熟悉DruidDataSource的配置,源码,连个sql注入都实现不了。
Error querying database. Cause: java.sql.SQLException: sql injection violation, comment not allow : select
* from
USER_INFO
as u
where u.user_name like '%aaaa' or 1=1; --%'
part alway true condition not allow
Cause: java.sql.SQLException: sql injection violation, select alway true condition not allow : select
* from
USER_INFO
as u
where u.user_name like '%aaaa' or 1=1; --%'
DruidDataSource会默认给你注入wallfilter。上面2个错误 大概意思是没有允许部分条件 永远true,没有允许select 语句的条件永远true。因为我们注入的sql有 or 1=1这个永真条件。 DruidDataSource给你判断了这个sql可能出现了注入,所以给报错了。 并且DruidDataSource也不允许执行的sql语句出现 -- 这样的单行注释。如果有 那么也会报错
如果想实现sql 注入那么需要关闭这些限制。
//注入自定义配置,将这些限制关闭
@Bean(name = "wallConfig")
public WallConfig wallConfig() {
WallConfig config = new WallConfig();
config.setNoneBaseStatementAllow(true);
config.setCommentAllow(true);
config.setConditionAndAlwayTrueAllow(true);
config.setSelectWhereAlwayTrueCheck(false);
return config;
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。