Добро пожаловать в форум, Guest  >>   Войти | Регистрация | Поиск | Правила | В избранное | Подписаться
Все форумы / Java Новый топик    Ответить
 Hibernate блокировки - как работает пример?  [new]
faustgreen
Member

Откуда:
Сообщений: 473
Реализация примера из официальной доки по хиберу - оптимистическая блокировка с исключением определенного поля при проверки ентити на изменение (https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#locking-optimistic (глава - Excluding attributes)).
+
public class TestExcludingTrackingForPropertyWithOptimistikLocking2 extends BaseTest{

	public static void main(String[] args) {

		doInJPA(entityManager -> {

			Query query = entityManager.createQuery("Delete from Phone24");
			query.executeUpdate();
			
			Phone24 phone = new Phone24();
			phone.setId(1L);
			phone.setNumber("+123-456-3333");
			phone.setCallCount(0);
			
			entityManager.persist(phone);
			
		});
		
		doInJPA(entityManager -> {
			
			int isolationLevel = entityManager.unwrap(Session.class).doReturningWork(Connection::getTransactionIsolation);
			System.out.println("Isolation level is [" + isolationLevel + "]");
			
			Phone24 phone = entityManager.find(Phone24.class, 1L);
			phone.setNumber("+123-456-0000");

			doInJPA(entityManager2 -> {
				Phone24 _phone = entityManager2.find(Phone24.class, 1L);
				_phone.incrementCallCount();
				System.out.println("Bob changes the Phone call count");
			});
			
			System.out.println("Alice changes the Phone number");
			
		});
		
		entityManagerFactory.close();
		
	}
}

@Entity(name = "Phone24")
@Table(name = "Phones24")
class Phone24 {

	@Id
	private Long id;
	@Column(name = "`number`")
	private String number;
	@OptimisticLock(excluded = true)
	private long callCount;
	@Version
	private Long version;

	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	public String getNumber() {
		return number;
	}
	public void setNumber(String number) {
		this.number = number;
	}
	public long getCallCount() {
		return callCount;
	}
	public void setCallCount(long callCount) {
		this.callCount = callCount;
	}
	public Long getVersion() {
		return version;
	}
	public void setVersion(Long version) {
		this.version = version;
	}

	public void incrementCallCount() {
		this.callCount++;
	}
}

Метод DoInJPA:
+
protected static void doInJPA(Consumer<EntityManager> code) {

		if (entityManagerFactory == null){
			buildDefaultEntityManagerFactory();
		}
		
		EntityManager entityManager = null;
		EntityTransaction txn = null;
		try {
			entityManager = entityManagerFactory.createEntityManager();

			txn = entityManager.getTransaction();
			txn.begin();

			code.accept(entityManager);

			txn.commit();
		} catch (RuntimeException e) {
			if (txn != null && txn.isActive())
				txn.rollback();
			throw e;
		} finally {
			if (entityManager != null) {
				entityManager.close();
			}
		}
	}

Здесь все работает как положено.
1) В транзацкции №1 создается запись в бд.
2) В транзакции №2 эта запись читается, после чего изменяется состояние entity (поле "номер").
3) Затем запускается вложенная транзакция №3, которая меняет поле callCount и завершается.
4) В конце завершается транзакция №2.

Консоль:
+
Hibernate: delete from Phones24
Hibernate: insert into Phones24 (callCount, `number`, version, id) values (?, ?, ?, ?)
Isolation level is [4]
Hibernate: select phone24x0_.id as id1_182_0_, phone24x0_.callCount as callcoun2_182_0_, phone24x0_.`number` as number3_182_0_, phone24x0_.version as version4_182_0_ from Phones24 phone24x0_ where phone24x0_.id=?
Hibernate: select phone24x0_.id as id1_182_0_, phone24x0_.callCount as callcoun2_182_0_, phone24x0_.`number` as number3_182_0_, phone24x0_.version as version4_182_0_ from Phones24 phone24x0_ where phone24x0_.id=?
Bob changes the Phone call count
Hibernate: update Phones24 set callCount=?, `number`=?, version=? where id=? and version=?
Alice changes the Phone number
Hibernate: update Phones24 set callCount=?, `number`=?, version=? where id=? and version=?

