JPA и Spring транзакции - пожалуйста, объясните - PullRequest
1 голос
/ 24 августа 2009

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

У меня есть приложение на основе Spring, использующее Hibernate в качестве поставщика JPA, при поддержке MySQL. У меня есть объекты DAO, расширяющие Spring JpaDaoSupport. На них распространяется управление транзакциями Spring.

Созданный мной тестовый пример работает так: 1) Сущность создана с некоторым счетчиком, установленным в 0. 2) Затем создаются два потока, которые оба вызывают метод DAO incrementCounter () в цикле.

Я думал, что когда методы DAO покрыты транзакцией, тогда в ней будет только один поток (т.е. Spring позаботится о синхронизации). Но это оказалось ложным предположением.

После (временного) решения этой проблемы путем добавления synchronized к методу DAO я обнаружил, что Hibernate не хранит изменения, сделанные методом DAO, а другой поток, когда находя () объект, имеет старый данные. Помог только явный вызов this.getJpaTemplate().flush();.

Я также думал, что менеджер сущностей выдаст мне тот же экземпляр сущности из кэша контекста постоянства, но это также неверно. Я проверил hashCode () и equals (), и они в порядке - на основе бизнес-ключа сущности.

Любые комментарии приветствуются, так как мне кажется, что мне не хватает некоторых базовых понятий о том, как JPA / Spring работает с транзакциями.

  1. Должны ли методы DAO быть synchronized?
  2. Должен ли я вызывать flush () в конце каждого метода DAO?
  3. Является ли Spring ответственным за то, что вызовы методов DAO ведут себя транзакционно? (т.е. не позволяя двум потокам работать с одним и тем же объектом одновременно)
  4. Если нет, то как мне этого добиться?

Обратите внимание, что в моем тестовом примере я использую один объект DAO, но это должно быть нормально, поскольку бины Spring являются синглетонами - верно?

Спасибо за любую помощь.

public class EntityDaoImpl extends JpaDaoSupport implements EntityDao {

public synchronized void incrementCounter( String znacka ) 
{

  String threadName = Thread.currentThread().getName();
  log.info(threadName + " entering do incrementCounter().");

  Entity ent = this.getJpaTemplate().find( Entity.class, znacka );
  log.info("Found an entity "+ent.getZnacka()+"/"+ent.hashCode()+" - " + ObjectUtils.identityToString( ent ) );
  log.info(threadName + ": Actual count: "+ent.getCount() );

  ent.setCount( ent.getCount() + 5 );

  int sleepTime = threadName.endsWith("A") ? 700 : 50;
  try { Thread.sleep( sleepTime ); }
  catch( InterruptedException ex ) {  }

  ent.setCount( ent.getCount() + 5 );
  this.getJpaTemplate().flush();

  log.info(threadName + " leaving incrementCounter().");
}
}

Без synchronized и flush(), это давало мне вывод как

Thread A: Actual count: 220
...
Thread B: Actual count: 220
...
Thread A: Actual count: 240
...
Thread B: Actual count: 250
...
Thread A: Actual count: 250

... и т. Д., Что означает, что один поток перезаписал изменения другого.

Ответы [ 3 ]

3 голосов
/ 18 сентября 2009

Я думал, что когда методы DAO покрыты транзакцией, тогда в ней будет только один поток

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

(т. Е. Spring позаботится о синхронизации).

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

Я также думал, что менеджер сущностей выдаст мне тот же экземпляр сущности из кэша контекста постоянства

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

Должны ли методы DAO быть синхронизированы?

Как уже упоминалось в других публикациях, обычно методы DAO являются методами CRUD, и вы обычно не синхронизируете их, поскольку DAO просто выбирает / обновляет / etc. и не знает более широкий контекст того, что вы делаете.

Является ли Spring ответственным за то, что вызовы методов DAO ведут себя транзакционно? (т.е. не позволяя двум потокам работать с одним и тем же объектом одновременно)

Опять транзакция! = Синхронизация.

Если нет, как мне этого добиться?

Как упоминалось в других публикациях, вы можете выполнить синхронизацию на уровне Java или установить уровень изоляции транзакции в SERIALIZABLE. Другой вариант - использовать синтаксис SELECT FOR UPDATE (например, LockMode.UPGRADE в Hibernate), чтобы сообщить базе данных, что вы собираетесь внести изменение, и база данных должна заблокировать строки.

Если вы не привязаны к пессимистической блокировке, вы можете реализовать оптимистическую блокировку, например используя версию Hibernate. В вашем конкретном примере incrementCounter вы получите множество оптимистичных ошибок блокировки, но можно предположить, что реальное приложение не будет так себя вести.

3 голосов
/ 24 августа 2009
  1. Должны ли методы DAO быть синхронизированы? Мои обычно нет, потому что они не имеют никакого государства. Незачем.
  2. Должен ли я вызывать flush () в конце каждого метода DAO? Нет, я этого не делаю.
  3. Является ли Spring ответственным за то, что вызовы методов DAO ведут себя транзакционно? (т.е. не позволяя двум потокам работать с одним и тем же объектом одновременно). Я думаю, что вы путаете транзакции, изоляцию и синхронизацию.
  4. Если нет, то как мне этого добиться? Я думаю, что вы должны беспокоиться о синхронизации и изоляции.

Ваш пример - это не то, что я бы назвал DAO. Я думаю, что ваш тест на самом деле не правильная идиома. Если бы у меня был объект Foo, у меня был бы интерфейс FooDao, который бы объявлял методы CRUD для этого объекта:

public interface FooDao
{
    List<Foo> find();
    Foo find(Serializable id);
    void saveOrUpdate(Foo foo);
    void delete(Foo foo);
}

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

Spring обычно имеет сервисный уровень, который использует объекты домена и персистентности для реализации вариантов использования. Вот где транзакции должны быть объявлены, потому что единица работы связана с вариантом использования, а не с DAO. У DAO нет возможности узнать, является ли она частью более крупной транзакции. Вот почему транзакции обычно объявляются на сервисе.

1 голос
/ 26 августа 2009

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

Ответ зависит от вашей заявки:

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