论坛首页 Java企业应用论坛

hibernate:从业务上避免脏数据?NO!

浏览 13948 次
该帖已经被评为隐藏帖
作者 正文
   发表时间:2011-09-25  
gdfloyd 写道
关于并发,LZ先看看Hibernate的乐观锁,悲观锁和自动版本化再说吧


这里说的不是并发谢谢。 hibernate 乐观锁?version?呵呵。。
0 请登录后投票
   发表时间:2011-09-25  
aa87963014 写道

 

原帖内容已删除,对描述部分做新的修改,将不针对hibernate这一个orm框架:

 

对于我们现在的大多数框架来说,任何操作都是基于数据库的。相当于把sql语言转换形式进行实现:

 

"update User set level = level +1;" 等价于 "User user= get(User); user.setlevel(user.getLevel+1);" ?

 

我们的应用都是基于数据库的数据操作,前者是告诉数据库自行修改数据操作,而后者着是直接替换数据库中的值!

 

有严重的并发修改问题。所谓并发修改不仅仅是同一时间修改同一条记录的时候的问题,这种情况数据库有锁等方式解决。

但是对于后者这种方式,数据库将无能为力,因为orm框架每次update到数据库中的值全部都是全新的!全部都是替换操作。

 

在并发修改的情况下(从 User user= get(User);把user信息取出来到update(user);这个过程中,任何一个其他线程的 get(User) update(user) ;操作都会照成潜在的并发问题。第一个取出的user的对象是可靠的,其他的非指向user引用的对象全部都为不可靠数据!)user的信息会被任意的覆盖,因为所有的user信息都是取得保存在内存中,数据库中uid为1的user记录在数据库中永远只有一个,并且有锁机制保证修改顺序。

 

但是在orm框架中会存在多个user对象,只不过这些user对象的uid为1,而且可怕的是这些user互不相关!在你update(user);的时候实质上是在不停的插入全新的user信息,内存中的user值和数据库中的值没有任何关系。但是,从业务逻辑上来说,你对user对象属性操作仅仅是代替数据库操作。

 

本质上user的属性在整个内存中应当只有1个,因为你只是代替数据库去修改某个属性。你不应该具有多个存储能力,每个实体实质上都存储了一条记录的信息。

 

举例:

 

	@Transactional
	public void a(){
		User user1=baseDAO.get(User.class,1);		
		user1.setLevel(user1.getLevel()+1);
		
		User user2=baseDAO.get(User.class,1);		
		user2.setLevel(user2.getLevel()+1);
		
		baseDAO.update(user1);
		baseDAO.update(user2);
	}

 在这个方法里面,任意对user的操作都是可靠的,因为hibernate的一级缓存将后面取出来的user2指向的是user1的引用,user1、user2的修改实质上仅仅修改内存中的唯一user对象。在update之后没有任何问题,因为从始至终只修改的是一个记录。

 

典型的并发修改例子:

 

@Transactional
	public void b(){
		User user1=baseDAO.get(User.class,1);		
		user1.setLevel(user1.getLevel()+1);
		
		User user2=baseDAO.get(User.class,1);		
		user2.setLevel(user2.getLevel()+1);

		baseDAO.update(user1);
		baseDAO.update(user2);
	}

	@Transactional
	public void c(){
		User user3=baseDAO.get(User.class,1);		
		user3.setLevel(user3.getLevel()+1);	
		
		try {
			Thread.sleep(5000);//模拟业务操作需要5秒时间
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		
		baseDAO.update(user3);
	}
 

 

b方法比c方法晚一秒钟调用,假设user的leve原本的值为1;在method b,method c执行完毕之后 数据库中实际值为2,而不是3。

因为b方法修改的数据库记录被c方法的修改结果覆盖了。

 

这类问题根本原因不是用加锁或者修改业务逻辑去解决的。而是为什么在内存中会有多个不一样的user。数据应该都是唯一的,数据库中找不到id为1的2条记录,内存中也不应该出现id为1的2个对象。

 

既然我们是orm框架用面向对象的方式去修改数据,操作数据的本质应该不能改变:既永远只操作一条记录。可以参照method a是如何实现的,无论你update(user);多少次,最终修改的只有一条记录。只不过hibernate这种方式仅仅在一个事物中是这样做的,没有做到整个内存中只存在一个user对象。

 

 

针对,hibernate这个orm框架来说。他可以用一级缓存保证同一个事物内数据的一致性,但是处理不了不同事物的一致。那么还有一个二级缓存可能让内存中的user指向同一个对象(应该是有的),具体有没有这么做,还需要测试、研究下才能最终确认。

 

如果我的推断是正确的,那么在hibernate开启二级缓存之后,才能算是一个完整的orm框架。否则将是一个错误设计,简单的说:数据中只有一条记录,那么内存中只能有一个实体,update操作只能由这个实体操作,其他的任意实体都不可取。你以为你按照产品说明书上一步步操作没有问题,实际上问题一直存在!只不过你没有碰。

 

------

ps:

1、我是在接触游戏开发才有这个想法,至于一些设计可能你觉得有问题,这个不想多争论,除非你指出我设计上的错误。

2、并发问题、并发压力之类的,一般都说的是数据库底层操作的问题,但是实际在业务方法上你可能悄无声息的制造了并发问题。

3、这里请不要说xx不适合开发xx之类的,这里说的和开发什么东西没太大关系,任何一个使用hibernate的应用都会有这样的问题,除非你从业务上就避免了多个地方修改同一条记录的逻辑。一旦有,你就应该有这个思考。

4、如果错,请狂喷。在下洗耳恭听、虚心学习。

说了半天你终于把问题描述清楚了。

这根本就是一个数据库并发与锁的问题.你不过是把问题给简单化了。考虑复杂一点的问题,如果你的level不是这样简单就能计算出来的,也就是说不能用level=level+1这样直接更新,难到不是有同样的问题。比如 

temp_level = 当前级别;

if(当前时间==国庆 )

{

  temp_level=temp_level+temp_level*国庆特别升级规则

  update User set level =temp_level

 

}

如果有两种同时方式触发上面用代码,难到不是同样的问题。

 

 

 

 

 

 

 

0 请登录后投票
   发表时间:2011-09-25  

我也在考虑这个事情,是有一定的迷惑性。

一:update User set level = level +1  where uid = 1;

二:update User set level = 2 where uid = 1;

三:user.setLevel(2);update(user); /  user.setLevel(user.getLevel()+1);update(user);

按照现有的情况。

 

二、三的实质是一样的,都具有存储数据的能力。

一 不具有存储数据能力。也就不会制造出错误数据来。

 

数据原本是存在数据库中,数据库有锁机制确保的仅仅是执行顺序。除非你在查询的时候加锁,否则上面举的例子对数据库来说不存在任何问题。而且,又是为什么要加锁?在这个事件中,我把level改成了2,在另外一个事件中又把level改成了3.非常合理正常的操作。

 


数据一旦取出存在User实体内,那么数据的“重心”移到了User实体上了,而不是数据库中的 uid = 1的这条记录。

在数据库中,具有存储数据能力的只有uid = 1的记录,但是到了我们的程序中不仅仅实体对象User有了存储能力,sql语句也具有了存储能力。这个是很不正常的问题。

 

先回家了。。希望能看到问题,然后找到好的解决办法。或者我是错的。

 

 

0 请登录后投票
   发表时间:2011-09-25   最后修改:2011-09-25
引用

    @Transactional  
    public void b(){  
        User user1=baseDAO.get(User.class,1);         
        user1.setLevel(user1.getLevel()+1);  
          
        User user2=baseDAO.get(User.class,1);         
        user2.setLevel(user2.getLevel()+1);  
  
        baseDAO.update(user1);  
        baseDAO.update(user2);  
    }  
  
    @Transactional  
    public void c(){  
        User user3=baseDAO.get(User.class,1);         
        user3.setLevel(user3.getLevel()+1);   
          
        try {  
            Thread.sleep(5000);//模拟业务操作需要5秒时间  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
          
        baseDAO.update(user3);  
    }  


初看这两个事务方法好像没什么问题,但是你仔细想想。
b方法的业务逻辑和c方法的业务逻辑访问的数据都有两步操作。
1.拿对象
2.更新对象数据
Why ?
这样的操作最经典的实现是CAS操作,这是另一个话题。我们先不谈。
为什么不这样设计?
看下面的代码
User类的片段:
/** 加钱 */
void addMoney(int addValue){
    setMoney(getMoney() + addValue);
}
/** 扣钱 (为了简单,好说明问题。这里我们假设User的钱足够多,不考虑不够的情况)*/
void removeMoney(int removeValue){
    setMoney(getMoney() - removeValue);
}

UserBusiness类代码:(也就是业务逻辑吧)
@Transactional 
/** 这里是事务方法,而且假设没有其他访问钱的方法。 */
public void transMoney(User userA, User userB, int value) {
    //顺序加锁的代码省略
    userA.removeMoney(value);
    userB.addMoney(value);
}

这样是不是好很多呢?最起码我的业务方法不用考虑我的对象是不是过期的对象(和数据库的内容不一致)
剩下的问题就是业务逻辑谁来调,调用者的User对象怎么获得。

你可能担心会不会两个线程同时运行到transMoney中。一个A给B打钱,一个B给A打钱。
这个是事务ACID的问题。我们也不谈。

hibernate 有一级缓存,二级缓存。即便是都没有。我们也一样能给出好的设计或者更优雅的设计。

问题的关键是。我们怎么用这个工具。

如果我们这样用代码:
引用

 @Transactional  
    public void b(){  
        User user1=baseDAO.get(User.class,1);         
        user1.setLevel(user1.getLevel()+1);  
          
        User user2=baseDAO.get(User.class,1);         
        user2.setLevel(user2.getLevel()+1);  
  
        baseDAO.update(user1);  
        baseDAO.update(user2);  
    } 


你还不如不用,直接jdbc一条语句搞定。

真实的游戏后台开发,用户级的数据都是要做缓存和异步存储的。每个用户数据还要有脏位标记符。同步是否来判断用的。

Hibernate 的最大作用是帮助开发人员自动生成非面向对象的SQL语句。让我们关注点放到业务逻辑上。而不用担心具体的数据存储问题。提高开发效率。

如果不了解hibernate,抽时间好好看看。比如hibernateTemplate 的 get 和 saveOrUpdate 方法。Gavin King设计的优雅程度远远超出你的想象。

说一个框架不好不行的时候是否自己先反思下,是不是我没有用对? 是不是哪里理解有偏差。假如这个框架真的有问题,为什么还这么多人在用? 想过没有?
0 请登录后投票
   发表时间:2011-09-26   最后修改:2011-09-26

楼上,虽然你写了很多。 但是对我毫无帮助。

1、你的第一个例子是一个事物内,和我举得例子不再一个层面上。

2、企业级开发经验就是按照你写的那样。把同一记录操作放在一个事物内,并且用业务逻辑或者hibernate锁 数据库锁来避免脏数据的操作。

3、为什么这么多人用,但是没怎么想过去改进?这个问题也是我比较迷惑的。但是我思考了下归咎于下面几点:

     一、ORM垃圾。(对这类人来说他根本就不想去管ORM如何实现)

     二、对数据修改比较多、但数据总量不太多的项目(例如游戏类,hibernate的一些粉丝也许会说,hibernate的产品说明书上说了不适合游戏开发。。。)

     三、业务逻辑没问题,那么代码实现你就不能说不!这个才是我想表达的主要思想。各位一直都认为有些问题需要用业务逻辑去避免。例如ls你举的例子,把加钱和扣钱放在一起。但是你举的这个例子也不够好。因为加钱和扣钱不是针对同一个对象来说的。解释下就是:用户取钱,是用户得到钱,至于扣钱是银行扣钱。这种问题加事物是必须的。我一直在说的例子表示一个用户在这个银行存钱,在另外一个分行取钱。当然你也许看到这里就想骂人了,“这不是纯扯淡么!!

 

没错,我一直说的、举的例子就是为了实现这么一个扯淡的操作,把这些“错误”的设计实现出来。

 

另外,各位用hibernate真的企业级开发太久了,以至于一些东西第一反应就是:NO!

 

我会在另外一贴告诉大家我希望改造后的hibernate的全貌。我称之为逆ORM,以前没想过,也没见过。最终大家看到之后也许会惊叹一番~

 

0 请登录后投票
   发表时间:2011-09-26  
aa87963014 写道

我也在考虑这个事情,是有一定的迷惑性。

一:update User set level = level +1  where uid = 1;

二:update User set level = 2 where uid = 1;

三:user.setLevel(2);update(user); /  user.setLevel(user.getLevel()+1);update(user);

按照现有的情况。

 

二、三的实质是一样的,都具有存储数据的能力。

一 不具有存储数据能力。也就不会制造出错误数据来。

 

数据原本是存在数据库中,数据库有锁机制确保的仅仅是执行顺序。除非你在查询的时候加锁,否则上面举的例子对数据库来说不存在任何问题。而且,又是为什么要加锁?在这个事件中,我把level改成了2,在另外一个事件中又把level改成了3.非常合理正常的操作。

 


数据一旦取出存在User实体内,那么数据的“重心”移到了User实体上了,而不是数据库中的 uid = 1的这条记录。

在数据库中,具有存储数据能力的只有uid = 1的记录,但是到了我们的程序中不仅仅实体对象User有了存储能力,sql语句也具有了存储能力。这个是很不正常的问题。

 

先回家了。。希望能看到问题,然后找到好的解决办法。或者我是错的。

 

 

问题就在这里,只要数据取出来存放在程序中,都会有你说的问题,这跟hibernate没有太大的关系。

0 请登录后投票
   发表时间:2011-09-26  
76052186 写道
aa87963014 写道

我也在考虑这个事情,是有一定的迷惑性。

一:update User set level = level +1  where uid = 1;

二:update User set level = 2 where uid = 1;

三:user.setLevel(2);update(user); /  user.setLevel(user.getLevel()+1);update(user);

按照现有的情况。

 

二、三的实质是一样的,都具有存储数据的能力。

一 不具有存储数据能力。也就不会制造出错误数据来。

 

数据原本是存在数据库中,数据库有锁机制确保的仅仅是执行顺序。除非你在查询的时候加锁,否则上面举的例子对数据库来说不存在任何问题。而且,又是为什么要加锁?在这个事件中,我把level改成了2,在另外一个事件中又把level改成了3.非常合理正常的操作。

 


数据一旦取出存在User实体内,那么数据的“重心”移到了User实体上了,而不是数据库中的 uid = 1的这条记录。

在数据库中,具有存储数据能力的只有uid = 1的记录,但是到了我们的程序中不仅仅实体对象User有了存储能力,sql语句也具有了存储能力。这个是很不正常的问题。

 

先回家了。。希望能看到问题,然后找到好的解决办法。或者我是错的。

 

 

问题就在这里,只要数据取出来存放在程序中,都会有你说的问题,这跟hibernate没有太大的关系。

是和hibernate没任何关系,和hibernate这类框架有关系。我不针对hibernate这一框架。只不过用的比较多久直接拿hibernate来说事。

0 请登录后投票
   发表时间:2011-09-26  
不会的,我记得Hibernate应该是首先查询缓存的,对于任意get方法,如果主键一致,那么获取的都是同一对象,当然如果你非要new一个出来重新赋值,那么我们也没办法,再有hibernate对于数据库的更新不是实时的,在满足一定的条件下才会刷新数据库,目的就是为了防止这种类型的并发,这个叫做脏读是吧?
0 请登录后投票
   发表时间:2011-09-26  
我觉得你应该首先去搞清楚ORM的初衷和应用场景然后再来讨论,任何一种技术和框架都有其实用的场景和局限,no silver bullet,就是这样。看你说的这些,我觉得你自己没有搞清楚ORM的应用场景和自己的需求,非得拿着棒槌去绣花,然后怪棒槌绣花不好用。如果真的到了你需要对Hibernate进行改造的话只能说明一个问题 :ORM解决不了你的需求,你应当寻求另外的解决方案了,不然就只不过是把棒槌磨成针,压根儿就不是一回事了。
0 请登录后投票
   发表时间:2011-09-26  
dwbin 写道
不会的,我记得Hibernate应该是首先查询缓存的,对于任意get方法,如果主键一致,那么获取的都是同一对象,当然如果你非要new一个出来重新赋值,那么我们也没办法,再有hibernate对于数据库的更新不是实时的,在满足一定的条件下才会刷新数据库,目的就是为了防止这种类型的并发,这个叫做脏读是吧?


如果我告诉你,你连hibernate一级缓存是什么都不太了解,你会不会骂我。。。。

另外我从来都没提过什么同一事物内操作,很多人没明白什么样的业务场景就回帖~~

当然,企业级开发过多就会反射性的从业务逻辑上避免这类问题~~

关于脏读,hibernate/其他框架可以有各种办法判断脏读,但是一旦出现这个问题无法解决,只能throw error。
我说的这个多说白了就是为了解决这个问题,还多些ls提点。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics