用OO方法解一道算术题(数据库缓存技术)

用OO方法解一道算术题

板桥里人 http://www.jdon.com 2005/12/26(转载请保留)

  本篇主要为说明使用面向对象的分析和设计方法可以帮助更快地认识事物,更快地排除编程设计过程一个个拦路虎。

  这是在开发Jdon Framework 1.4批量查询过程碰到的一个小案例。J2EE系统中避免不了批量分页查询,就象一个孩子满房间找东西一样,这样的频繁查找是对数据库的折腾,这里有一篇文章试图通过存储过程等数据库操作实现批量查询的案例:海量数据库的查询优化及分页算法方案,但是在设计上这是一个错误的方向,向数据库要性能要潜力的余地已经很小了。

  我以前讲了,面向数据库的设计编程方法已经过去,其中一点是,我们将使用缓存来替代大部分的数据库直接操作,将对数据库的折腾带来的重负载转移到中间J2EE服务器上,而中间服务器可以方便地实现集群拓展。

  那么按照这个思路,由于批量查询条件是各种各样,似乎很难将结果进行缓存,但是,在实际操作中,用户的操作总是按照我们系统提供的导引进行操作,用户又可以随意对任何一个操作进行反复进行,通过缓存至少我们可以降低这部分反复操作对数据库负载开销。

  我们根据查询条件读取数据库时,采取固定块的读取方法,每个块可以为200个数据,也就是说,我们就每次都读取200个数据,然后将这个数据块放在缓存中供下次查询。

  我们将查询结果以分页的方式显示给用户,但是这个分页的结果可能是随机的,例如有可能每页显示20个或每页显示100个或300个等等,这些分页的结果是从固定数据块中获取的。

  一般分页查询条件有两个参数:startIndex:本页在数据块中的开始位置,因为每页开始不一定和数据块的开始是吻合的;还有一个参数是count:就是当前页面显示多少行数据。

好了,算术题来了

  我们需要每次比较查询条件的startIndex和count和数据块,有可能当前这个查询的count超过固定数据块的长度,那么就要启动下一个数据块获取。

  其他场景还有:我们是通过PageIterator(Iterator一个子类)这个对象推送到表现层的,在PageIterator中保存的应该是符合查询条件的所有数据块总和,但是PageIterator的startIndex开始应该是查询条件的startIndex,而count则可能是查询条件的count,也可能是数据块的实际长度,因为符合查询条件的数据集合可能达不到数据块的固定长度,比如固定长度是200,但是满足查询条件的数据只有7个,而查询条件的count可能是任意数值,因此PageIterator的count必须返回一个正确的数值给表现层。

  还有一个场景是:我们必须查询数据库获得数据块,同时将数据块以一个主键Key值保存到缓存中,那么缓存的这个Key值如何实现?Key值的粒度越细;缓存的利用率就越低。

  我开始以为非常简单,不就是查询条件和缓存数据块以及数据块数据块在start/count上比较嘛,也许早就有一种算法来解决这个问题,只是我不知道而已,但是我现在也不可能去google搜索算法大全。

  谈到算法,我想表达一种观点:学习软件的人都要学习算法,其实这是被诱导欺骗了,算法其实就是数学,算法的发明有赖于数学的突破,所以,算法本身没什么东西,但是它将软件诱导服从于数学了,结果,执著于算法概念搞软件的人最后发现,软件其实类似CAD绘图工具,真正创造性思想来自算法后面的数学。最后产生软件比数学低人一等的想法,相当一段时间软件被用作计算的工具,其实这些都是极大地对软件认识不足以及不尊重,软件自身本身就是和数学平等一样的,都是人类表达世界的媒介,特别是面向对象技术和SOA的发展更加表达这种观点,软件最大的追求是什么?

  现在我们回到主题上来,如果我们认为这个算术题仅仅是个算法问题,我们只要穷尽其所有情况就可以,但是深入进去时,会发现可变的条件太多了,可以说每当你发现一个规律后,发现它又是有前提的,这个前提可能还依赖于你的规律,又搞出先有鸡还是先有蛋的老问题出来,这是编程设计中经常会碰到的拦路虎。

  因为不重视这个算术题,以为很简单,所以就按照直觉直接开始编程设计,有如下代码:

  首先,我们设计数据块的缓存Key,它肯定是有查询条件语句组成,比如String sqlquery, Collection queryParams表示前者是查询SQL语句,后面参数是查询参数值,数据块缓存Key是由这两个参数组成,既然是数据块又必须有一个起点,因此我们还需要数据块起点作为它的Key值,它的起点如何计算,公式如下:

