параллелизм в спящем режиме - PullRequest
3 голосов
/ 21 декабря 2009

У меня есть сервлет, который выполняет некоторую работу для пользователя и затем уменьшает кредит пользователя. Когда я наблюдаю за кредитом пользователя в базе данных в режиме реального времени, если есть много одновременных запросов от одного и того же пользователя, кредит вычитался неправильно из-за контроля параллелизма. T Предположим, у меня есть один сервер и используется управление базой данных в спящем режиме. Я использую управление транзакциями, чтобы охватить весь запрос, см. Подробности в коде. У меня есть несколько вопросов:

  1. Почему счетчик кредитов в db прыгает повсюду, когда сталкивается с множеством одновременных запросов от одного и того же пользователя? почему не работает мой контроль транзакций?

  2. Если базовые данные были изменены после того, как я получил учетную запись пользователя и затем попытался обновить ее, почему я не получил HibernateException(eg.StaleObjectException)?

  3. У меня есть интервал транзакций по полному запросу пользователя, есть ли лучший способ? Пожалуйста, критикуйте. Не стесняйтесь переписать пример структуры кода, если вы чувствуете, что я все делаю неправильно.

Main servlet class:
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

            try{
                Manager.beginTransaction();
                cmdDowork(request, response);
                Manager.commitTransaction();
            }catch(Exception exp){
                Manager.rollbackTransaction();
                exp.printStackTrace();
            }
            finally{
                Manager.closeSession();
            }
}

public void cmdDowork(){
try{
     UserAccount userAccount = lazyGetUserAccount(request.getParameter("userName"));

     doWorkForUser(userAccount);//time and resource consuming process

     if(userAccount!=null) {

    decUserAccountQuota(userAccount);

     }

}catch (HibernateException e){
    e.printStackTrace();

}
}

public static UserAccount lazyGetUserAccount(String userName) {
        UserAccount userAccount = Manager.getUserAccount(userName);
        if(userAccount == null){
            userAccount = new UserAccount(userName);
            userAccount.setReserve(DEFAULT_USER_QUOTA);
            userAccount.setBalance(DEFAULT_USER_QUOTA);
            Manager.saveUserAccount(userAccount);
        }
     return userAccount;
}
    private boolean decUserAccountQuota(UserAccount userAccount) {

        if(userAccount.getBalance() 

<p>Edit: code I used to test optimistic locking as suggested by the answer, I am not getting a
any StaleObjectException, the update were committed successfully..

Session em1=Manager.sessionFactory.openSession();
         Session em2=Manager.sessionFactory.openSession();</p>

<code>    em1.getTransaction().begin();
    em2.getTransaction().begin();
    UserAccount c1 = (UserAccount)em1.get( UserAccount.class, "jonathan" );
    UserAccount c2 = (UserAccount)em2.get( UserAccount.class, "jonathan" );
    c1.setBalance( c1.getBalance() -1 );
    em1.flush();
    em1.getTransaction().commit();






    System.out.println("balance1 is "+c2.getBalance());
    c2.setBalance( c2.getBalance() -1 );
    em2.flush(); // fail
    em2.getTransaction().commit();

    System.out.println("balance2 is "+c2.getBalance());
</code>

1 Ответ

15 голосов
/ 21 декабря 2009

У вас есть два способа справиться с этой ситуацией: либо с блокировкой pessimist , либо с блокировкой optimist . Но вы, кажется, не используете ни то, ни другое, что, вероятно, объясняет неправильное поведение.

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

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

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

Прочтите другой вопрос на SO о блокировке пессимиста и оптимиста. Также обратите внимание на разделы "1021 * транзакции и параллелизм " и " аннотации гибернации ".

Это должно быть так же просто, как добавить @Version в соответствующее поле, optimisticLockStrategy значение по умолчанию равно VERSION (используется отдельный столбец).

- ОБНОВЛЕНИЕ -

Вы можете проверить, работает ли он в тестовом примере. Я создал простую сущность Counter с полями ID, value и version.

 public class Counter implements Serializable {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @Basic(optional = false)
    @Column(name = "ID")
    private Integer id;

    @Column(name = "VALUE")
    private Integer value;

    @Column(name = "VERSION")
    @Version
    private Integer version;
    ...
}

Если вы обновляете одну сущность последовательно , она работает:

  id = insertEntity( ... );

  em1.getTransaction().begin();               
  Counter c1 = em1.find( Counter.class, id );                
  c1.setValue( c1.getValue() + 1 );
  em1.flush();
  em1.getTransaction().commit();


  em2.getTransaction().begin();
  Counter c2 = em2.find( Counter.class, id );
  c2.setValue( c2.getValue() + 1 );
  em2.flush(); // OK
  em2.getTransaction().commit(); 

Я получаю одну сущность с value=2 и version=2.

Если я имитирую два одновременных обновления:

id = insertEntity( ... );

em1.getTransaction().begin();
em2.getTransaction().begin();

Counter c1 = em1.find( Counter.class, id );
Counter c2 = em2.find( Counter.class, id );

c1.setValue( c1.getValue() + 1 );
em1.flush();    
em1.getTransaction().commit();

c2.setValue( c2.getValue() + 1 );
em2.flush(); // fail    
em2.getTransaction().commit();

, тогда 2-ой сброс завершается неудачей:

Hibernate: update COUNTER set VALUE=?, VERSION=? where ID=? and VERSION=?
Hibernate: update COUNTER set VALUE=?, VERSION=? where ID=? and VERSION=?
Dec 23, 2009 11:08:46 AM org.hibernate.event.def.AbstractFlushingEventListener performExecutions
SEVERE: Could not synchronize database state with session
org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [org.ewe.Counter#15]
        at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:1765)

Это так, потому что фактические параметры в операторах SQL:

   update COUNTER set VALUE=1, VERSION=1 where ID=xxx and VERSION=0   
   --> 1 row updated
   update COUNTER set VALUE=1, VERSION=1 where ID=xxx and VERSION=0   
   --> 0 row updated, because version has been changed in between
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...