7.16 JDBC连接池
常见的c3p0之类的连接池,直接向应用分配自己管理的连接。绝大多数JDBC方法,都会抛出SQLException,要求每一处SQL操作都能正确处理连接意外并不现实。典型的,数据库重启之后,初次SQL操作往往会失败,接下来连接池才会恢复所有连接。Mysql的Connector/J,抛出CommunicationsException的问题并不少见。所以Limax提供一个简单的连接池实现,通过区分SQLException决定是否重启操作,避免把可以通过重启操作解决的问题推给应用。
-
7.16.1 基本原理
1. 连接通过consumer的方式提供给应用, SQL操作集合在一个特定范围内完成,这样所有的SQLException才有可能被连接池者监管,consumer才有机会被重启。这里提供两个范围支持,函数范围和线程范围(跨线程转移连接不支持,这也不必要,即便使用常规连接池,线程也是从连接池请求新连接,而不至于私下交换)。
2. 通过重启consumer也顺便解决死锁,超时之类的异常。
-
7.16.2 编程接口
-
接口limax.sql.SQLConnectionConsumer
应用实现这个接口打包SQL操作集合
-
接口limax.sql.SQLExecutor
定义方法void execute(SQLConnectionConsumer consumer) throws Exception
定义常量COMMIT和ROLLBACK,专用于线程范围连接池。
execute(SQLExecutor.COMMIT) 提交事务。
execute(SQLExecutor.ROLLBACK)回滚事务。
-
类limax.sql.SQLPooledExecutor
构造函数:
SQLPooledExecutor(String url, int size, boolean threadAffinity, Consumer<Exception> logger)
url为数据库连接url,size决定了连接池尺寸,threadAffinity决定了该连接池是函数范围还是线程范围,logger用于记录导致consumer重启的那些异常,便于应用排查重启原因,改进设计。
SQLPooledExecutor(String url, int size, Consumer<java.lang.Exception> logger) SQLPooledExecutor(String url, int size, boolean threadAffinity) SQLPooledExecutor(String url, int size)
简化版本的构造函数,其中不含threadAffinity参数的构造函数,默认threadAffinity为false。
构造函数返回时,size个连接已经建立,这样可以确保数据库认可连接url,并且允许建立size个连接,如果构造函数迟迟不返回,应该检查数据库服务器是否启动,这种情况下logger也会不断输出导致重连的异常。
成员函数:
void execute(SQLConnectionConsumer consumer) throws Exception
执行consumer中应用提供的SQL操作集合,consumer不必catch异常,特别是SQLException,应该在execute外catch异常,保证连接池能够检查所有异常决定是否重启,导致重启的异常通过logger记录,不会导致的重启的异常通过execute往外抛。。
void shutdown()
关闭连接池,拒绝新的执行请求。
-
类limax.sql.RestartTransactionException
线程范围连接池使用这个异常来指明事务需要被重启。
-
-
7.16.3 函数范围连接池
一次execute就能完成整个事务,使用函数范围连接池。这种类型的连接池,绝大多数情况够用了。
-
特性
1. 连接初始化为auto-commit方式。
2. 一次execute之后无论成败,连接重新设置为auto-commit方式。
3. 执行多语句事务,首先应该将auto-commit设置为false。
-
示例
public final class MysqlTest { public static void main(String[] args) throws Exception { SQLPooledExecutor executor = new SQLPooledExecutor( "jdbc:mysql://192.168.1.3:3306/test?user=root&password=admin&characterEncoding=utf8", 3, e -> e.printStackTrace()); Thread.sleep(10000); executor.execute(conn -> { conn.setAutoCommit(false); try (PreparedStatement ps = conn.prepareStatement("INSERT INTO test(name) VALUES(?)")) { ps.setString(1, "A"); ps.executeUpdate(); ps.setString(1, "B"); ps.executeUpdate(); } }); executor.shutdown(); } }
在sleep的10秒期间,重启数据库,观察输出,执行结束后两条记录均被插入数据库。
-
-
7.16.4 线程范围连接池
线程范围连接池跨越多个execute操作,除非复杂应用,这种类型的连接池很少用到,最典型的应用是limax.zdb.LoggerMysql.java中的writer连接池,一次checkpoint操作发起的全部修改动作打包为一个事务。
-
特性
1. 连接初始化为非auto-commit方式,consumer禁止设置auto-commit。
2. 线程首次调用execute,连接分配给线程,意味着事务的开始。
3. 任何一次execute抛出异常或者以SQLExecutor.COMMIT,SQLExecutor.ROLLBACK作为consumer调用execute,连接将归还连接池,事务结束。
4. 任何一次execute抛出异常,事务回滚并结束。其中,RestartTransactionException指示应用可以重启事务。
-
示例
public final class MysqlTest { private static SQLPooledExecutor executor = new SQLPooledExecutor( "jdbc:mysql://192.168.1.3:3306/test?user=root&password=admin&characterEncoding=utf8", 3, true); private static void insert(String s) throws Exception { System.out.println("insert " + s); executor.execute(conn -> { try (PreparedStatement ps = conn.prepareStatement("INSERT INTO test(name) VALUES(?)")) { ps.setString(1, s); ps.executeUpdate(); } }); } public static void main(String[] args) throws Exception { while (true) { try { insert("A"); Thread.sleep(5000); insert("B"); Thread.sleep(5000); insert("C"); executor.execute(SQLExecutor.COMMIT); Thread.sleep(5000); } catch (RestartTransactionException e) { System.out.println("restart"); } } } }
程序启动之后,任何时间点重启数据库,可以看到类似如下的结果。
insert A
insert B
insert C
restart
insert A
restart
insert A
restart
insert A
insert B
发生了3次restart,原因在于,连接池size = 3,轮转分配,首次restart之后的两次insert A,触发了后续连接的恢复。
可以看出,无论以任何方式重启数据库,ABC插入数据库的事务完整性总能得到保证。
特别注意, executor.execute(SQLExecutor.COMMIT);跟其它SQL操作在同一个try块内, 千万不能习惯性放到finally块,这个操作同样有可能抛出异常。
-
-
7.16.5 高级话题
1. SQLConnectionConsumer约束了应用实现,同时也就约束了更好的事务完整性。
2. 既然操作可能重启,那么必须设计可重启的代码,比如某些参数多次执行不可改变。
3. 一些大型结果集的SELECT操作,也许设计允许失败,这种情况下,应该判断操作是否重启,作出相应决策。
4. 如果数据库服务器长时间维护,应用也许会表现为不响应,碰到这种情况应该检查logger输出,检查连接状态。
5. RestartTransactionException从RuntimeException派生,而不是SQLException,这是应用复杂性决定的,也许调用栈内大多数方法都跟SQL毫无关系,可以参考limax.zdb.Checkpoint.java中的使用。