文章目录
一、mysql客户端与mysql服务建立连接后如果太长时间没动静,mysql服务端连接器就会自动将它断开。这个时间是由参数 wait_timeout控制的,默认值是 8 小时。
mysql客户端与mysql服务建立连接后如果太长时间没动静,mysql服务端连接器就会自动将它断开。这个时间是由参数 wait_timeout控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候如果你要继续,就需要重连,然后再执行请求了。
那么应用程序中 一般就是通过连接池来管理Mysql连接。如果一个应用与mysql建立连接后,长时间不用,理论也会出现这个问题。那么如果想一直有连接 ,数据库连接池一定帮我们做什么, 下面来看下应用中常用连接池DruidDataSource是 如何保持与mysql 连接的?
connection是如何被回收利用的?
1、DruidDataSource连接池配置
先来看下官网推荐的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整体上就生产者,消费者模式。
生产者:线程 生产一个数据库连接放入 连接池中。
消费者:应用程序从连接池中获取一个线程。
共享资源:存放连接的 连接池(这个连接池实际上就是 存放连接的数组)。
共享资源需要互斥:那么涉及到生产者 消费者 需要互斥访问 共享资源。
生产者 消费者需要协同合作:
数组已满,那么生产者 需要等待,当消费者消费一个 连接池,那么唤醒等待 的生产者
数组已空,消费者需要等待,同时唤醒生产者 产生一个连接。当连接创建好后,唤醒等待的 消费者。
整体流程:
DruidDataSource启动之后,会启动三个核心线程
线程 | 说明 |
---|---|
CreateConnectionThread | 创建连接,做为生产者,满足消费者对连接的需求。 |
DestroyConnectionThread | 销毁连接,将空闲、不健康的连接回收。将连接池维持在最小连接数。 |
LogStatsThread | 打印日志,定期打印连接池的状态。 |
1、获取连接方法
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);
}
}
1.1、初始化
- 初始化connections数组:存放可被获取的连接
- evictConnections数组:超过最小空闲驱逐时间,会从connection中移到这个数组中
- keepAliveConnections数组:存活的连接池,销毁时,如果keepAlive=true,那么会从connection中移到这个数组中。数组的大小都是maxActive
- 初始化initialSize个Connection
- 创建CreateConnectionThread线程,druid内部默认使用一个线程异步地创建连接,当然可以指定createScheduler线程池,开启多个线程创建连接,但是请把keepAlive设为true,否则不会开启异步线程创建连接
- 创建DestroyConnectionThread线程,定期扫描连接池内过期的连接,如果想对连接池外面正在使用的连接也进行清理的话,需要指定removeAbandoned为true,清理线程会判断连接是否正在使用,是否超过了清理时间而进行清理
//连接池中可用的连接(未被拿走),
//内部会维护一个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();
}
}
1.2、getConnectionDirect创建线程
调用getConnectionInternal获取经过各种包装的Connection
主要方法pollLast(nanos)
创建好的连接 放入 connection数组中。
唤醒等待在 获取连接方法 的线程。
1.3、Connection回收
应用程序调用Connection#close(),实际上调用的是DruidDataSource的recyle(DruidPooledConnection conn),我们直接分析recyle的实现
1.4、异步清理Connection
DestroyConnectionThread线程会定期执行一次清理动作,默认是60000ms执行一次,可以指定timeBetweenEvictionRunsMillis控制清理的频率,主要逻辑在于DestroyTask,首先会执行shrink对过期时间进行处理,然后根据removeAbandoned的值判断是否需要进行清理abandoned的连接。shrink只针对连接池的连接进行清理,而removeAbandoned会对从连接池外的连接进行清理
1.5、shrink保持最小空闲连接
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();
2、初始连接如何保持连接
DruidDataSource并没有和mysql保持连接,而是定时去清理无效连接。
通过启动一个线程,定时遍历connection数组,查看连接是否无效 ,如果无效那么 移除这个连接。 再判断activeCount + poolingCount <= minIdle
池外和池内连接是否小于 最小空闲连接,如果小于那么唤醒创建连接线程创建线程。
三、关于DruidDataSource的wallfilter防sql注入
1、mybatis的防sql注入#{},${}
我们都知道Mybatis是可以防止sql注入的。 比如用 #{}
这样的占位符传参可以防止sql注入。而${}
这样的占位符不能防止sql注入。原因是什么?看下面2个sql分别和#{}
,${}
1.1 ${}
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; -- '
1.2、#{}
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
1.3、得出结论?
#{}
底层mybatis调用的jdbc的PreparedStatement,会预编译,因此不会产生SQL注入问题;
${}
匹配的是真实传递的值,传递过后,会与sql语句进行字符串拼接。${}
会与其他sql进行字符串拼接,不能预防sql注入问题。
1.4#{} 生成的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方法,主要是下面这段:
1、如果你的数据源用的是DruidDataSource
那么即便${}
这样的占位符,传的参数是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;
}