int blockID = start / count;
int blockStart = blockID * count;

  得出的blockStart是一个数据块起点,这是动态的值。

  然后,我们设计一个方法从数据库读取然后保存到缓存中:

private List getBlockKeys(QueryConditonDatakey qcdk) {
  logger.debug(" start=" + qcdk.getStart() );
  List keys = blockCacheManager.getBlockKeysFromCache(qcdk);
  if ((keys == null) || (!cacheEnable)) {
    keys = blockQueryJDBC.fetchDatas(qcdk);
    blockCacheManager.saveBlockKeys(qcdk, keys);
  }
  return keys;
}

  这样获得一个以List表示的数据块结果。

  现在,我们要根据这个结果,和查询条件进行比较,得出一个PageIterator结果出来,根据直觉得出如下(当然你的直觉可能比我更加精确):

private List test(QueryConditonDatakey qcdk , int count){
  List keys = getBlockKeys(qcdk);
  int thisDataBlockLength = keys.size() + qcdk.getBlockStart() - qcdk.getStart();
  int currentBlocklength = count;
  if (thisDataBlockLength == QueryConditonDatakey.BLOCK_SIZE) {
    if (count > thisDataBlockLength) {
      //必须满足查询块长度
      //再创建一个DataBlock和CacheBlock 进入循环
      keys.addAll(test(qcdk, count));
    }
  }
  return keys;
}

  可惜这个方法试图做两件事情:获得符合查询条件的数据块,并且获得真实的count,从而能够创建PageIterator。但是开始显得力不从心了。

  我们再看看如何生成PageIterator的代码:

public PageIterator getPageIterator(String sqlqueryAllCount, String sqlquery, Collection queryParams, int startIndex, int count) {
  logger.debug("enter getPageIterator ..");
  QueryConditonDatakey qcdk = new QueryConditonDatakey(sqlquery, queryParams,                     startIndex);

  List keys = test(qcdk, count);//获得符合查询条件的数据块

  int dataBlockLength = keys.size();
  int thisDataBlockLength = keys.size() + qcdk.getBlockStart() - startIndex;

  int currentBlocklength = count;
  if (thisDataBlockLength < 200) {
    if (count > thisDataBlockLength){
      currentBlocklength = thisDataBlockLength;
    }
  }
  int endIndex = startIndex + currentBlocklength;
  int allCount = getDatasAllCount(queryParams, sqlqueryAllCount);//符合条件所有
  logger.debug(" allcount=" + allCount + " keys.length=" + keys.size());
  logger.debug(" startIndex=" + startIndex + " endIndex = " + endIndex);
  return new PageIterator(allCount, keys.toArray(), startIndex, endIndex);
}

  我们觉得应该将test方法内容拉进getPageIterator了,否则有些计算是重复的,而且无法把握两个方法中同样的值是否肯定一致。但是编程直觉告诉我们,将这么多代码放进入一个方法肯定是不对的。

  至少我当时是陷入了多种矛盾和重复中去,而且这段代码虽然可以编译,但是测试下来是有BUG的,因此这段代码肯定是有BUG,或者说不正确的,但是如何保证正确呢?我知道必须持续走下去,我的钻牛角尖顶真的劲头是不小的,但是在这里我被我自己挡住了,说句体外话:牛劲很大的人适合搞软件,有两种可能:被自己拦住了,不是别人,才考虑选择另外一条道路了;但是可怕的是第二种:牛角越钻越尖,变成钻牛角尖,最后觉得自己都变傻了,年纪一大就吃力,只好改行。

  现在,我碰到这样问题,当我在一个问题上原本计划一两个小时都没有完成时,我知道必须“斩仓止损”了,需要换另外一种思维来对待它。

  既然这个问题花费我一段时间,当然也可能我比较笨,或者当初上小学时没有做尽天下所有的算术题,但是和我相似的人应该有一些,怎么办呢?

当你关注它时,它就是一个对象

  这是我使用面向对象分析方法的一个触发点,这道算术题花费我不少时间,该对它特别关注了,在这个算术题中,到底存在哪些对象呢?

  我找到了,我相信你也会找到,数据块Block是一个对象,其实我们费老鼻子劲就是获得一个满足当前页面的Block数据块,而且还有在这个数据块范围内的查询起点和实际页面显示个数。

  另外,这个数据块在不同阶段还表现不同,在缓存里有一个数据块,称为缓存数据块;在数据库中有一个数据块叫数据库数据块;客户端查询条件组合的数据块;根据前两者计算出的当前数据块。

  数据块代码如下:


public class Block {

  private int start;
  private int count;

  private List list;

  public Block(int start, int count) {
    super();
    this.start = start;
    this.count = count;
  }

  public int getCount() {
    return count;
  }

  public void setCount(int count) {
    this.count = count;
  }

  ......
}

  当然,这个数据块对象是最终简化后的结果,其中有反复随着认识深入逐步修改的结果。

  通过将数据块作为一个对象来看待以后,我们思考就更加可以符合实际情况,说白了,就更容易写流水帐了:

1.创建一个数据库的数据块dataBaseBlock

2.创建缓存的数据块cacheBlock

3.创建客户端查询条件的数据块clientBlock,当然这时只有start和count,list有待计算。

4.创建当前页面可用的数据块,这是我们的结果值,这个数据块的start和count需要分别计算,list可能是一个cacheBlock/dataBaseBlock,也可能是多个cacheBlock/dataBaseBlock,需要计算。

  最后实现的功能代码可见开源Jdon Framework1.4(需要专门测试近期会公布)。

  通过本篇文章记录真实实战开发中的思考过程,目的在于让大家了解,面向对象的方法不是一种技术;而是一种思考方式,也是可以解脱我们程序员痛苦编程的一种值得尝试的方法。

  另外,我也想说明的是:OO方法不只是表现在重整Refactoring,也就是说先使用直觉将结果不顾一切地输出,然后再考虑使用OO来整理。如果你一开始使用OO方法,而不是直觉,那么一切本身也许就很流畅,如同庖丁解牛一样。

  最后,所幸的是,不是所有的程序员都必须解这个题,因为你只要直接使用Jdon框架就可以了。将不能把握的、或者不能取得统一解决方案的解决办法放入框架,这样能够大大降低项目延期或失败的风险。我想这点对于所有聪明的人都会明白的。

 -------------------------------

我认为将startIndex作为key的组成部分是没有必要的,因为如果startIndex最终会作为sql的一部分:select * from a limit...,而且,如果使用MS SqlServer,startIndex就不起作用了。Hibernate就是把sql、参数、分页信息等等作为缓存key,所以Hibernate的批量查询缓存的实用性并不高。缓存中的数据块应该从第一条记录开始,到MaxResults,如果分页超过了这个范围,则调整MaxResults并重新执行查询。还有,分页的时候不要从数据库中取,而是在缓存中的数据块中进行分页,下面是我的key的实现:
public class CachedQueryKey implements Serializable {
private String queryString;
private Object[] args;
//....getters/setters,hasCode,equals,toString
}
数据块采用List作为数据内容也是不合理的,因为根据查询内容的不同会产生许多VO,还不如使用RowSet,我实现了一个动态Bean:
public interface Rows extends Serializable{
/**
* 将ResultSet对象的一行导入。
*/
public void addRow(final ResultSet rs) throws SQLException;

/**
* 将Row对象导入
*/
public void addRow(final Row row);

/**
* 遍历ResultSet对象,并全部导入
*/
public void addRows(final ResultSet rs) throws SQLException;

/**
* 遍历行集的所有行,返回Iterator接口。实现者可以采用内部类实现Iterator接口。
*/
public Iterator iterator();

/**
* 返回指定行
*/
public Row get(int index);

/**
* 如果行集中没有数据,返回true
*/
public boolean isEmpty();

/**
* 返回行集的最大预设容量(number of row)
*/
public int getMaxRows();

/**
* 预设行集的容量(number of row)
*/
public void setMaxRows(int maxRows);

/**
* 返回行集的实际容量
*/
public int getRows();

/**
* 取得行集中某一行的指定列。
* @param colName 列名
* @param rowIndex 行索引,第一行为0,第二行为1,...
*/
public Object get(String colName, int rowIndex);

/**
* 返回指定范围的子行集。
* @param startIndex 起始索引,必须>=0 <=getRows
* @param maxRows 子集的容量
*/
public Rows sub(int startIndex, int maxRows);

/**
* 将行集的每一行转换为指定类型的java bean对象,并以List的形式返回。
* @param type 指定的类型,必须是标准的JavaBean
*/
public List toList(Class type);
}
这个是接口,实现类必须注意不能使用Map作为每一行,因为Keys会占用过多的内存,如果追求效率,可以使用数组Object[][]

由于很多应用要求实时性较高,所以缓存的使用也是可选的,我使用动态代理实现,这样,如果不想使用缓存就不要代理即可。
还有,分页算法应该和查询以及缓存的应用分开,作为一个单独的Aspact使用,分页算法最重要的是取得真实查询结果的总行数,这个数据是分页的基础,它也需要缓存。


__________________
Not only how,but also why!
http://202.108.205.200:8080/

 

banq


发表文章: 6457
来  自: 上海
注册时间: 2002-08

Re: 用OO方法解一道算术题 发表时间: Dec 27, 2005 11:38 AM
回复此消息回复
切磋一下:
>数据块采用List作为数据内容也是不合理的,因为根据查询内容的不同会产生许多VO,还不如使用RowSet,我实现了一个动态Bean

数据块中存放的是主键集合,不是VO;不能使用属于持久层的对象在全层之间传来传去,你的自己动态Bean不错。
这里List和Block都是一个临时变量,在Jdon框架中,在全层之间传递是PageIterator,类似你的Rows


>我认为将startIndex作为key的组成部分是没有必要的
真正到数据块查询(dataBaseBlock)的startIndex不是查询条件的startIndex,而是经过计算:
int blockID = start / count;
int blockStart = blockID * count;
中的blockStart,
这样能够保证从数据库或缓存获取数据块时,表明这个数据块是符合我们起点要求的数据块。

如果不使用起点作为Key一部分,那么可能缓存不起作用,因为同一个查询条件下,多个页面的区别就是起点不一样,这样我们可以将包涵每个页面的数据块缓存。

另外,“缓存中的数据块应该从第一条记录开始”不知你是指符合查询条件的第一条吗?还是当前页面的第一条呢?我前面是指当前页面第一条。
你可能也是这个意思,你说"分页超过了这个范围,则调整MaxResults并重新执行查询",
这是一种实现思路,只是我现在提供遍历的这个类PageIterator是一个简单的POJO,是一个DTO,没有自己操作数据块能力,这也给我带来实现难度。

所以有时想,MF提出的非贫血模型多好,自己不但带数据,还可以在任何时刻操作数据库,但是它是不是象Hibernate的Open in View模式,将持久层的潘多拉盒子打开,在各层到处是飞舞的数据库影子。



__________________
Java学习开发三件宝: Domain Model(域建模)、Patterns(模式)和Framework(框架)。
集三宝理念于一身,小中型J2EE项目快速开发工具:Jdon Framework

 

banq


发表文章: 6457
来  自: 上海
注册时间: 2002-08

Re: 用OO方法解一道算术题 发表时间: Dec 27, 2005 11:44 AM
回复此消息回复
可喜的是有cats_tiger 这样同志者和我一样在做这样的探索,而我在google上搜索看到更多是在数据库端做功夫,这里有一篇文章试图通过存储过程等数据库操作实现批量查询的案例:
海量数据库的查询优化及分页算法方案:

http://www.pconline.com.cn/pcedu/empolder/db/sql/0501/538958_6.html

但是在设计上这是一个错误的方向,向数据库要性能要潜力的余地已经很小了。

更重要的是,造成代码可维护性和可拓展性很差,更可怕的是:几乎所有搞数据库系统的人都是这种思路,思维不改;而且他们对选择其他道路还在怀疑观望,这种思维下的程序代码质量能高吗?

真应该让更多人明白:数据库时代已经过去了。





__________________
Java学习开发三件宝: Domain Model(域建模)、Patterns(模式)和Framework(框架)。
集三宝理念于一身,小中型J2EE项目快速开发工具:Jdon Framework

 

yuxie

发表文章: 50
注册时间: 2004-11

Re: 用OO方法解一道算术题 发表时间: Dec 27, 2005 10:08 PM
回复此消息回复
老大,既然这是一个错误的方向,那么拿这篇文章举的一个相关例子,在一个有上亿条记录的人口数据库里边,不做数据库优化的情况下,如何能够按某个条件(比如人名或出生日期),做到快速查出相关记录呢?这里的纪录一般可是关系到多个表的哦。

我最近刚做了一个sql的优化(实际上是hsql),优化前查询速度是80多秒,优化后查询速度是80多毫秒,请问老大为什么说向数据库要性能要潜力的余地已经很小了呢?

请用缓存实现上边说的人口数据库的例子~这里的查询都是随机的,一次查出来的数据一般也都在一页之内。一页以上大家就在加条件限制范围了,没人会一页一页翻着看哦。

 

banq


发表文章: 6457
来  自: 上海
注册时间: 2002-08

Re: 用OO方法解一道算术题 发表时间: Dec 28, 2005 10:09 AM
回复此消息回复
》化前查询速度是80多秒,优化后查询速度是80多毫秒
提高了1000倍,但是80毫秒就到头了,业务数据是不断增加的,这种方式是没有弹性的,可伸缩性不大。

那么我们使用其他手段是否可以达到这种目的,我相信也会达到的。

