看一下,自己项目的druid连接池是否设置了PS Cache。是否需要设置?

1. Druid的相关配置

spring:
  datasource:
    name: mysql_test
    type: com.alibaba.druid.pool.DruidDataSource
    #druid相关配置
    druid:
      #打开PSCache,并指定每个连接上PSCache的大小。oracle设为true,mysql设为false。分库分表较多推荐设置为false。
      pool-prepared-statements: true
      # max-pool-prepared-statement-per-connection-size: 20
      # 或者(只要maxOpenPreparedStatements或者max-pool-prepared-statement-per-connection-size大于0,那么pool-prepared-statements默认true)
      maxOpenPreparedStatements: 20

在github上druid的dataSource相关配置文章中:

image.png

指定了该参数的使用场景。

2. mysql下为什么不推荐使用

使用场景:oracle设为true,mysql设为false。分库分表较多推荐设置为false。

2.1 为什么要做PS Cache

当使用PrepareStatement的时候,同样的SQL语句参数不同时,我们希望客户端或服务端就只需要对SQL语句只解析一次(即只创建一次PreparedStatement对象),这样在一些大并发场景下性能更佳,尤其是互联网高并发的重复SQL场景,解析会占据较大的CPU和时间开销,而本身的执行时间可能占用并不大。

2.2 mysql下的PS Cache

druid会将PrepareStatement对象存储到Map中(LRU缓存,可以通过参数决定缓存的大小)。

存储的源码:com.alibaba.druid.pool.DruidPooledConnection#closePoolableStatement

closeStatement时,将PrepareStatement对象放入缓存中。

    public void closePoolableStatement(DruidPooledPreparedStatement stmt) throws SQLException {
        PreparedStatement rawStatement = stmt.getRawPreparedStatement();

     ...
        PreparedStatementHolder stmtHolder = stmt.getPreparedStatementHolder();
        stmtHolder.decrementInUseCount();
        if (stmt.isPooled() && holder.isPoolPreparedStatements() && stmt.exceptionCount == 0) {
            //放入缓存
            holder.getStatementPool().put(stmtHolder);

            stmt.clearResultSet();
            holder.removeTrace(stmt);

            stmtHolder.setFetchRowPeak(stmt.getFetchRowPeak());

            stmt.setClosed(true); // soft set close
        } ...
    }

查看LRU缓存对象源码:com.alibaba.druid.pool.PreparedStatementPool.LRUCache可以发现,该缓存对象操作不能保证线程安全。

    public class LRUCache extends LinkedHashMap {

        private static final long serialVersionUID = 1L;

        public LRUCache(int maxSize){
            super(maxSize, 0.75f, true);
        }

        protected boolean removeEldestEntry(Entry eldest) {
            boolean remove = (size() > dataSource.getMaxPoolPreparedStatementPerConnectionSize());

            if (remove) {
                closeRemovedStatement(eldest.getValue());
            }

            return remove;
        }
    }

druid在存储PreparedStatementHolder时并没有保证线程安全,是因为Connection连接本身是线程安全的。

2.3 mysql以及大量分库分表不推荐使用PS Cache

缓存本质上是空间换取时间。当占用大量空间但缓存命中率低且命中若没有收益,那么就不推荐使用缓存了。

  1. mysql的PS Cache是Connection级别的,当相同的sql被不同的Connection执行时,也不会共享彼此的Connection连接(会在新的Connection重新维护一份)。而执行sql从连接池中获取连接都是无规则的,(同一个会话中开启事务管理器除外),也就是同样的SQL在不同的连接中被使用是十分正常的事情。

  2. 即使每个元素在PS Cache占用的内存不多,但因为是Connection级别,当存在大量分库分表的场景下,会产生大量的Connection连接。累积下就会占用大量的内存。

  3. max-pool-prepared-statement-per-connection-size设置的过小,会导致命中率很低,基本没有什么作用。但设置的过大(例如200)会占用大量内存。而且即使命中中了,因为mysql不支持游标,效果不是很好。

综上所述:mysql的druid连接池没必要开启ps cache。

2.4 其他数据库是否开启PS Cache

此处讲述的主要是MySQL及其JDBC的PS Cache的部分问题,在实际的场景中可以作为参考避免一些不必要的问题发生(也未必会遇得到),这些糟点在其他数据库上未必是适用的,例如Oracle在SQL Parser有:Hard ParserSoft ParserSoft Parser的就是语法树结果,它存放在Oracle一个单独的共享区域中,并非Session级别,所以不同的Connection之间可以共享同样的SQL语句,Oracle针对所有的SQL语句都会使用LRU算法进行Cache,单从本文提到的PS Cache来讲在Oracle上体现的效果会更好一些。

推荐阅读

DruidDataSource一次踩坑记录

MySQL JDBC为什么都不开启PreparedStatement Cache

MySQL JDBC PrepareStatement基本的两种模式&客户端空间占用的源码分析

文章来源于互联网:Druid连接池在mysql的场景PS Cache是否需要开启?

发表评论