基于Java连接池Druid的应用开发

本文介绍基于MySQL JDBC连接池Druid连接并访问Lindorm宽表引擎的使用方法。

前提条件

注意事项

  • Lindorm SQL前端接入节点使用SLB做负载均衡,客户端连接前端节点。为了将客户端请求较为均匀地发送到各个前端节点,建议连接保持的时间不宜太长,可以配置phyMaxUseCount参数。

  • 执行查询前从连接池获取连接,执行完查询后要及时调用conn.close()将连接返回到连接池中。再次使用时,请从连接池中获取新连接,避免长时间使用一个连接导致Druid无法及时检查到连接失效。

  • 在复杂网络环境下,若遇到网关性能达到瓶颈、网络链路长、网络抖动、重传率或丢包率高等情况,可能会导致连接中断。建议确保连接池配置合理,并在必要时通过业务代码侧的重试机制优化。

  • 服务端升级重启时,连接可能会短暂中断。即使使用了连接池,业务侧仍可能感知到异常。建议捕获异常并重试。

  • 根据业务情况合理调整连接池配置,并确保配置生效。您可以在程序中通过DruidDataSource#getStatData()DruidDataSource#dump()方法定期获取生效的配置与连接池的信息,并在日志中查看、核对相关配置信息。

操作步骤

  1. 通过连接池Druid连接Lindorm宽表引擎前,需要安装连接池DruidLindorm JDBC Driver。

    Maven项目为例,在pom.xml文件的dependencies中添加以下依赖项。

    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.2.11</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.3.0</version>
    </dependency>

    如果您是通过druid-spring-boot-starter使用Druid连接池,则需要先排除druid-spring-boot-starter依赖的druid组件,再显式依赖druid组件,具体代码如下所示。

    <dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>druid-spring-boot-starter</artifactId>
       <version>1.2.11</version>
       <exclusions>
          <exclusion>
             <groupId>com.alibaba</groupId>
             <artifactId>druid</artifactId>
          </exclusion>
       </exclusions>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.2.11</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.3.0</version>
    </dependency>
  2. 配置连接池Druid的参数。在Maven项目的src/main/resources目录中新建druid.properties文件,并在文件中添加以下内容。

    # 驱动类名,无需替换
    driverClassName=com.mysql.cj.jdbc.Driver
    # urlLindorm宽表引擎MySQL协议的JDBC连接地址,username、password为宽表引擎的用户名及密码,可以在Lindorm控制台上获取。
    # database为需要连接的数据库,需替换为实际使用的数据库名称。其他参数建议保持不变,对性能有一定帮助。
    url=jdbc:mysql://ld-uf6k8yqb741t3****-proxy-sql-lindorm-public.lindorm.rds.aliyuncs.com:33060/database?sslMode=disabled&allowPublicKeyRetrieval=true&useServerPrepStmts=true&useLocalSessionState=true&rewriteBatchedStatements=true&cachePrepStmts=true&prepStmtCacheSize=100&prepStmtCacheSqlLimit=50000000&socketTimeout=120000
    username=****
    password=****
    
    # 初始化连接池即创建连接,建议保持不变
    init=true
    # 初始化连接池时建立连接的个数,可以根据实际情况调整
    initialSize=10
    # 连接池中允许的最大连接数量,可以根据实际情况调整,建议和业务线程池大小相同
    maxActive=40
    # 连接池中维护的空闲连接的数量,可以根据实际情况调整.对性能要求高的场景建议和maxActive的值相同,如果业务存在明显的峰谷波动,建议设置小一些
    minIdle=40
    # 获取连接最大等待时间,单位毫秒(ms),建议保持不变
    maxWait=30000
    
    #配置一个连接最大使用次数,避免长时间使用相同连接造成服务器端负载不均衡,对性能有略微影响
    phyMaxUseCount=30000
    # 连接保活配置项,建议保持不变,否则可能出现连接断开
    druid.keepAlive=true
    # 连接空闲多久以后,会检查连接的有效性
    druid.keepAliveBetweenTimeMillis=120000
    # 多久执行一次淘汰连接和保活操作
    timeBetweenEvictionRunsMillis=60000
    #空闲连接淘汰时间
    minEvictableIdleTimeMillis=1800000
    maxEvictableIdleTimeMillis=1800000
    
    # 连接验证配置项,建议保持不变
    testWhileIdle=true
    testOnBorrow=false
    testOnReturn=false

    参数说明

    参数

    说明

    url

    MySQL协议的Java JDBC连接地址。格式为jdbc:mysql://<MySQL兼容地址>/<数据库名>?<连接配置>

    数据库名不填写时默认连接default数据库。如何获取MySQL兼容地址,请参见查看连接地址

    连接配置可以有效提升性能,建议全部填写,详细说明,请参见连接配置说明

    重要
    • 如果应用部署在ECS实例,建议您通过专有网络访问Lindorm实例,以获得更高的安全性和更低的网络延迟。

    • 如果应用部署在本地,在通过公网连接Lindorm实例前,需在控制台开通公网地址。开通方式:在控制台选择数据库连接 > 宽表引擎,在宽表引擎页签单击开通公网地址

    • 通过专有网络访问Lindorm实例,url中请填写MySQL兼容地址对应的专有网络地址。通过公网访问Lindorm实例,url中请填写MySQL兼容地址对应的公网地址。

    username

    如果您忘记用户密码,可以通过Lindorm宽表引擎的集群管理系统修改密码。具体操作,请参见修改用户密码

    password

  3. 加载连接池Druid的参数并初始化连接池Druid。

    // 加载参数
    Properties properties = new Properties();
    InputStream inputStream = DruidPoolDemo.class.getClassLoader().getResourceAsStream("druid.properties");
    properties.load(inputStream);
    // 初始化连接池
    DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
  4. 通过连接池Druid获取JDBC的连接信息并访问Lindorm宽表引擎。

    /* -------------- 基于JDBC的访问示例 ----------------- */
    
    String tableName = "sql_table_" + new Random().nextInt(1000);
    // 创建表
    try (Connection connection = dataSource.getConnection()) {
        try (Statement statement = connection.createStatement()) {
            String sql = "create table if not exists " + tableName + "(id VARCHAR, name VARCHAR, primary key(id))";
            int ret = statement.executeUpdate(sql);
            System.out.println(ret);
        }
    }
    
    // 插入数据
    try (Connection connection = dataSource.getConnection()) {
        String sql = "insert into " + tableName + "(id,name) values(?,?)";
        try (PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setString(1, "aa");
            ps.setString(2, "bb");
    
            int ret = ps.executeUpdate();
            System.out.println(ret);
        }
    }
    
    //批量写入数据
    String insertSql = "insert into " + tableName + "(id,name) values(?,?)";
    int batchSize =100;
    try (Connection connection = dataSource.getConnection()) {
      try (PreparedStatement ps = connection.prepareStatement(insertSql)) {
        for (int i = 0; i < batchSize; i++) {
          ps.setString(1, "aa" + i);
          ps.setString(2, "bb" + i);
          //加入批次
          ps.addBatch();
        }
        //批次写入
        ps.executeBatch();
      }
    }
    
    // 查询数据
    try (Connection connection = dataSource.getConnection()) {
        String sql = "select * from " + tableName + " where id=?";
        try (PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setString(1, "aa");
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                String id = rs.getString(1);
                String name = rs.getString(2);
                System.out.println("id=" + id);
                System.out.println("name=" + name);
            }
        }
    }
    
    // 删除数据
    try (Connection connection = dataSource.getConnection()) {
        String sql = "delete from " + tableName + " where id=?";
        try (PreparedStatement ps = connection.prepareStatement(sql)) {
            ps.setString(1, "aa");
            ps.executeUpdate();
        }
    }
    说明
    • Lindorm SQL中,insert语句的语义与upsert相同。由于MySQL JDBC的客户端优化了insert语句,因此在写入数据时更推荐您使用insert语句。

    • Batch写入比单行写入更能节省RPC次数,服务器能够批量处理行写入,更容易达到更大的吞吐。但Batche写入如果一次性写入过多的行,可能会导致服务器OOMFull GC,进而影响服务稳定性。因此,建议您将Batch写入的行数控制在合理范围内,batchSizeBatch的行数,建议设置为50~100。

    • 您可以通过增加写入并发的方式增加写入吞吐。

常见问题

Q:连接时报错Read timed out是什么原因?

A:Druid连接池默认超时时间为10秒,因此会出现Read timed out报错。您可以在连接串中添加socketTimeout参数来设置超时时间,单位为毫秒(ms)。例如设置超时时长为2分钟(120,000毫秒):jdbc:mysql://ld-uf6k8yqb741t3****-proxy-sql-lindorm-public.lindorm.rds.aliyuncs.com:33060/" + database + "?sslMode=disabled&allowPublicKeyRetrieval=true&useServerPrepStmts=true&useLocalSessionState=true&rewriteBatchedStatements=true&cachePrepStmts=true&prepStmtCacheSize=100&prepStmtCacheSqlLimit=50000000&socketTimeout=120000