Статьи

Шаблоны блокировки гибернации — как работает режим оптимистичной блокировки

Явная оптимистическая блокировка

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

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

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

Вариант использования заказа продукта

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

productorderlineoptimisticlockmode1

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

implicitlockinglockmodenone1

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
doInTransaction(new TransactionCallable<Void>() {
    @Override
    public Void execute(Session session) {
        final Product product = (Product) session.get(Product.class, 1L);
        try {
            executeAndWait(new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    return doInTransaction(new TransactionCallable<Void>() {
                        @Override
                        public Void execute(Session _session) {
                            Product _product = (Product) _session.get(Product.class, 1L);
                            assertNotSame(product, _product);
                            _product.setPrice(BigDecimal.valueOf(14.49));
                            return null;
                        }
                    });
                }
            });
        } catch (Exception e) {
            fail(e.getMessage());
        }
        OrderLine orderLine = new OrderLine(product);
        session.persist(orderLine);
        return null;
    }
});

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
#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 Persistence LockModeType.OPTIMISTIC является подходящим кандидатом для таких сценариев, поэтому мы собираемся проверить его.

Hibernate поставляется с утилитой LockModeConverter , которая способна отобразить любой Java Persistence LockModeType на связанный с ним Hibernate LockMode .

Для простоты мы собираемся использовать специфический для Hibernate LockMode.OPTIMISTIC , который фактически идентичен его аналогу Java для персистентности.

Согласно документации Hibernate явный OPTIMISTIC Lock Mode будет:

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
try {
    doInTransaction(new TransactionCallable<Void>() {
        @Override
        public Void execute(Session session) {
            final Product product = (Product) session.get(Product.class, 1L, new LockOptions(LockMode.OPTIMISTIC));
 
            executeAndWait(new Callable<Void>() {
                @Override
                public Void call() throws Exception {
                    return doInTransaction(new TransactionCallable<Void>() {
                        @Override
                        public Void execute(Session _session) {
                            Product _product = (Product) _session.get(Product.class, 1L);
                            assertNotSame(product, _product);
                            _product.setPrice(BigDecimal.valueOf(14.49));
                            return null;
                        }
                    });
                }
            });
 
            OrderLine orderLine = new OrderLine(product);
            session.persist(orderLine);
            return null;
        }
    });
    fail("It should have thrown OptimisticEntityLockException!");
} catch (OptimisticEntityLockException expected) {
    LOGGER.info("Failure: ", expected);
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
#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

Поток операций выглядит так:

explicitlockinglockmodeoptimistic1

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

Риск состояния гонки

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 () выполнит этап before -action-commit-commit и затем зафиксирует фактическую транзакцию:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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();
    }
}

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

Вывод

Явная стратегия блокировки OPTIMISTIC предлагает ограниченную защиту от аномалий устаревшего состояния. Это состояние гонки является типичным случаем времени проверки на аномалию целостности данных времени использования .

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

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