重看Mysql联想到数据库连接池DruidDataSource

一、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;
    }

附上,官方的wallfilter配置文档

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值