我有一个servlet,它为用户做一些工作,然后减少用户的信用。当我实时查看数据库中的用户信用时,如果有来自同一用户的许多并发请求,则由于并发控制,信用被错误地扣除。 Ť
假设我有一台服务器,并且使用的数据库管理处于休眠状态。
我正在使用事务控制来处理整个请求,请参阅代码以获取详细信息。我有几个问题:

  • 当面对来自同一用户的多个并发请求时,为什么db中的信用计数器到处跳?为什么我的交易控制不起作用?
  • 如果在检索用户帐户然后尝试更新之后修改了基础数据,为什么我没有得到HibernateException(eg.StaleObjectException)
  • 我的交易跨整个用户请求,是否有更好的方法?请批评。如果您觉得我做错了所有事情,请随时重写示例代码结构。

  • Servlet主类:
    protected void doGet(HttpServletRequest请求,HttpServletResponse响应)抛出ServletException,IOException {

    尝试{
    Manager.beginTransaction();
    cmdDowork(请求,响应);
    Manager.commitTransaction();
    } catch(Exception exp){
    Manager.rollbackTransaction();
    exp.printStackTrace();
    }
    最后{
    Manager.closeSession();
    }
    }

    公共(public)无效cmdDowork(){
    尝试{
    UserAccount userAccount = lazyGetUserAccount(request.getParameter(“userName”)));

    doWorkForUser(userAccount); //时间和资源消耗过程

    if(userAccount!= null){

    decUserAccountQuota(userAccount);

    }

    }赶上(HibernateException e){
    e.printStackTrace();

    }
    }

    公共(public)静态UserAccount lazyGetUserAccount(String userName){
    UserAccount userAccount = Manager.getUserAccount(userName);
    if(userAccount == null){
    userAccount =新的UserAccount(userName);
    userAccount.setReserve(DEFAULT_USER_QUOTA);
    userAccount.setBalance(DEFAULT_USER_QUOTA);
    Manager.saveUserAccount(userAccount);
    }
    返回userAccount;
    }
    私有(private) bool(boolean) 值decUserAccountQuota(UserAccount userAccount){

    如果(userAccount.getBalance()

    编辑:我用来测试答案所建议的乐观锁定的代码,我没有得到
    任何StaleObjectException,更新都已成功提交。

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

    最佳答案

    您有两种方法来处理这种情况:使用悲观主义者锁定或乐观主义者锁定。但是您似乎都不使用两者,这可能解释了错误的行为。

  • 通过乐观锁定,Hibernate将检查在读取和保存用户帐户之间是否没有更改用户帐户。然后,并发事务可能会失败并回滚。
  • 使用悲观锁定,您在读取行时将其锁定,并且仅在事务完成时才将其解锁。这样可以防止并发事务读取会过时的数据。

  • 刷新实体可能会读取新数据,也可能不会读取新数据,这取决于当前事务是否已经提交,但这也不是解决方案。由于似乎还创建了用户帐户(如果该帐户不存在),因此您不能如此轻松地应用悲观主义者锁定。我建议您然后使用乐观锁定(例如,使用时间戳来检测并发修改)。

    在SO上阅读此other question有关悲观主义者和乐观主义者锁定的信息。还可以查看休眠章节“transaction and concurrency”和“hibernate annotations”。

    它应该像在相应字段上添加@Version一样简单,optimisticLockStrategy default valueVERSION(使用单独的列)。

    -更新-

    您可以测试它是否可以在测试用例中使用。我用CounterIDvalue字段创建了一个简单的实体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=2version=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();
    

    然后第二次冲洗失败:
    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
    

    08-18 13:16