Мне интеречно, что происходит, когда мы помещаем код транзакции №1 в начало транзакции №2:
+
public class TestExcludingTrackingForPropertyWithOptimistikLocking2 extends BaseTest{

	public static void main(String[] args) {

		doInJPA(entityManager -> {

			int isolationLevel = entityManager.unwrap(Session.class).doReturningWork(Connection::getTransactionIsolation);
			System.out.println("Isolation level is [" + isolationLevel + "]");
			
			Query query = entityManager.createQuery("Delete from Phone24");
			query.executeUpdate();
			
			Phone24 phone = new Phone24();
			phone.setId(1L);
			phone.setNumber("+123-456-3333");
			phone.setCallCount(0);
			
			entityManager.persist(phone);
                        entityManager.flush();
                        entityManager.clear();

			Phone24 phone = entityManager.find(Phone24.class, 1L);
			phone.setNumber("+123-456-0000");

			doInJPA(entityManager2 -> {
				Phone24 _phone = entityManager2.find(Phone24.class, 1L);
				_phone.incrementCallCount();
				System.out.println("Bob changes the Phone call count");
			});
			
			System.out.println("Alice changes the Phone number");
			
		});
		
		entityManagerFactory.close();
		
	}
}

Консоль:
+
Isolation level is [4]
Hibernate: delete from Phones24
Hibernate: insert into Phones24 (callCount, `number`, version, id) values (?, ?, ?, ?)
Hibernate: select phone24x0_.id as id1_182_0_, phone24x0_.callCount as callcoun2_182_0_, phone24x0_.`number` as number3_182_0_, phone24x0_.version as version4_182_0_ from Phones24 phone24x0_ where phone24x0_.id=?
Hibernate: select phone24x0_.id as id1_182_0_, phone24x0_.callCount as callcoun2_182_0_, phone24x0_.`number` as number3_182_0_, phone24x0_.version as version4_182_0_ from Phones24 phone24x0_ where phone24x0_.id=?
Bob changes the Phone call count
Hibernate: update Phones24 set callCount=?, `number`=?, version=? where id=? and version=?

**** Тут мы долго висим *****

ERROR: Lock wait timeout exceeded; try restarting transaction
Exception in thread "main" javax.persistence.RollbackException: Error while committing the transaction
	at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:81)
	at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:104)
	at by.pva.hibernate.part01._myUtils.BaseTest.doInJPA(BaseTest.java:44)
	at by.pva.hibernate.part01.locking.optimistic.TestExcludingTrackingForPropertyWithOptimistikLocking2.lambda$1(TestExcludingTrackingForPropertyWithOptimistikLocking2.java:53)
	at by.pva.hibernate.part01._myUtils.BaseTest.doInJPA(BaseTest.java:42)
	at by.pva.hibernate.part01.locking.optimistic.TestExcludingTrackingForPropertyWithOptimistikLocking2.main(TestExcludingTrackingForPropertyWithOptimistikLocking2.java:33)
Caused by: javax.persistence.PessimisticLockException: could not execute statement
	at org.hibernate.internal.ExceptionConverterImpl.wrapLockException(ExceptionConverterImpl.java:273)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:108)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
	at org.hibernate.internal.ExceptionConverterImpl.convertCommitException(ExceptionConverterImpl.java:65)
	... 5 more
