Welcome to JiKe DevOps Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
682 views
in Technique[技术] by (71.8m points)

database - concurrency in hibernate

I have a servlet that does some work for user and then decrement user's credit. When I watch user's credit in the database in real time, if there are many concurrent requests from the same user, the credit has been deducted incorrectly due to concurrency control. T Assume I have one server and database management used is hibernate. I am using transaction control to span the whole request, please see code for detail. I have several questions:

  1. Why are the credit counter in db jumping all over the place when facing many concurrent request from same user? why isn't my transaction control working?

  2. If underlying data was modified after I retrieved user account and then attempt to update it, why didn't I get any HibernateException(eg.StaleObjectException)?

  3. I have transaction span across the full user request, is there a better way? Please critique. Feel free to rewrite the sample code structure if you feel I'm doing the whole thing wrong.

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() 

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();

    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());
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

Please log in or register to answer this question.

1 Answer

0 votes
by (71.8m points)

You have two ways to handle this situation: either with pessimist locking or with optimist locking. But you seem to use neither of both, which explain probably the incorrect behaviour.

  • With optimistic locking, Hibernate will check that the user account wasn't altered between the time it was read and saved. A concurrent transaction may then fail and be rolled back.

  • With pessimistic locking, you lock the row when you read it and it's unlocked only when transaction completes. This prevent a concurrent transaction to read data that would become stale.

Refreshing the entity may read new data or not depending whether the current transaction has already been committed or not, but is not a solution neither. Because you seem to also create the user account if it doesn't exist, you can't apply pessimist locking so easily. I would suggest you use optimistic locking then (and use for instance a timestamp to detect concurrent modifications).

Read this other question on SO about pessimist and optimist locking. Have also a look at hibernate chapter "transaction and concurrency" and "hibernate annotations".

It should be as simple as adding @Version on the corresponding field, the optimisticLockStrategy default value is VERSION (a separate column is used).

-- UPDATE --

You can test whether it works in a test case. I've created a simple entity Counter with an ID, value, and version fields.

 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;
    ...
}

If you update one entity sequentially it works:

  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(); 

I get one entity with value=2 and version=2.

If I simulate two concurrent updates:

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();

then the 2nd flush fails:

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)

This is so because the actual parameters in the SQL statements are:

   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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to JiKe DevOps Community for programmer and developer-Open, Learning and Share
...