Весна JPA:какова стоимость saveandflush против save?

У меня есть приложение, построенное из микрослужб. Одна служба получает данные, сохраняет их через Spring JPA и Eclipse link, а затем отправляет предупреждение (AMQP) второй службе.

в зависимости от конкретных условий, Вторая служба затем вызывает RESTfull веб-службу против сохраненных данных для получения сохраненной информации.

Я заметил, что иногда служба RESTfull возвращает нулевой набор данных, даже если данные были ранее сохранены. Глядя на код для сохраняющейся службы,сохранить используется вместо saveandflush поэтому я предполагаю, что данные не сбрасываются достаточно быстро для запроса нижестоящей службы.

  • есть ли стоимость с saveandflush, что я должен устать или должен разумно использовать его по умолчанию?
  • обеспечит ли это оперативность доступности данных для нижестоящих приложений?

Я должен сказать, что исходная функция персистентности обернута в @Transactional

1 ответов


возможный прогноз проблемы

думаю, проблема здесь не имеет ничего общего с save vs saveAndFlush. Проблема, кажется, связана с природой весны @Transactional методы и неправомерное использование этих транзакций в распределенной среде, которая включает как вашу базу данных, так и брокера AMQP; и, возможно, добавьте к этой ядовитой смеси некоторые основные недопонимания того, как работает контекст JPA.

в вашем объяснении вы, кажется, подразумеваете что вы начинаете транзакцию JPA в пределах @Transactional метод, и во время транзакции (но до ее совершения) вы отправляете сообщения брокеру AMQP; и позже, на другой стороне очереди, потребительское приложение получает сообщения и делает вызов службы REST. На этом этапе вы заметите, что транзакционные изменения со стороны издателя еще не были зафиксированы в базе данных и поэтому не видны стороне потребителя.

проблема чтобы вы распространяли эти сообщения AMQP в своей транзакции JPA до того, как она зафиксирована на диске. К тому времени, когда потребитель прочитает сообщение и обработает его, транзакция со стороны публикации может быть еще не завершена. Таким образом, эти изменения не видны приложению-потребителю.

если ваша реализация AMPQ-Кролик, то я видел эту проблему раньше: когда вы начинаете @Transactional метод, который использует менеджер транзакций базы данных, и в этом методе вы используете RabbitTemplate отправить соответствующее сообщение.

если RabbitTemplate не использует транзакционный канал (т. е. channelTransacted=true), затем ваше сообщение доставляется до того, как транзакция базы данных зафиксирована. Я считаю, что, включив транзакционные каналы (отключенные по умолчанию) в вашем RabbitTemplate вы решаете часть проблемы.

<rabbit:template id="rabbitTemplate" 
                 connection-factory="connectionFactory" 
                 channel-transacted="true"/>

когда канал transacted, то RabbitTemplate "присоединяется" к текущей транзакции базы данных (которая, по-видимому, является транзакцией JPA). Как только ваш Транзакция JPA фиксирует, она запускает некоторый код эпилога, который также фиксирует изменения в вашем канале кролика, который заставляет фактическую "отправку" сообщения.

о save vs saveAndFlush

вы можете подумать, что промывка изменений в контексте JPA должна была решить проблему, но вы ошибаетесь. Очистка контекста JPA просто заставляет изменения в ваших сущностях (в этот момент только в памяти) записываться на диск, но они все еще записывается на диск в рамках соответствующей транзакции базы данных, которая не будет фиксироваться до фиксации транзакции JPA. Это происходит в конце вашего @Transactional метод (и, к сожалению, через некоторое время после того, как вы уже отправили свои сообщения AMQP - если вы не используете транзакционный канал, как описано выше).

Итак, даже если вы очистите свой контекст JPA, ваш потребительский applicatipn не увидит этих изменений (согласно классическим правилам уровня изоляции базы данных), пока ваш @Transactional способ закончил в приложении издателя.

при вызове save(entity) на EntityManager не нужно синхронизировать любые изменения сразу. Большинство реализаций JPA просто отмечают сущности как грязные в памяти и ждут до последней минуты, чтобы синхронизировать все изменения с базой данных и зафиксировать эти изменения на уровне базы данных.

Примечание: есть случаи, в которых вы можете захотеть, чтобы некоторые из этих изменений спустились на диск сразу, а не до причудливого EntityManager решает сделать так. Классический пример этого происходит, когда в таблице базы данных есть триггер, который необходимо запустить для создания дополнительных записей, которые понадобятся позже во время транзакции. Таким образом, вы заставляете флеш изменений на диск так, что триггер вынужден работать.

сбросив контекст, вы просто заставляете синхронизацию изменений в памяти на диск, но это не означает мгновенную фиксацию базы данных этих изменений. Следовательно, эти изменения вы flush не обязательно будет видимым для других транзакций. Скорее всего, они не будут, основываясь на традиционных уровнях изоляции базы данных.

проблема 2PC

еще одна классическая проблема заключается в том, что ваша база данных и ваш брокер AMQP являются двумя независимыми системами. Если речь идет о кролике, то у вас нет 2PC (двухфазная фиксация).

таким образом, вы можете учитывать интересные сценарии, например, транзакцию базы данных успешно фиксирует, но тогда кролик не сможет зафиксировать ваше сообщение, в случае чего вам придется повторить всю транзакцию, возможно, пропустив побочные эффекты базы данных и просто повторно попытавшись отправить сообщение кролику.

вы, вероятно, должны прочитать эту статью на распределенные транзакции весной, С и без XA, в частности, раздел о цепных транзакциях полезен для решения этой проблемы.

они предлагают более сложный менеджер транзакций определение. Например:

<bean id="jdbcTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<bean id="rabbitTransactionManager" class="org.springframework.amqp.rabbit.transaction.RabbitTransactionManager">
    <property name="connectionFactory" ref="connectionFactory"/>
</bean>

<bean id="chainedTransactionManager" class="org.springframework.data.transaction.ChainedTransactionManager">
    <constructor-arg name="transactionManagers">
        <array>
            <ref bean="rabbitTransactionManager"/>
            <ref bean="jdbcTransactionManager"/>
        </array>
    </constructor-arg>
</bean>

и затем, в вашем коде, вы просто используете этот цепной менеджер транзакций для координации как транзакционной части базы данных, так и транзакционной части кролика.

теперь есть еще потенциал, который вы фиксируете свою часть базы данных, но что ваша часть транзакции кролика терпит неудачу.

Итак, представьте себе что-то вроде этого:

@Retry
@Transactional("chainedTransactionManager")
public void myServiceOperation() {
    if(workNotDone()) {
        doDatabaseTransactionWork();
    }
    sendMessagesToRabbit();
}

таким образом, если ваша транзакционная часть кролика не удалась причина, и Вы были вынуждены повторить всю цепную транзакцию, вы избежали бы повторения побочных эффектов базы данных и просто убедитесь, что отправили неудачное сообщение кролику.

в то же время, если ваша часть базы данных терпит неудачу, то вы никогда не отправляли сообщение кролику, и не было бы никаких проблем.

кроме того, если ваши побочные эффекты базы данных идемпотентны, то вы можете пропустить проверку, просто повторно применить изменения базы данных и просто повторно попытаться отправить сообщение кролику.

правда в том, что изначально то, что вы пытаетесь сделать, кажется обманчиво легким, но как только вы вникаете в различные проблемы и понимаете их, вы понимаете, что это сложный бизнес, чтобы сделать это правильно.