单表低查询万级树型留言板

来源:互联网 发布:stc单片机 编辑:程序博客网 时间:2024/06/06 04:29
 这是Snow_Young学JSP早期的作品了,没有用MVC,只是想做个试验,表达自己的思想,能力有限,有笨拙的地方,望大家多多体谅。

之前遇到的留言板大多数是单级或者只有两级留言,也遇到过一些多级留言的,很多限制级数,且需要很多次查询数据库以达到遍历效果。我学JSP的时候也想做一个留言板,最后想法停留在了多级留言板上面。为了避免过多次查询数据库,于是有了这样的解决方案:

一、不使用父节点指针,而是使用面包屑方式记录层次附属关系。
二、只需要两次数据库查询,一次是查询当页的所有留言,另一次是查询根留言的count。
三、为了易于存储,使用了varchar型,四个字节存储一个面包屑,其中前三位是93进制数,右对齐,最大可以记录到804357,第四位是93进制数的92的后一位,为了排序方便。
四、利于查询的速度,主要用于那个面包屑的排序,将面包屑字段设为unique key,并以用户的ip和昵称作为基础生成一个四个字节的字符串,之后获得自动id之后生成93进制数进行替换。简便起见,只是将ip和昵称两个字符串连接之后取其MD5码的随机连续4位。

下面咱们简单介绍一下。

转载请保留出处:http://blog.csdn.net/Calceola/archive/2008/08/24/2824131.aspx

一、93进制数

  其实是使用了ascii码的33(!)到125(})来进行存储,例如十进制的92就可以表示为“  }”,而93就可以表示为“ "!”(不包括全角双引号)。上文所提到的93进制的92后一位即为ascii码的126(~),是为了排序方便的,后面程序的时候还会说到。

二、面包屑

  主要用途是减少查询次数,除去面包屑方法之外,据Snow_Young所知还有父节点指针方法和子节点指针方法,如果还有其它的,望广大网友告知,谢谢!
  父节点指针方法和子节点指针方法顾名思义,就是说在数据库中建立一个字段来存放该节点的父节点或者子节点的索引值,例如有一棵树是这样的结构:

  1
  ├ 2
  │├ 3
  │└ 4
  └ 5

  那么这5个节点对应的父节点指针和子节点指针集合则分别为:

  1 null {2, 5}
  2 1 {3, 4}
  3 2 {}
  4 2 {}
  5 1 {}

  这样的做法有助于减少数据存储量(子节点方式也不是很省),关系也更清晰,但是遍历起来需要查询多次数据库,在程序和数据库之间的消耗太大。
  那么面包屑方式则有所改进,相应字段记录了从根节点到达该节点的路径,按照往常的存储方式则为:(不包括首尾全角双引号)

  1 “001|”
  2 “001002|”
  3 “001002003|”
  4 “001002004|”
  5 “001005|”

   只要按照面包屑字段进行排序,那么进行这个树型的遍历就很轻松了。但是美中不足是,用10进制数存储面包屑会使得可以使用的id量很小很小,例如3位只能支持到999,超过了范围就会造成排序问题了。
   因此,Snow_Young对这个存储方式再次进行了改进,使用了93进制数,避免使用33之前和126之后的字符,以便特殊情况下需要直接对数据库进行操作。按照上例的方式,Snow_Young的面包屑是这样的:(不包括首尾全角双引号)

   1 “  "~”
   2 “  "  #~”
   3 “  "  #  $~”
   4 “  "  #  %~”
   5 “  "  &~”

   这个和上例一样易于排序,只是可表达的数范围上面有了本质的区别,前面已经说到了,就不赘述了。
   大概你已经注意到Snow_Young使用了“~”作为了中止符,其实这个和上方的“|”一样的道理,这两个字符分别是两种方式生成的面包屑字符串中最大的字符,这样降序排序后自然而然就形成了树型结构的深度遍历顺序,很方便。

 三、MD5

   MD5是现在世界上很风行的加密方式,同时也可以用此来获得随机字串,相关资料在网上有很多,php中甚至将md5()作为了内置函数供程序员直接使用。但是java中是没有这样的一个MD5类或者方法的,不过我们可以建立一个java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");对象,然后md.update(byte[]类型参数);进行md5加密,接着调用byte[] md.digest()方法来获得加密的数据,最后将数据转换成十六进制数字符串。Snow_Young在这个程序中的这部分代码也是贴过来的,呵呵,详情请见http://www.blogjava.net/haogj/archive/2006/07/04/56604.html。

   下面我们来看一下关键部分的处理方式:(详细源码请见最下方)

转载请保留出处:http://blog.csdn.net/Calceola/archive/2008/08/24/2824131.aspx

 一、发表页面

  其实这一个页面中最重要的只有一行:
  1. <input name="id" type="hidden" value="<%=id %>" />

  一定要获得上个页传递过来的id值,否则不管怎么发都是根留言了,如果是要发表根留言则id=0。

二、发表结果页面

  1.     String pathseed = pathline + md5((ip + author).getBytes()).substring(psCutStart, psCutStart + 4);    //这里生成前方方案四中的随机字串,前把这个字串提交到数据库,之后获得自增id之后再替换掉。
  2.     pathseed = pathseed.replace("'""''");    //这个是必不可少的,因为后面用到的mysql代码中使用了单引号作为字符串标识符,所以如果不将单引号处理的话,极有可能造成sql代码无法正常解析,产生异常。后面的pathline同样也需要处理。
  1.     stmt.execute(sql, Statement.RETURN_GENERATED_KEYS);    //与平时用到的execute()方法不同,这里又加了一个参数,是告诉Statement执行完之后返回自增id。
  2.     rs = stmt.getGeneratedKeys();
  3.     rs.next();
  4.     int autoid = rs.getInt(1);    //这样就获得自增id了。
  1.     if (superior == 0) {    //判断是否为根节点
  2.       pathline = numToStr(autoid) + "~";    //这行语句运用上了一个自定义方法:numToStr(),用于将一个整形转换成93进制的字符串,后面加上~以便于以后提取记录时用sql直接排序。
  3.       pathline = pathline.replace("'""''");    //之后将单引号转换。
  4.       sql = "update messages set pathline='" + pathline + "', superior=" + autoid + " where id=" + autoid + " limit 1";
  5.     } else {
  6.       pathline = pathline.replace("~", numToStr(autoid) + "~");
  7.       pathline = pathline.replace("'""''");
  8.       sql = "update messages set pathline='" + pathline + "' where id=" + autoid + " limit 1";
  9.     }    //最后直接替换数据库pathline字段中的值,提交部分就大体完成了。


三、留言板显示页面 index.jsp

  这个页面中有两个比较重要的部分,第一个是这个sql语句,后面会有关于它的介绍:
  1.       sql = "select * from messages where superior in (" +
  2.           "select id from (" +
  3.           "select id from messages where depth=0 and id<=(" +
  4.           "select id from messages where depth=0 order by id desc limit " + ((pageid - 1) * pagesize) + ",1" +
  5.           ") order by id desc limit " + pagesize +
  6.           ")t" + 
  7.           ") order by pathline desc";
  另外一个是控制显示的部分,由于使用了div布局,所以需要在逻辑上判断嵌套关系,但也只需要一次循环就够了:

  1.       <%
  2.         while (rs.next()) {    //逐行遍历数据库中记录
  3.           depth = rs.getInt("depth");    //用于判断深度
  4.           if (depth == 0) {    //while语句按照需要输出"</div>"
  5.             while (lastdepth != -1) {
  6.               out.print("</div>");
  7.               lastdepth--;
  8.             }
  9.           } else {
  10.             while (lastdepth != 0 && lastdepth > depth - 1) {
  11.               out.print("</div>");
  12.               lastdepth--;
  13.             }
  14.           }
  15.           lastdepth = depth;
  16.         %>
  17.         <div class="message">
  18.           》》》div内容部分《《《
  19.         <%
  20.         }
  21.         while (lastdepth != 0) {
  22.           out.print("</div>");
  23.           lastdepth--;
  24.         }
  25.       %>
  26.     </div>
  分页的部分就不说明了。

四、工具集

  工具集中有三个非常重要的方法,一个是md5算法方法,一个是编码转换方法,还有一个93进制转换方法。

  1. public static String md5(byte[] source) {
  2.   String s = null;
  3.   char hexDigits[] = {'0''1''2''3''4''5''6''7''8''9''a''b''c''d',  'e''f'};
  4.   try {
  5.     java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5");
  6.     md.update(source);
  7.     byte tmp[] = md.digest();
  8.     char str[] = new char[16 * 2];
  9.     int k = 0;
  10.     for (int i = 0; i < 16; i++) {
  11.       byte byte0 = tmp[i];
  12.       str[k++] = hexDigits[byte0 >>> 4 & 0xf];
  13.       str[k++] = hexDigits[byte0 & 0xf];
  14.     }
  15.     s = new String(str);
  16.   } catch( Exception e ) {
  17.     e.printStackTrace();
  18.   }
  19.   return s;
  20. }
  1. public static String toUTF8(String str) {
  2.   try {
  3.     byte[] temp = str.getBytes("ISO8859-1");
  4.     str = new String(temp, "utf-8");
  5.   } catch(Exception e) { }
  6.   return str;
  7. }
  1. public static String numToStr(int n) {
  2.   String result = "";
  3.   int a = 0;
  4.   while (n != 0) {
  5.      a = n % 93;
  6.      result = String.valueOf(Character.toChars(a + 33)) + result;
  7.      n = n / 93;
  8.   }
  9.   while (result.length() < 3) {
  10.     result = " " + result; 
  11.   }
  12.   return result;
  13. }
  下面对上方那个sql讲讲。

  这个sql一共使用了四层嵌套,我们从中间开始说起:

  "select id from messages where depth=0 order by id desc limit " + ((pageid - 1) * pagesize) + ",1":这个子查询是用来获得应当取得的所有根节点中的最大id,然后使用"select id from messages where depth=0 and id<=() order by id desc limit " + pagesize来获得当页的所有根节点的id。这种用法是刚学来的,据说记录数上万之后可以提升非常多的效率,当然,如果数据较少的话,一个子查询就可以代替这两个了。然后是"select id from ()t",因为mySQL的第二层子查询不能使用limit,只好变通的使用了这个方法来实现。最后由"select * from messages where superior in () order by pathline desc"获得了该页的所有记录,并按面包屑排序,完成了我们的需求。

  也许童鞋们都发现了这个程序并非万级啊?其实程序初衷是实现万级的,但实在没什么意义。如果真想实现万级,其实很简单:把pathline字段的类型改成text或更高就可以了,呵呵,不过要小心效率哦!

这个程序的源码请到这里下载:http://download.csdn.net/source/597267

转载请保留出处:http://blog.csdn.net/Calceola/archive/2008/08/24/2824131.aspx

谢谢对Snow_Young研究的支持,如果有不足之处请大家给我留言,谢谢!
原创粉丝点击