Я написал этот класс для перезагрузки DataSource, используемого всем приложением, при изменении сохраненных данных конфигурации.
Как видите, он управляется CDI и отображается как Singleton, а событие «конфигурация изменена» поступает через метод configurationReload(...), но сейчас это не актуально.
Эталонное обновление охраняется ReentrantReadWriteLock, но мне интересно, нужно ли оно вообще.
@Singleton
@ThreadSafe
class ReloadingDataSource implements DataSource {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
@GuardedBy("readWriteLock")
private DataSource delegateDataSource;
@Inject
ReloadingDataSource(@Nonnull final Configuration configuration) {
delegateDataSource = createDataSource(configuration);
}
private DataSource createDataSource(final Configuration configuration) {
... Create a ComboPooledDataSource using properties extracted from Configuration.
}
@Override
public Connection getConnection() throws SQLException {
readLock.lock();
try {
return delegateDataSource.getConnection();
} finally {
readLock.unlock();
}
}
...
private void configurationReload(
@Observes @Reload final ConfigurationChanged configurationChanged,
@Nonnull final Configuration configuration) {
final ConfigurationEvent event = configurationChanged.getConfigurationEvent();
if (event.getType() != AbstractFileConfiguration.EVENT_RELOAD && !event.isBeforeUpdate()) {
return;
}
writeLock.lock();
try {
destroyDelegateDataSource();
delegateDataSource = createDataSource(configuration);
} finally {
writeLock.unlock();
}
}
private void destroyDelegateDataSource() {
try {
DataSources.destroy(delegateDataSource);
} catch (final SQLException ignored) {
// Do nothing.
}
}
}
Если мы проигнорируем стоимость создания нового источника данных, может ли вышеуказанная стратегия быть заменена на AtomicReference<DataSource>, как показано ниже?
Это привело бы к повышению производительности и облегчению чтения кода.
Есть ли лучшие способы справиться с этим, о которых я не знаю?
@Singleton
@ThreadSafe
class ReloadingDataSource implements DataSource {
private final AtomicReference<DataSource> delegateDataSource;
@Inject
ReloadingDataSource(@Nonnull final Configuration configuration) {
delegateDataSource = new AtomicReference<>(createDataSource(configuration));
}
private DataSource createDataSource(final Configuration configuration) {
... Create a ComboPooledDataSource using properties extracted from Configuration.
}
@Override
public Connection getConnection() throws SQLException {
return delegateDataSource.get().getConnection();
}
...
private void configurationReload(
@Observes @Reload final ConfigurationChanged configurationChanged,
@Nonnull final Configuration configuration) {
final ConfigurationEvent event = configurationChanged.getConfigurationEvent();
if (event.getType() != AbstractFileConfiguration.EVENT_RELOAD && !event.isBeforeUpdate()) {
return;
}
// Updated as per eckes tip. Is this what you meant?
final DataSource newDataSource = createDataSource(configuration);
while (true) {
final DataSource oldDataSource = delegateDataSource.get();
if (delegateDataSource.compareAndSet(oldDataSource, newDataSource)) {
destroyDelegateDataSource(oldDataSource);
break;
}
}
}
private void destroyDelegateDataSource(final DataSource oldDataSource) {
try {
DataSources.destroy(oldDataSource);
} catch (final SQLException ignored) {
// Do nothing.
}
}
}
если два источника данных открыты одновременно, это не проблема, да, все в порядке. обратите внимание, однако, что в вашем текущем impl у вас может быть несколько вызовов обновления, борющихся за новую версию (если возможно получить одновременные события обновления).
@eckes Ты прав! Я упустил этот момент. Я должен тебе кофе.
@jtahlborn Да, во время создания «первого» нового источника данных может наблюдаться несколько изменений. Но я не понимаю, какую проблему это может создать? Я имею в виду, что «второй» и последующие потоки будут просто ждать получения блокировки.
@eckes Я обновил пример Atomic, как вы предложили. Не могли бы вы проверить, правильно ли я понял, что вы имели в виду? Пропустить compareAndSet, потому что эталонное обновление само по себе является атомарным? Это мне не понятно
обновления конфигурации "заказаны"? если вы начнете обрабатывать update1, а затем появится update2 (с «более новой» информацией), вы можете оказаться в ситуациях, когда окончательный результат основан на потерях изменений update1 и update2.
@jtahlborn Да, обновления заказываются. При обработке update1 я нахожусь в области блокировки записи, поэтому, если появляется update2, он ждет в writeLock.lock (), пока update1 не выполнит writeLock.unlock (). Таким образом, update2 просто переопределяет update1, поскольку он следует тому же пути кода. Но может я ошибаюсь
Я про вашу новую версию, в которой нет блокировки записи?
@jtahlborn Ага, извини, у меня плохо! Что вы предлагаете в таком случае?
Да, новая версия похожа на то, что я предлагал. Я думаю, вы можете обойтись без цикла и вместо этого использовать getAndSet(). Он также вернет старое значение, необходимое для его уничтожения (в конечном итоге). Но я подозреваю, особенно если ваши события изменения однопоточные, оба достаточно быстрые.
@eckes, второй вариант кажется мне очень неработающим, похоже, это какая-то блокировка спина. Предположим, что этот if (delegateDataSource.compareAndSet(oldDataSource, newDataSource)) { возвращает true - на данный момент никто не останавливает delegateDataSource.get(), чтобы продолжить, и, как я понимаю, это должны быть отдельные операции
@Eugene Я буду ждать ответа eckes, потому что я заблудился и не хочу писать чушь.
@eckes getAndSet кажется прекрасным, в основном он завершает то, что я делаю сейчас
Вращение на compareAndSet - это нормальный шаблон для оптимистической блокировки, он должен работать, но в данном случае он не нужен. (Вы можете использовать его, если используете результат get для вычисления чего-либо для набора (например, в счетчике)
Я также предлагаю вместо этого использовать некоторый источник данных для пула, так как это может лучше управлять состоянием открытых или заимствованных соединений. Однако не уверен, какие из них поддерживают чистую реконфигурацию.
@eckes Я использую объединенную реализацию от C3P0. Читая javadoc для уничтожения, он фактически не заставляет ресурсы закрыться. В нем также указано, что ресурсы очищаются при финализации, поэтому я могу просто разыменовать старый источник данных. Может быть...
@LppEdd Если переконфигурирование происходит редко, все в порядке.




Если вам требуется, чтобы ваши обновления обрабатывались упорядоченным образом, вам все равно потребуется заблокировать метод перезагрузки. В этом случае вы можете отказаться от логики AtomicReference и просто использовать volatile:
public class RDS {
private volatile DataSource delegate;
public Connection getConnection() throws SQLException {
return delegate.getConnection();
}
private void reload(Configuration config) {
DataSource old = null;
synchronized(this) {
old = delegate;
delegate = createDataSource(config);
}
destroyDataSource(old);
}
}
Обратите внимание, однако, что у вас все еще потенциально есть другие проблемы, когда соединения могут все еще использоваться для старого источника данных, когда вы его закрываете (упоминается в первом комментарии @eckes к вопросу). Чтобы исправить это, вам понадобится что-то вроде пула соединений с логикой типа получения / выпуска, который закрывает старый делегат после того, как все существующие соединения будут освобождены.
Мне кажется, что если вы можете сгенерировать новый экземпляр, атомарное обновление - это нормально. Однако во всех случаях необходимо учитывать, когда следует уничтожить старый экземпляр. Тот факт, что
getConnectionне выполняется, не означает, что объекты из этого источника данных все еще используются. В зависимости от того, нужно ли вам ссылаться на старое состояние или нет, вы можете вывести createDataStore () из цикла или даже пропустить compareAndSet.