该帖已经被评为隐藏帖
|
|
---|---|
作者 | 正文 |
发表时间:2011-09-25
gdfloyd 写道 关于并发,LZ先看看Hibernate的乐观锁,悲观锁和自动版本化再说吧
这里说的不是并发谢谢。 hibernate 乐观锁?version?呵呵。。 |
|
返回顶楼 | |
发表时间: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
} 如果有两种同时方式触发上面用代码,难到不是同样的问题。
|
|
返回顶楼 | |
发表时间:2011-09-25
我也在考虑这个事情,是有一定的迷惑性。 按照现有的情况。
二、三的实质是一样的,都具有存储数据的能力。 一 不具有存储数据能力。也就不会制造出错误数据来。
数据原本是存在数据库中,数据库有锁机制确保的仅仅是执行顺序。除非你在查询的时候加锁,否则上面举的例子对数据库来说不存在任何问题。而且,又是为什么要加锁?在这个事件中,我把level改成了2,在另外一个事件中又把level改成了3.非常合理正常的操作。
在数据库中,具有存储数据能力的只有uid = 1的记录,但是到了我们的程序中不仅仅实体对象User有了存储能力,sql语句也具有了存储能力。这个是很不正常的问题。
先回家了。。希望能看到问题,然后找到好的解决办法。或者我是错的。
|
|
返回顶楼 | |
发表时间: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设计的优雅程度远远超出你的想象。 说一个框架不好不行的时候是否自己先反思下,是不是我没有用对? 是不是哪里理解有偏差。假如这个框架真的有问题,为什么还这么多人在用? 想过没有? |
|
返回顶楼 | |
发表时间:2011-09-26
最后修改:2011-09-26
楼上,虽然你写了很多。 但是对我毫无帮助。 1、你的第一个例子是一个事物内,和我举得例子不再一个层面上。 2、企业级开发经验就是按照你写的那样。把同一记录操作放在一个事物内,并且用业务逻辑或者hibernate锁 数据库锁来避免脏数据的操作。 3、为什么这么多人用,但是没怎么想过去改进?这个问题也是我比较迷惑的。但是我思考了下归咎于下面几点: 一、ORM垃圾。(对这类人来说他根本就不想去管ORM如何实现) 二、对数据修改比较多、但数据总量不太多的项目(例如游戏类,hibernate的一些粉丝也许会说,hibernate的产品说明书上说了不适合游戏开发。。。) 三、业务逻辑没问题,那么代码实现你就不能说不!这个才是我想表达的主要思想。各位一直都认为有些问题需要用业务逻辑去避免。例如ls你举的例子,把加钱和扣钱放在一起。但是你举的这个例子也不够好。因为加钱和扣钱不是针对同一个对象来说的。解释下就是:用户取钱,是用户得到钱,至于扣钱是银行扣钱。这种问题加事物是必须的。我一直在说的例子表示一个用户在这个银行存钱,在另外一个分行取钱。当然你也许看到这里就想骂人了,“这不是纯扯淡么!!”
没错,我一直说的、举的例子就是为了实现这么一个扯淡的操作,把这些“错误”的设计实现出来。
另外,各位用hibernate真的企业级开发太久了,以至于一些东西第一反应就是:NO!
我会在另外一贴告诉大家我希望改造后的hibernate的全貌。我称之为逆ORM,以前没想过,也没见过。最终大家看到之后也许会惊叹一番~
|
|
返回顶楼 | |
发表时间:2011-09-26
aa87963014 写道
我也在考虑这个事情,是有一定的迷惑性。 按照现有的情况。
二、三的实质是一样的,都具有存储数据的能力。 一 不具有存储数据能力。也就不会制造出错误数据来。
数据原本是存在数据库中,数据库有锁机制确保的仅仅是执行顺序。除非你在查询的时候加锁,否则上面举的例子对数据库来说不存在任何问题。而且,又是为什么要加锁?在这个事件中,我把level改成了2,在另外一个事件中又把level改成了3.非常合理正常的操作。
在数据库中,具有存储数据能力的只有uid = 1的记录,但是到了我们的程序中不仅仅实体对象User有了存储能力,sql语句也具有了存储能力。这个是很不正常的问题。
先回家了。。希望能看到问题,然后找到好的解决办法。或者我是错的。
问题就在这里,只要数据取出来存放在程序中,都会有你说的问题,这跟hibernate没有太大的关系。 |
|
返回顶楼 | |
发表时间:2011-09-26
76052186 写道
aa87963014 写道
我也在考虑这个事情,是有一定的迷惑性。 按照现有的情况。
二、三的实质是一样的,都具有存储数据的能力。 一 不具有存储数据能力。也就不会制造出错误数据来。
数据原本是存在数据库中,数据库有锁机制确保的仅仅是执行顺序。除非你在查询的时候加锁,否则上面举的例子对数据库来说不存在任何问题。而且,又是为什么要加锁?在这个事件中,我把level改成了2,在另外一个事件中又把level改成了3.非常合理正常的操作。
在数据库中,具有存储数据能力的只有uid = 1的记录,但是到了我们的程序中不仅仅实体对象User有了存储能力,sql语句也具有了存储能力。这个是很不正常的问题。
先回家了。。希望能看到问题,然后找到好的解决办法。或者我是错的。
问题就在这里,只要数据取出来存放在程序中,都会有你说的问题,这跟hibernate没有太大的关系。 是和hibernate没任何关系,和hibernate这类框架有关系。我不针对hibernate这一框架。只不过用的比较多久直接拿hibernate来说事。 |
|
返回顶楼 | |
发表时间:2011-09-26
不会的,我记得Hibernate应该是首先查询缓存的,对于任意get方法,如果主键一致,那么获取的都是同一对象,当然如果你非要new一个出来重新赋值,那么我们也没办法,再有hibernate对于数据库的更新不是实时的,在满足一定的条件下才会刷新数据库,目的就是为了防止这种类型的并发,这个叫做脏读是吧?
|
|
返回顶楼 | |
发表时间:2011-09-26
我觉得你应该首先去搞清楚ORM的初衷和应用场景然后再来讨论,任何一种技术和框架都有其实用的场景和局限,no silver bullet,就是这样。看你说的这些,我觉得你自己没有搞清楚ORM的应用场景和自己的需求,非得拿着棒槌去绣花,然后怪棒槌绣花不好用。如果真的到了你需要对Hibernate进行改造的话只能说明一个问题 :ORM解决不了你的需求,你应当寻求另外的解决方案了,不然就只不过是把棒槌磨成针,压根儿就不是一回事了。
|
|
返回顶楼 | |
发表时间:2011-09-26
dwbin 写道 不会的,我记得Hibernate应该是首先查询缓存的,对于任意get方法,如果主键一致,那么获取的都是同一对象,当然如果你非要new一个出来重新赋值,那么我们也没办法,再有hibernate对于数据库的更新不是实时的,在满足一定的条件下才会刷新数据库,目的就是为了防止这种类型的并发,这个叫做脏读是吧?
如果我告诉你,你连hibernate一级缓存是什么都不太了解,你会不会骂我。。。。 另外我从来都没提过什么同一事物内操作,很多人没明白什么样的业务场景就回帖~~ 当然,企业级开发过多就会反射性的从业务逻辑上避免这类问题~~ 关于脏读,hibernate/其他框架可以有各种办法判断脏读,但是一旦出现这个问题无法解决,只能throw error。 我说的这个多说白了就是为了解决这个问题,还多些ls提点。 |
|
返回顶楼 | |