Java:与 Hikari 数据源对象的并发
Java: Concurency with HikariDataSource Object
我有一个 class 看起来像这样:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
public class ConnectionPool {
private HikariDataSource hds;
private final String propertyFileName;
public ConnectionPool(String propertyFileName) {
if (propertyFileName == null) {
throw new IllegalArgumentException("propertyFileName can't be null");
}
this.propertyFileName = propertyFileName;
reloadFile();
}
public void reloadFile() {
if (hds != null) {
hds.close();
}
hds = new HikariDataSource(new HikariConfig(propertyFileName));
}
public HikariDataSource getHikariDataSource() {
return hds;
}
public String getPropertyFileName() {
return propertyFileName;
}
public void executeQuery(final String sql, final CallBack<ResultSet, SQLException> callBack) {
new Thread(new Runnable() {
@Override
public void run() {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
connection = hds.getConnection();
preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();
callBack.call(resultSet, null);
} catch (SQLException e) {
callBack.call(null, e);
} finally {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException ignored) {}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException ignored) {}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException ignored) {}
}
}
}
}).start();
}
public void executeUpdate(final String sql, final CallBack<Integer, SQLException> callBack) {
//TODO
}
public void execute(final String sql, final CallBack<Boolean, SQLException> callBack) {
//TODO
}
public void connection(final String sql, final CallBack<Connection, SQLException> callBack) {
//TODO
}
}
问题是 reloadFile()
方法可以从使用 hds
的不同线程调用。因此,当我在另一个线程中使用它的连接对象时,hds
可能已关闭。解决这个问题的最佳方法是什么?我应该在创建新的 HikariDataSource
对象后等待几秒钟再关闭旧对象(直到查询完成)吗?
编辑:另一个问题:hds
应该是 volatile
,以便所有线程都可以看到 hds
的更改吗?
几个选项:
同步对数据源的所有访问,以便只有一个线程可以处理它。不可扩展,但可行。
滚动您自己的连接池,例如 Apache Commons Pooling,以便每次访问(无论线程如何)都请求一个数据源,并且池会根据需要创建一个数据源。是否会扰乱数据 ACID,仅取决于是否需要脏数据、何时刷新数据、事务性等
每个线程也可以使用ThreadLocal 拥有自己的数据源,这样每个线程都完全独立于彼此。同样,数据质量可能是一个问题,如果您有 "lots" 个线程(取决于您的定义)并且打开的连接过多会导致客户端或服务器出现资源问题,则资源可能是一个问题。
在 HikariDataSource
中的源代码中进行了非常非常快速和简要的查看。在其 close()
中,它正在调用其内部 HikariPool
的 shutdown()
方法,为此它将尝试正确关闭池连接。
如果你甚至想避免任何 in-progress 连接被强制关闭的机会,一种方法是使用 ReadWriteLock
:
public class ConnectionPool {
private HikariDataSource hds;
private ReentrantReadWriteLock dsLock = ....;
//....
public void reloadFile() {
dsLock.writeLock().lock();
try {
if (hds != null) {
hds.close();
}
hds = new HikariDataSource(new HikariConfig(propertyFileName));
} finally {
dsLock.writeLock().unlock();
}
}
public void executeQuery(final String sql, final CallBack<ResultSet, SQLException> callBack) {
new Thread(new Runnable() {
@Override
public void run() {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
dsLock.readLock().lock();
try {
connection = hds.getConnection();
// ....
} catch (SQLException e) {
callBack.call(null, e);
} finally {
// your other cleanups
dsLock.readLock().unlock();
}
}
}).start();
}
//....
}
这将确保
- 多线程可以访问您的数据源(以获取连接等)
- 数据源的重新加载需要等到使用数据源的线程完成
- 没有线程在重新加载时能够使用数据源获取连接。
你究竟为什么要让 HikariCP 重新加载?许多重要的池参数(minimumIdle
、maximumPoolSize
、connectionTimeout
等)都可以在运行时通过 JMX bean 进行控制,而无需重新启动池。
重新启动池是 "hang" 您的应用程序在连接关闭和重建时持续几秒钟的好方法。如果您不能通过 JMX 界面执行您需要的操作,Adrian 的建议似乎是一个相当合理的解决方案。
其他解决方案也是可能的,但更复杂。
编辑:仅供娱乐,这里是更复杂的解决方案...
public class ConnectionPool {
private AtomicReference<HikariDataSource> hds;
public ConnectionPool(String propertyFileName) {
hds = new AtomicReference<>();
...
}
public void reloadFile() {
final HikariDataSource ds = hds.getAndSet(new HikariDataSource(new HikariConfig(propertyFileName)));
if (ds != null) {
new Thread(new Runnable() {
public void run() {
ObjectName poolName = new ObjectName("com.zaxxer.hikari:type=Pool (" + ds.getPoolName() + ")");
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
HikariPoolMXBean poolProxy = JMX.newMXBeanProxy(mBeanServer, poolName, HikariPoolMXBean.class);
poolProxy.softEvictConnections();
do {
Thread.sleep(500);
} while (poolProxy.getActiveConnections() > 0);
ds.close();
}
}).start();
}
}
public HikariDataSource getHikariDataSource() {
return hds.get();
}
public void executeQuery(final String sql, final CallBack<ResultSet, SQLException> callBack) {
new Thread(new Runnable() {
@Override
public void run() {
...
try {
connection = getHikariDataSource().getConnection();
...
}
}
}).start();
}
}
这将(原子地)换出池,并启动一个线程,等待所有活动连接返回,然后关闭孤立的池实例。
这假设您让 HikariCP 生成唯一的池名称,即不在您的属性中设置 poolName
,并且 registerMbeans=true
.
我有一个 class 看起来像这样:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
public class ConnectionPool {
private HikariDataSource hds;
private final String propertyFileName;
public ConnectionPool(String propertyFileName) {
if (propertyFileName == null) {
throw new IllegalArgumentException("propertyFileName can't be null");
}
this.propertyFileName = propertyFileName;
reloadFile();
}
public void reloadFile() {
if (hds != null) {
hds.close();
}
hds = new HikariDataSource(new HikariConfig(propertyFileName));
}
public HikariDataSource getHikariDataSource() {
return hds;
}
public String getPropertyFileName() {
return propertyFileName;
}
public void executeQuery(final String sql, final CallBack<ResultSet, SQLException> callBack) {
new Thread(new Runnable() {
@Override
public void run() {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
connection = hds.getConnection();
preparedStatement = connection.prepareStatement(sql);
resultSet = preparedStatement.executeQuery();
callBack.call(resultSet, null);
} catch (SQLException e) {
callBack.call(null, e);
} finally {
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException ignored) {}
}
if (preparedStatement != null) {
try {
preparedStatement.close();
} catch (SQLException ignored) {}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException ignored) {}
}
}
}
}).start();
}
public void executeUpdate(final String sql, final CallBack<Integer, SQLException> callBack) {
//TODO
}
public void execute(final String sql, final CallBack<Boolean, SQLException> callBack) {
//TODO
}
public void connection(final String sql, final CallBack<Connection, SQLException> callBack) {
//TODO
}
}
问题是 reloadFile()
方法可以从使用 hds
的不同线程调用。因此,当我在另一个线程中使用它的连接对象时,hds
可能已关闭。解决这个问题的最佳方法是什么?我应该在创建新的 HikariDataSource
对象后等待几秒钟再关闭旧对象(直到查询完成)吗?
编辑:另一个问题:hds
应该是 volatile
,以便所有线程都可以看到 hds
的更改吗?
几个选项:
同步对数据源的所有访问,以便只有一个线程可以处理它。不可扩展,但可行。
滚动您自己的连接池,例如 Apache Commons Pooling,以便每次访问(无论线程如何)都请求一个数据源,并且池会根据需要创建一个数据源。是否会扰乱数据 ACID,仅取决于是否需要脏数据、何时刷新数据、事务性等
每个线程也可以使用ThreadLocal 拥有自己的数据源,这样每个线程都完全独立于彼此。同样,数据质量可能是一个问题,如果您有 "lots" 个线程(取决于您的定义)并且打开的连接过多会导致客户端或服务器出现资源问题,则资源可能是一个问题。
在 HikariDataSource
中的源代码中进行了非常非常快速和简要的查看。在其 close()
中,它正在调用其内部 HikariPool
的 shutdown()
方法,为此它将尝试正确关闭池连接。
如果你甚至想避免任何 in-progress 连接被强制关闭的机会,一种方法是使用 ReadWriteLock
:
public class ConnectionPool {
private HikariDataSource hds;
private ReentrantReadWriteLock dsLock = ....;
//....
public void reloadFile() {
dsLock.writeLock().lock();
try {
if (hds != null) {
hds.close();
}
hds = new HikariDataSource(new HikariConfig(propertyFileName));
} finally {
dsLock.writeLock().unlock();
}
}
public void executeQuery(final String sql, final CallBack<ResultSet, SQLException> callBack) {
new Thread(new Runnable() {
@Override
public void run() {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
dsLock.readLock().lock();
try {
connection = hds.getConnection();
// ....
} catch (SQLException e) {
callBack.call(null, e);
} finally {
// your other cleanups
dsLock.readLock().unlock();
}
}
}).start();
}
//....
}
这将确保
- 多线程可以访问您的数据源(以获取连接等)
- 数据源的重新加载需要等到使用数据源的线程完成
- 没有线程在重新加载时能够使用数据源获取连接。
你究竟为什么要让 HikariCP 重新加载?许多重要的池参数(minimumIdle
、maximumPoolSize
、connectionTimeout
等)都可以在运行时通过 JMX bean 进行控制,而无需重新启动池。
重新启动池是 "hang" 您的应用程序在连接关闭和重建时持续几秒钟的好方法。如果您不能通过 JMX 界面执行您需要的操作,Adrian 的建议似乎是一个相当合理的解决方案。
其他解决方案也是可能的,但更复杂。
编辑:仅供娱乐,这里是更复杂的解决方案...
public class ConnectionPool {
private AtomicReference<HikariDataSource> hds;
public ConnectionPool(String propertyFileName) {
hds = new AtomicReference<>();
...
}
public void reloadFile() {
final HikariDataSource ds = hds.getAndSet(new HikariDataSource(new HikariConfig(propertyFileName)));
if (ds != null) {
new Thread(new Runnable() {
public void run() {
ObjectName poolName = new ObjectName("com.zaxxer.hikari:type=Pool (" + ds.getPoolName() + ")");
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
HikariPoolMXBean poolProxy = JMX.newMXBeanProxy(mBeanServer, poolName, HikariPoolMXBean.class);
poolProxy.softEvictConnections();
do {
Thread.sleep(500);
} while (poolProxy.getActiveConnections() > 0);
ds.close();
}
}).start();
}
}
public HikariDataSource getHikariDataSource() {
return hds.get();
}
public void executeQuery(final String sql, final CallBack<ResultSet, SQLException> callBack) {
new Thread(new Runnable() {
@Override
public void run() {
...
try {
connection = getHikariDataSource().getConnection();
...
}
}
}).start();
}
}
这将(原子地)换出池,并启动一个线程,等待所有活动连接返回,然后关闭孤立的池实例。
这假设您让 HikariCP 生成唯一的池名称,即不在您的属性中设置 poolName
,并且 registerMbeans=true
.