1.19.2015

12. Пессимистическая блокировка. Hibernate



В Hibernate,  пессимистическая блокировка,  реализована блокировками уровня баз данных. То есть при использовании pessimistic locking имеет смысл глянуть, прежде всего, на то, какая СУБД используется, на основе каких блокировок самой СУБД,  могут реализовываться механизмы блокировки Hibernate. Если тип блокировки не поддерживается СУБД, то hibernate пытается выбрать альтернативный  вид блокировки, а не вызывать exception, что в  свою очередь делает код программы более гибким, и позволяет нам использовать, различные Базы Данных.  И так pessimistic locking  случается, когда вы считываете данные (Сущность - Entity) и пытаетесь овладеть данными для монопольного владения или изменения Сущности, блокировка  удерживается пока, транзакция в hibrnate сессии не окончится, так в теории, перейдем к практике.  В Hibernate существует специальный класс LockOptions, который позволяет нам выбрать тип блокировки. Обычно экземпляр этого класса используется в следующих методах:

Session.load(Class, id, LockOptions);
Session.get(Class, id, LockOptions);
Session.lock(Object, LockOptions);
Session.refresh( Object, LockOptions);
Query.setLockMode(alias, LockOptions);

Выбрать  режим блокировки позволяет нам метод  .setLockMode(), который принимает  один единственный параметр типа LockMode. И так,  какие уровни блокировки у нас есть, давайте   начнем с рассмотрения оптимистичных блокировок и закончим пессимистическими  блокировками:

LockMode.NONE - не используются, ни какие блокировки баз данных на уровне запроса, по умолчанию load() и get() используют LockMode.NONE, если запрашивается объект(Entity) и он существует в любом кэше hibernate, то используется кэш.  Если нет объекта в кэше, то переходит в режим  блокировки LockMode.READ и формируется  sql  запрос.  Является уровнем блокировки по умолчанию в hibernate.
LockMode.READ - блокировка используется обычно для чтения сущности из Базы Данных, заносит в кэш сущность. Не используются,  ни какие внутренние блокировки баз данных на уровне sql запроса.
LockMode.OPTIMISTIC – блокировка используется обычно для принудительной проверки версионности у сущности, случается эта проверка, когда заканчивается транзакция, смотрим на прошлый пример с двумя транзакциями:
 ….
                session = sessionFactory.openSession();
                //начало 2-й транзакции.
                session.getTransaction().begin();
                LockOptions options =  new LockOptions();
                options.setLockMode(LockMode.OPTIMISTIC);
                person = (Person)session.get(Person.class,id,options);
                System.out.format("Transaction2 before change Person: %s\n", person);
                try {
                    //ждем пока 1-я транзакция изменит  данные и завершится.
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();  session.getTransaction().rollback();
                }
                System.out.format("Transaction2 LockMode.OPTIMISTIC Person: %s\n", person);
               /* здесь произойдет вызов проверки версионности и будет вызвана исключительная ситуация org.hibernate.OptimisticLockException. */
                session.getTransaction().commit();

смотрим на лог вывода программы:

Transaction2 before change Person: Person{id=1, name='Vitaly', version1=0}
Hibernate: update Person1 set MyName=?, MyText=?, version1=? where id=? and version1=?
Transaction1 after change Person: Person{id=1, name='Person Transaction1', version1=1}
Transaction2 LockMode.OPTIMISTIC Person: Person{id=1, name='Vitaly', version1=0}
Hibernate: select version1 from Person1 where id =? Проверка версионности
Exception in thread "main" org.hibernate.OptimisticLockException: Newer version [1] of entity [[org.vit.ch1.Person#1]] found in database

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

                session = sessionFactory.openSession();
                //начало 2-й транзакции.
                session.getTransaction().begin();
                LockOptions options =  new LockOptions();
                options.setLockMode(LockMode.OPTIMISTIC_FORCE_INCREMENT);
                person = (Person)session.get(Person.class,id,options);
                System.out.format("Transaction2 before change Person: %s\n", person);
                try {
                    //ждем пока 1-я транзакция изменит данные и завершится.
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();  session.getTransaction().rollback();
                }
                System.out.format("Transaction2 LockMode. OPTIMISTIC_FORCE_INCREMENT  Person: %s\n", person);
               /* здесь произойдет вызов проверки версионности + автоинкремент версионного поля  и будет вызвана исключительная ситуация org.hibernate.StaleObjectStateException. */
                session.getTransaction().commit();

смотрим на лог вывода программы:

Transaction2 before change Person: Person{id=1, name='Vitaly', version1=0}
Hibernate: update Person1 set MyName=?, MyText=?, version1=? where id=? and version1=?
Transaction1 after change Person: Person{id=1, name='Person Transaction1', version1=1}
Transaction2 LockMode. OPTIMISTIC_FORCE_INCREMENT Person: Person{id=1, name='Vitaly', version1=0}
Hibernate: update Person1 set version1=? where id=? and version1=? -  Попытка увеличить версионное поле
Exception in thread "main" org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [org.vit.ch1.Person#1]

LockMode.WRITE - случается, когда объект сущности(Entity) обновляется (update) или вставляется (insert), этот уровень блокировки используется только внутренним механизмом hibernate и его использовать в методах запрещено.
LockMode.PESSIMISTIC_READ - делает блокировку строки на чтение,  на время блокировки строки первой транзакцией, ни какая вторая транзакция не может её обновить.  Я использую postgresql и у неё есть специальная sql команда  для блокирования строки на чтение, SELECT … FOR SHARE  смотрим код:

                session = sessionFactory.openSession();
                //начало 2-й транзакции.
                session.getTransaction().begin();
                LockOptions options =  new LockOptions();
                options.setLockMode(LockMode.PESSIMISTIC_READ);
                person = (Person)session.get(Person.class,id,options);
                System.out.format("Transaction2 before change Person: %s\n", person);
                try {
                    /*ждем пока 1-я транзакция изменит данные, но строка заблокирована 2-й транзакцией, здесь происходит взаимоблокировка (deadlock).*/
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();  session.getTransaction().rollback();
                }
              // суда мы ни когда не попадем из-за взаимоблокировки!!!
                System.out.format("Transaction2  LockMode.PESSIMISTIC_READ Person: %s\n", person);

Лог программы:

Hibernate: select person0_.id as id0_0_, person0_.MyName as MyName0_0_, person0_.MyText as MyText0_0_, person0_.version1 as version4_0_0_ from Person1 person0_ where person0_.id=? for share
Transaction2 before change Person: Person{id=1, name='Vitaly'}
Hibernate: update Person1 set MyName=?, MyText=?, version1=? where id=? and version1=?
// все здесь произошёл deadlock

LockMode.PESSIMISTIC_WRITE  - блокировка использует механизм внутренний монопольной блокировки Баз Данных на уровне запроса - select ... for update , т. е. запрошенная запись (Entity) блокируется для дальнейшего изменения, если запись не может быть заблокирована немедленно, то происходит ожидание освобождения данных. Смотрим на листинг кода:

            session = sessionFactory.openSession();
                //начало 2-й транзакции.
                session.getTransaction().begin();
                LockOptions options =  new LockOptions();
                options.setLockMode(LockMode.PESSIMISTIC_WRITE);
                person = (Person)session.get(Person.class,id,options);
                System.out.format("Transaction2 before change Person: %s\n", person);
                try {
                    /*ждем пока 1-я транзакция изменит данные, но строка заблокирована 2-й транзакцией, здесь происходит взаимоблокировка (deadlock).*/
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();  session.getTransaction().rollback();
                }
              // суда мы ни когда не попадем!!!
                System.out.format("Transaction2  LockMode.PESSIMISTIC_WRITE Person: %s\n", person);

Лог программы:

Hibernate: select person0_.id as id0_0_, person0_.MyName as MyName0_0_, person0_.MyText as MyText0_0_, person0_.version1 as version4_0_0_ from Person1 person0_ where person0_.id=? for update
Transaction2 before change Person: Person{id=1, name='Vitaly', version1=0}
Hibernate: update Person1 set MyName=?, MyText=?, version1=? where id=? and version1=?
// все здесь произошёл deadlock

LockMode.PESSIMISTIC_FORCE_INCREMENT -   блокировка  подобна блокировке LockMode.PESSIMISTIC_WRITE, т.е. так же  выполняет запрос  select ... for update, плюс принудительно  увеличивает  версионное поле в объекте сущности(Entity),  да же если в текущей транзакции, не менялось состояние полей  у сущности. Смотрим листинг примера:

                session = sessionFactory.openSession();
                //начало 2-й транзакции.
                session.getTransaction().begin();
                LockOptions options =  new LockOptions();
                options.setLockMode(LockMode.PESSIMISTIC_FORCE_INCREMENT);
                person = (Person)session.get(Person.class,id,options);
                System.out.format("Transaction2 before change Person: %s\n", person);
                try {
                    /*ждем пока 1-я транзакция изменит данные, но строка заблокирована 2-й транзакцией, здесь происходит взаимоблокировка (deadlock).*/
                    thread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();  session.getTransaction().rollback();
                }
              // суда мы ни когда не попадем!!!
                System.out.format("Transaction2  LockMode.PESSIMISTIC_WRITE Person: %s\n", person);

Лог программы:

Hibernate: select person0_.id as id0_0_, person0_.MyName as MyName0_0_, person0_.MyText as MyText0_0_, person0_.version1 as version4_0_0_ from Person1 person0_ where person0_.id=? for update
Hibernate: update Person1 set version1=? where id=? and version1=?автоинкрементен версионности
Transaction2 before change Person: Person{id=1, name='Vitaly', version1=1}
2014-09-29 20:09:08,932 DEBUG [org.hibernate.SQL] - <update Person1 set MyName=?, MyText=?, version1=? where id=? and version1=?>
Hibernate: update Person1 set MyName=?, MyText=?, version1=? where id=? and version1=?

LockMode.FORCE – эту блокировку не советуют использовать,   используйте блокировку LockMode.PESSIMISTIC_FORCE_INCREMENT ,  блокировка объявлена как deprecated. 
LockMode.UPGRADE   блокировка объявлена как deprecated, подобна блокировке LockMode.PESSIMISTIC_WRITE, используйте её.
LockMode.UPGRADE_NOWAIT - блокировка похожа на LockMode.PESSIMISTIC_WRITE, но только использует запрос select … for update nowait, отличие в том, что если запись не может быть заблокирована сразу, то происходит исключение. На моей памяти запрос select … for update nowait поддерживает только БД Oracle, соответственно если БД не поддерживает такие запросы, то LockMode.UPGRADE_NOWAIT преобразуется к блокировке LockMode.PESSIMISTIC_WRITE.

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

3 комментария: