从框架层面统一解决脏更新的探讨

概述

         对于我们做web的来说,都会遇到这样的情况:两个人(或多个人)同时打开同一条数据的修改页面,(如果不做处理)这时候就会发生很不好的现象:最后一个提交表单的人,会把之前所有提交的修改都覆盖掉了!

        上面这个现象,我们常常说,这叫并发现象!其实,更专业一点的,这叫“数据的脏更新问题”。这个问题,在一般场景下,其实可以看成是无所谓的事情,但在一些比较严谨的或者“爱计较”的用户来说,这是不能忍的!因为我好不容易更新的东西,居然被别人覆盖了,甚至是一个领导修改完之后,发现居然被别人的修改覆盖了(我们就遇到过这样的现象,客户就向我们投诉了)!

         对于上面的现象,我们不讨论要不要解决或有没有必要解决,我们要讨论的是,如果要解决,该怎么解决?我们系统中是解决了的,毕竟是收费客户投诉……哭

方案探讨

         对于这个现象的解决方案来说,我先说几个方案,并讨论一下这几个方案的优缺点,大家可以多补充,一起探讨一下。

  1. 最简单最有效的,莫过于直接在各个修改的业务代码上加上判断。具体做法时,在进入修改页面时,把当前数据的修改时间(或有能记录数据变更的版本字段就更好)带到页面,在页面的form表单里hidden住,在提交修改时,对比这个时间,与服务器中当前数据的那个字段的值是否相等,如果相等,表示这段时间内没有被别人修改过,可以进行修改保存;否则就给出不能修改的提示。  这个方案的好处是:简单有效,灵活性很高,对不同的表可以取不同的合适的字段做对比信息;但其缺点也很明显:工作量大家、代码分散、维护困难等。
  2. 比较高端的做法是,在进入某数据的修改页面时,对当前数据加锁(锁的方式可以是数据库锁,也可以是在表中加一个标志字段表示数据正在被修改),这样别人在进入这条数据的修改页面时,系统会自动提示数据有别人在修改,在用户修改完之后将锁释放。  这个方式的优点是,防患于未改,而且因为使用锁机制,可以统一处理,减少工作量并易于后期维护;其缺点特别明显:就在于这个锁的释放!我们的web项目不像C/S结构,我们是无状态的一个交互方式,我们无法知道用户的操作状态,比如,我打开一个修改页面(这时加了锁),然后我直接把页面或浏览器关掉了,这时锁是不释放的,这样这条数据就永远被锁死了!这就得需要想一个特别的方案来释放这些“死锁”数据。
  3. 比较实际的做法,也是我使用的方法。就是根据自己的项目特征,从框架层面做一些处理。所以,本文将重点探讨一下这种方式的实现及不足之处。

框架层面统一解决脏更新的探讨

        这个方案需要有三个基本条件:页面渲染使用统一接口(如Velocity、FreeMark等渲染框架);数据库操作使用统一接口(如Hibernate、ibatis/myBatis等ORM框架,或自定义的统一操作入口);表中要有能做判断的字段。我们的项目使用的是Velocity+SpringMVC+Spring+iBatis(而我又对iBatis的常用操作接口做了封装,如:插入数据,根据id修改数据,根据指定的条件修改数据等等),而我们每个表中都正好有一个updateTime字段(更新时间),每次更新数据时,这个字段都会被更新为最新时间。

        有了这三个条件,我们就可以使用以下思路去做:在进入一个页面时,在对页面进行渲染前,我会把当前的页面中所用到的所有的实体类中的updateTime时间获取出来,并getTime()为long类型,做为判断的标志信息,hidden到页面的所有form表单中;在提交表单时,如果有这个hidden信息,则在根据id修改单条数据的接口中做判断。

        因为代码在公司研发网络(局域网),无法给出完整代码,这里我把关键地方的代码列出一下:

首先,要新建一个Filter,里面将这个hidden值取出来,放到线程池变量(ThreadLocal)中,这个就不需要写代码了吧?

其次,在渲染页面的地方,重写VelocityLayoutView.renderScreenContent方法,将其最后一行的改为:

velocityContext.put(this.sreenContentKey, appendVersionStamp(velocityContext, sw.toString()));

 appendVersionStamp这个方法代码核心如下:

 

 

private String appendVersionStamp(Context velocityContext, String pageHtml){
    if(StringUtils.isBlank(pageHtml) || (pageHtml.toLowerCase().indexOf("<form ")==-1 &&pageHtml.toLowerCase("<form>").indexOf()==-1)){
        return pageHtml;//如果页面内容为空,或没有form表单,则不处理
    }
//下面的代码比较多,就不写了,大概意思就是:将velocityContext中所有实体类找出来,然后取出其updateTime(如果updateTime为空,则取addTime),将实体类的类名+id+updateTime.getTime,作为一组标志。如果有标志,则把它hidden到每个from表单中,其hidden的name="_update_version_"。
}

再者,在根据id修改数据的接口中对其进行判断,其核心代码如下:

public <T> int updateById(T t){
    String version = VerifyHolder.getUpdateVersion();//从线程变量中取出Filter放置的页面提交上来的版本信息
    if(StringUtils.isNotEmpty(version)){//表单中有提交时,就可以更新。
        //这里的代码比较多,其逻辑为:根据id先从数据库中查出当前数据的updateTime(这里要缓存到线程变量中,因为如果业务中有多次修改操作的话,会引起误判),判断类名+id+updateTime.getTime(),前面类+id一致的情况下(这是确保验证的是同一条数据),如果updateTime不一致,则抛出异常,不给操作,否则就进行更新操作。
    }
}

 

优缺点探讨

优点:简洁明了,代码量小,维护方便,最重要的是,对开发透明。

缺点:对系统的要求较多;只能对根据id修改的接口才能做这样的统一处理,其它地方,因为是统一规则,无法个性化处理;

 

最后说一句:这种思路还可以使用AOP等切面技术实现,但思路是一致的。

 

欢迎大家参与探讨!

猜你喜欢

转载自hellohank.iteye.com/blog/2225611
0条评论
添加一条新回复