Рубрики
Без рубрики

Как работает LockModeType.ОПТИМИСТИЧНАЯ работа в JPA и спящий режим

Автор оригинала: Vlad Mihalcea.

В моем предыдущем посте я представил основные концепции блокировки сохраняемости Java.

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

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

Допустим, у нас есть следующая модель предметной области:

Наш пользователь, Алиса, хочет заказать товар. Покупка проходит следующие этапы:

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

Недостатки неявной блокировки

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

doInTransaction(session -> {
	final Product product = (Product) session.get(Product.class, 1L);
	try {
		executeSync(() -> doInTransaction(_session -> {
			Product _product = (Product) _session.get(Product.class, 1L);
			assertNotSame(product, _product);
			_product.setPrice(BigDecimal.valueOf(14.49));
		}));
	} catch (Exception e) {
		fail(e.getMessage());
	}
	OrderLine orderLine = new OrderLine(product);
	session.persist(orderLine);
});

Тест генерирует следующие выходные данные:

#Alice selects a Product
Query:{[select abstractlo0_.id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_.id=?][1]} 

#The price engine selects the Product as well
Query:{[select abstractlo0_.id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_.id=?][1]} 
#The price engine changes the Product price
Query:{[update product set description=?, price=?, version=? where id=? and version=?][USB Flash Drive,14.49,1,1,0]}
#The price engine transaction is committed
DEBUG [pool-2-thread-1]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection

#Alice inserts an OrderLine without realizing the Product price change
Query:{[insert into order_line (id, product_id, unitPrice, version) values (default, ?, ?, ?)][1,12.99,0]}
#Alice transaction is committed unaware of the Product state change
DEBUG [main]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection

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

Явная блокировка на помощь

Сохраняемость Java LockModeType.ОПТИМИСТИЧНЫЙ является подходящим кандидатом для таких сценариев, поэтому мы собираемся проверить его.

Hibernate поставляется с Преобразователем режима блокировки утилитой, которая может сопоставить любую сохраняемость Java LockModeType с соответствующим режимом гибернации LockMode .

Для простоты мы будем использовать специальный режим блокировки для режима гибернации.ОПТИМИСТИЧНЫЙ , который фактически идентичен своему аналогу с сохраняемостью Java.

Согласно документации Hibernate, режим ЯВНОЙ ОПТИМИСТИЧНОЙ блокировки будет:

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

Я скорректирую наш тестовый случай, чтобы вместо этого использовать явную ОПТИМИСТИЧЕСКУЮ блокировку:

try {
    doInTransaction(session -> {
        final Product product = 
            (Product) session.get(Product.class, 1L, new LockOptions(LockMode.OPTIMISTIC));

        executeSync(() -> {
            doInTransaction(_session -> {
                Product _product = (Product) _session.get(Product.class, 1L);
                assertNotSame(product, _product);
                _product.setPrice(BigDecimal.valueOf(14.49));
            });
        });

        OrderLine orderLine = new OrderLine(product);
        session.persist(orderLine);
    });
    fail("It should have thrown OptimisticEntityLockException!");
} catch (OptimisticEntityLockException expected) {
    LOGGER.info("Failure: ", expected);
}

Новая тестовая версия генерирует следующие выходные данные:

#Alice selects a Product
Query:{[select abstractlo0_.id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_.id=?][1]} 

#The price engine selects the Product as well
Query:{[select abstractlo0_.id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_.id=?][1]} 
#The price engine changes the Product price
Query:{[update product set description=?, price=?, version=? where id=? and version=?][USB Flash Drive,14.49,1,1,0]} 
#The price engine transaction is committed
DEBUG [pool-1-thread-1]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection

#Alice inserts an OrderLine
Query:{[insert into order_line (id, product_id, unitPrice, version) values (default, ?, ?, ?)][1,12.99,0]} 
#Alice transaction verifies the Product version
Query:{[select version from product where id =?][1]} 
#Alice transaction is rolled back due to Product version mismatch
INFO  [main]: c.v.h.m.l.c.LockModeOptimisticTest - Failure: 
org.hibernate.OptimisticLockException: Newer version [1] of entity [[com.vladmihalcea.hibernate.masterclass.laboratory.concurrency.AbstractLockModeOptimisticTest$Product#1]] found in database

Поток операций выглядит следующим образом:

Версия продукта проверяется в конце транзакции. Любое несоответствие версий вызывает исключение и откат транзакции.

К сожалению, проверка версии на уровне приложения и фиксация транзакции не являются атомарной операцией. Проверка происходит в entityverifyversionпроцессе , на этапе перед фиксацией транзакции :

public class EntityVerifyVersionProcess implements BeforeTransactionCompletionProcess {
    private final Object object;
    private final EntityEntry entry;

    /**
     * Constructs an EntityVerifyVersionProcess
     *
     * @param object The entity instance
     * @param entry The entity's referenced EntityEntry
     */
    public EntityVerifyVersionProcess(Object object, EntityEntry entry) {
        this.object = object;
        this.entry = entry;
    }

    @Override
    public void doBeforeTransactionCompletion(SessionImplementor session) {
        final EntityPersister persister = entry.getPersister();

        final Object latestVersion = persister.getCurrentVersion( entry.getId(), session );
        if ( !entry.getVersion().equals( latestVersion ) ) {
            throw new OptimisticLockException(
                    object,
                    "Newer version [" + latestVersion +
                            "] of entity [" + MessageHelper.infoString( entry.getEntityName(), entry.getId() ) +
                            "] found in database"
            );
        }
    }
}

Вызов метода AbstractTransactionImpl.commit() выполнит этап перед транзакцией-фиксация , а затем зафиксирует фактическую транзакцию:

@Override
public void commit() throws HibernateException {
    if ( localStatus != LocalStatus.ACTIVE ) {
        throw new TransactionException( "Transaction not successfully started" );
    }

    LOG.debug( "committing" );

    beforeTransactionCommit();

    try {
        doCommit();
        localStatus = LocalStatus.COMMITTED;
        afterTransactionCompletion( Status.STATUS_COMMITTED );
    }
    catch (Exception e) {
        localStatus = LocalStatus.FAILED_COMMIT;
        afterTransactionCompletion( Status.STATUS_UNKNOWN );
        throw new TransactionException( "commit failed", e );
    }
    finally {
        invalidate();
        afterAfterCompletion();
    }
}

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

Явная ОПТИМИСТИЧНАЯ стратегия блокировки обеспечивает ограниченную защиту от аномалий устаревшего состояния. Это состояние гонки является типичным случаем Несоответствия времени проверки времени использования целостности данных.

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

Код доступен на GitHub .