Caused by: org.hibernate.PessimisticLockException: could not execute statement
	at org.hibernate.dialect.MySQLDialect$3.convert(MySQLDialect.java:537)
	at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42)
	at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:113)
	at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:99)
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:200)
	at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3449)
	at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:3311)
	at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:3723)
	at org.hibernate.action.internal.EntityUpdateAction.execute(EntityUpdateAction.java:201)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:604)
	at org.hibernate.engine.spi.ActionQueue.lambda$executeActions$1(ActionQueue.java:478)
	at java.util.LinkedHashMap.forEach(Unknown Source)
	at org.hibernate.engine.spi.ActionQueue.executeActions(ActionQueue.java:475)
	at org.hibernate.event.internal.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:348)
	at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:40)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:102)
	at org.hibernate.internal.SessionImpl.doFlush(SessionImpl.java:1360)
	at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:451)
	at org.hibernate.internal.SessionImpl.flushBeforeTransactionCompletion(SessionImpl.java:3210)
	at org.hibernate.internal.SessionImpl.beforeTransactionCompletion(SessionImpl.java:2378)
	at org.hibernate.engine.jdbc.internal.JdbcCoordinatorImpl.beforeTransactionCompletion(JdbcCoordinatorImpl.java:447)
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.beforeCompletionCallback(JdbcResourceLocalTransactionCoordinatorImpl.java:183)
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl.access$300(JdbcResourceLocalTransactionCoordinatorImpl.java:40)
	at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.commit(JdbcResourceLocalTransactionCoordinatorImpl.java:281)
	at org.hibernate.engine.transaction.internal.TransactionImpl.commit(TransactionImpl.java:101)
	... 4 more
Caused by: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:123)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1092)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1040)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1347)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:1025)
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:197)
	... 24 more
7 май 21, 16:06    [22319576]     Ответить | Цитировать Сообщить модератору
 Re: Hibernate блокировки - как работает пример?  [new]
faustgreen
Member

Откуда:
Сообщений: 473
Пробовал расставить задержки и посмотреть, что происходит в бд:
+
		doInJPA(entityManager -> {
			
			int isolationLevel = entityManager.unwrap(Session.class).doReturningWork(Connection::getTransactionIsolation);
			System.out.println("Isolation level is [" + isolationLevel + "]");

			Query query = entityManager.createQuery("Delete from Phone24");
			query.executeUpdate();
		
			System.out.println("after delete");
			try {
				Thread.sleep(30000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			Phone24 phonex = new Phone24();
			phonex.setId(1L);
			phonex.setNumber("+123-456-3333");
			phonex.setCallCount(0);
			
			entityManager.persist(phonex);
			entityManager.flush();
			entityManager.clear();
			
			System.out.println("after persist");
			try {
				Thread.sleep(30000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			Phone24 phone = entityManager.find(Phone24.class, 1L);
			phone.setNumber("+123-456-0000");

			doInJPA(entityManager2 -> {
				Phone24 _phone = entityManager2.find(Phone24.class, 1L);
				_phone.incrementCallCount();
				//_phone.setNumber("+7098765443"); // Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [by.pva.hibernate.part01.locking.optimistic.Phone24#1]
				System.out.println("Bob changes the Phone call count");
			});


В базе никакие изменения не происходили (удаление и сохранение новой записи). Но при этом Hiber выводит лог (delete ... ; insert ...;).
Правильно ли я понимаю, что до конца транзакции никакие фзические модификации бд не происходят,
а все это деле делается виртуально?
А эксепшен в последнем примере вызван тем, что встроенная транзакция №3 не может завершится из-за того,
что запись вроде еще не существует (она должна появиться после выполнения транзакции №2, но она в свою очередь ждет завершения транзакции №3)?

Сообщение было отредактировано: 7 май 21, 16:22
7 май 21, 16:29    [22319588]     Ответить | Цитировать Сообщить модератору
 Re: Hibernate блокировки - как работает пример?  [new]
Stanislav Bashkyrtsev
Member

Откуда: СПб
Сообщений: 142
Две транзакции не будут видеть изменения друг друга пока одна из них не закоммитит изменения. Однако если они изменяют одну и ту же строку (UPDATE/DELETE), то 2ая будет заблокирована и будет ждать пока первая не закончится.

Однако в твоем примере я не вижу такой ситуации пока. Может в таблице уже эта строка на момент когда ты запускаешь пример? Например, осталась с прошлых запусков?
7 май 21, 16:51    [22319597]     Ответить | Цитировать Сообщить модератору
 Re: Hibernate блокировки - как работает пример?  [new]
faustgreen
Member

Откуда:
Сообщений: 473
Stanislav Bashkyrtsev, да она есть на момент запуска.
Но почему все работает, когда очистка таблицы и создание новой записи вынесена в отдельную транзакцию, и не работает когда все это совмещено? И в первом и во втором примере происходит изменение одной и той же энтити (записи в бд).

Сообщение было отредактировано: 7 май 21, 17:13
7 май 21, 17:15    [22319612]     Ответить | Цитировать Сообщить модератору
 Re: Hibernate блокировки - как работает пример?  [new]
faustgreen
Member

Откуда:
Сообщений: 473
Да и вроде ж как для энтити используется оптимистичная блокировка. (flush mode и isolation lvl дефолтные, бд - MySql).
Так если Боб в своей транзакции (в первом рабочем примере) вместе с изменением количества вызовов изменит и номер - то сработает optimistic locking и выпадет соответсвующий эксепшн.
7 май 21, 17:29    [22319616]     Ответить | Цитировать Сообщить модератору
 Re: Hibernate блокировки - как работает пример?  [new]
faustgreen
Member

Откуда:
Сообщений: 473
Интуиция подсказывает, что решение искать нужно где то тут:

В первом случае когда транзакции №2 и №3 изменяют одну и ту же запись, она существует в бд (очистка таблицы и вставка записи уже закомичены в перврой транзакции). Т.е. они работают с физически существующей записью.

Во втором же случае запись перед стартом существует, потом вторая транзакция удаляет все и создает новую запись и потом ее изменяет (я так понимаю где то во временных таблицах), а вот с чем работает транзакция №3 - с изначальной записью, которая была при запуске примера или записью, которую создала транзакция №2 (возможно это разные записи).

Это все догадки, я еще толком не понимаю как это работает ...

Сообщение было отредактировано: 7 май 21, 17:36
7 май 21, 17:44    [22319619]     Ответить | Цитировать Сообщить модератору
 Re: Hibernate блокировки - как работает пример?  [new]
faustgreen
Member

Откуда:
Сообщений: 473
И еще момент, раз до конца транзакции изменения физически не происходят в таблице, но при этом лог хибернейта выводит, что изменения произошли (delete ..., insert ..., ), то получается что он отсылает все запросы (*.executeQuery()) в бд а она уже на своей стороне решает что с ними делать ? (Разруливает блокировки). Т.е. решение надо искать в доках СУБД?
7 май 21, 17:54    [22319621]     Ответить | Цитировать Сообщить модератору
 Re: Hibernate блокировки - как работает пример?  [new]
PetroNotC Sharp
Member

Откуда:
Сообщений: 8257
faustgreen,
Сабж о двух параллельных транзакциях. Зачем ты все запутал третьей?
7 май 21, 19:05    [22319636]     Ответить | Цитировать Сообщить модератору
 Re: Hibernate блокировки - как работает пример?  [new]
Андрей Панфилов
Member

Откуда: Москва > Melbourne
Сообщений: 3852
faustgreen,

вам надо копать в направлении работы RR (repeatable read) в MySQL с оптимистичными блокировками хибера:
- то что во втором случае вылетает PessimisticLockException - судя по всем до запуска теста в БД что-то уже есть, и эту строку внутренняя транзакция видит
- а вот то что в первом случае все проходит чисто - мне не особо очевидно (видно опыт основной с Oracle и PostgreSQL, где RC по умолчанию, дает о себе знать): раз там RR, то финальный апдейт от внешней транзакции не видит обновление версии, которое произвела внутренняя транзакция, хотя чего оно (БД) не ругается на то что обновляемая строка поменялась для меня загадка (ибо нет опыта с RR и MySQL)

т.е.:

faustgreen

Здесь все работает как положено.
1) В транзацкции №1 создается запись в бд.
2) В транзакции №2 эта запись читается, после чего изменяется состояние entity (поле "номер").
3) Затем запускается вложенная транзакция №3, которая меняет поле callCount и завершается.
4) В конце завершается транзакция №2.


нифига оно не работает как положено

Сообщение было отредактировано: 7 май 21, 21:04
7 май 21, 21:08    [22319683]     Ответить | Цитировать Сообщить модератору
 Re: Hibernate блокировки - как работает пример?  [new]
Stanislav Bashkyrtsev
Member

Откуда: СПб
Сообщений: 142
faustgreen
Stanislav Bashkyrtsev, да она есть на момент запуска.
Но почему все работает, когда очистка таблицы и создание новой записи вынесена в отдельную транзакцию, и не работает когда все это совмещено? И в первом и во втором примере происходит изменение одной и той же энтити (записи в бд).
Это правильное замечание :) Т.к. в твоем случае FlushMode=AUTO, то Hibernate будет флушить изменения только в этих случаях:
1. Перед коммитом транзакции
2. Перед select'ом
3. Если вручную вызвать flush()

В твоем же коде ничего такого не происходит. Т.е. ты поменял Entity, однако UPDATE не произойдет сразу. В твоем случае он будет происходить только по выходу из doInJPA(). Поэтому 1ое обновление на самом деле запись не лочит. И 2ая транзакция спокойно обновляет запись.

А вот entityManager.createQuery("Delete from Phone24") - вот он сразу выполняется, т.к. запрос написан явно. Вот этот DELETE лочит запись и не дает 2ой транзакции ее обновить.
faustgreen
В первом случае когда транзакции №2 и №3 изменяют одну и ту же запись, она существует в бд (очистка таблицы и вставка записи уже закомичены в перврой транзакции).
Если же таблица пустая на момент старта, то этот DELETE ничего не залочит, INSERT даже если и произойдет (зависит от ID Generation Strategy), то новую запись 2ая транзакция еще не увидит. Ну и т.к. UPDATE во 2ой транзакции ничего не увидит, то и ничего и не заблочит.

Сообщение было отредактировано: 7 май 21, 21:09
7 май 21, 21:12    [22319685]     Ответить | Цитировать Сообщить модератору
 Re: Hibernate блокировки - как работает пример?  [new]
Stanislav Bashkyrtsev
Member

Откуда: СПб
Сообщений: 142
Андрей Панфилов
а вот то что в первом случае все проходит чисто - мне не особо очевидно (видно опыт основной с Oracle и PostgreSQL, где RC по умолчанию, дает о себе знать): раз там RR, то финальный апдейт от внешней транзакции не видит обновление версии, которое произвела внутренняя транзакция, хотя чего оно (БД) не ругается на то что обновляемая строка поменялась для меня загадка (ибо нет опыта с RR и MySQL)
У него стоит exclude:
@OptimisticLock(excluded = true)
private long callCount;
Т.е. версия в принципе не обновляется, так что независимо от уровня изоляции OptimisticLockingException мы не получим если обновляем только это поле.

Сообщение было отредактировано: 7 май 21, 21:18
7 май 21, 21:26    [22319689]     Ответить | Цитировать Сообщить модератору
 Re: Hibernate блокировки - как работает пример?  [new]
faustgreen
Member

Откуда:
Сообщений: 473
Stanislav Bashkyrtsev, Андрей Панфилов спасибо! Общее направление понял, нужно будет почитать по этой теме.
7 май 21, 23:00    [22319721]     Ответить | Цитировать Сообщить модератору
Все форумы / Java Ответить