关键是使用这个方法hsql,可能破坏代码的质量,最后导致核心逻辑掌握在少数人手里,这是危险的,不能规模复制或生产。

当然,我不否认,在实际情况下,在时间紧迫下,可以采取这样措施。

无论如何,我们探讨的是一个可以规模复制的解决之道,也就是适合大多数“笨人”的解道。

不但追求性能卓越;而且代码质量优异,后者更应该是程序员追求的目标,如果yuxie有意,我愿意和你一起研究这个上亿数据优化,我刚刚测试过Jdon framework 1.4,10万数据读写速度都没有变化,到后面越来越快,就象读10个数据一样。





__________________
Java学习开发三件宝: Domain Model(域建模)、Patterns(模式)和Framework(框架)。
集三宝理念于一身,小中型J2EE项目快速开发工具:Jdon Framework

 

yuxie

发表文章: 50
注册时间: 2004-11

Re: 用OO方法解一道算术题 发表时间: Dec 28, 2005 1:50 PM
回复此消息回复
>我愿意和你一起研究这个上亿数据优化
老大说的是中间件这一端的优化吗(还不能涉及sql优化,以免技术掌握在少数人手里)?8好意思,那我想说的是,当数据量比较大,又可能涉及多个表连接的时候,又涉及基本无规律的查询,这时候不管你框架怎么优化都没用……

原因很简单。缓存是什么?缓存的只可能是经常访问到的数据,在访问无规律的情况下,使用缓存只能降低性能,我最看不惯的就是老大经常提起的缓存之上论。这时候数据库是根本,不重视就会吃苦头。您如果一直坚持自己的观点给人培训那实在太不责任了。

至于您说的那10万条记录越来越快……我无语……现实的情况可不是单表数据的列表显示那么简单……如果您的客户需求都这么简单……还需要程序员干什么……

 

banq


发表文章: 6457
来  自: 上海
注册时间: 2002-08

Re: 用OO方法解一道算术题 发表时间: Dec 28, 2005 2:40 PM
回复此消息回复
呵呵,yuxie兄好像有些生气了,我们只是探讨一些观点,不代表我否定在数据库优化上做,不但数据库优化要做,更深层次的优化也要做,两者结合当然更好啦,也就是说DBA和设计师一起努力来提高系统性能。

我刚刚贴了google的搜索原理:
http://www.jdon.com/jive/thread.jsp?forum=91&thread=24454
文章中反复谈了优化数据结构的重要性,当然也有这句:

“我们必须有一个巧妙的算法来决定哪些旧网页需要重新抓取,哪些新网页需要被抓取。这个目标已经由实现了。受需求驱动,用代理cache创建搜索数据库是一个有前途的研究领域。”

找到算法:尽量不用到频繁到性能差的数据源获取数据,这个思想在实践中值得借鉴。

大家保留各自观点吧。


__________________
Java学习开发三件宝: Domain Model(域建模)、Patterns(模式)和Framework(框架)。
集三宝理念于一身,小中型J2EE项目快速开发工具:Jdon Framework

 

cats_tiger


发表文章: 178
注册时间: 2003-05

Re: 用OO方法解一道算术题 发表时间: Dec 29, 2005 4:00 PM
回复此消息回复
>如果不使用起点作为Key一部分,那么可能缓存不起作用,因为同一个查询条件下,多个页面的区别就是起点不一样,这样我们可以将包涵每个页面的数据块缓存。
这个是关键,我是把符合条件的1~1000条数据保存在缓存中,分页在缓存中进行(使用Rows.sub方法),这样就减少了数据库操作,这个是与Hibernate和banq老大的不同的地方。那么,如果每页显示50条,而我需要20页之后的数据呢?重新查询并扩大范围就可以了。还有就是多数人在查询的时候,是不会关心几千条数据的。
将每个页面都作为一个缓存块,其缺点是:1)分页的时候需要查询数据库
2)不同的缓存块的过期时间不同,如果数据库内容发生变化,会导致混乱,除非同时更新所有缓存快。

>数据块中存放的是主键集合
那是不是每次使用某的时候还是要查询数据库呀?跟Jive的做法类似。

>另外,“缓存中的数据块应该从第一条记录开始”不知你是指符合查询条件的第一条吗?
我是指符合条件的第一条。缓存中存放内容包括:符合条件第一条到MaxResults条数据;符合条件的数据的实际数量。

另外,数据库的调优也很重要,一条索引就可以提高几百倍的性能。另外,SQL的调优也非常重要,例如使用exists就比in快的多。

缓存比较适合经常使用的数据和分页查询的情况,对于很多"边查询边修改"的用例就不合适了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值