Discuz!官方免费开源建站系统

 找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索

从Discuz7.2/X2盖楼机制到X2.5盖楼抢楼合一分析Discuz的mysql技术包袱

[复制链接]
mark35 发表于 2012-9-12 11:28:05 | 显示全部楼层 |阅读模式
本帖最后由 mark35 于 2012-12-4 10:39 编辑

本文因未知问题因而在我个人空间中被管理隐藏不显示,故存档于 http://xiaozhong.biz/thread-289-1-1.html


任何数据库应用都存在分页性能问题,mysql的性能问题似乎更大些。Discuz7.2(以下简称dz7.2)和Discuz X2(简称X2)的抢楼机制基本相同,抢楼与普通帖子(盖楼)是不同的处理机制。到了Discuz X2.5(简称X2.5)就把抢楼与盖楼机制统一。看似X2.5的处理方式更好,但实际上我认为因为缺乏DBA的支持,DZ因为mysql的弱智低能带来的称重包袱而导致开发正走向歧路。

先分析dz7.2/X2,X2.5各自的技术实现

dz7.2使用 postposition 表来保存后台“帖子优化”操作以及抢楼帖的信息,这两个功能使用的是同一个机制:

  1. CREATE TABLE `NewTable` (
  2. `tid`  int(10) UNSIGNED NOT NULL ,
  3. `position`  int(10) UNSIGNED NOT NULL ,
  4. `pid`  int(10) UNSIGNED NOT NULL ,
  5. PRIMARY KEY (`tid`, `position`)
  6. )
复制代码
postposition.position与tid配合决定跟帖的列表顺序。当新抢楼跟帖入库时调用
  1. savepostposition($tid, $pid);
复制代码
来更新postposition表

  1. function savepostposition($tid, $pid, $position = 0) {
  2.     global $db, $tablepre;
  3.     if(!$position) {
  4.         $pos = $db->result_first("SELECT max(position) FROM {$tablepre}postposition WHERE tid='$tid'");
  5.         $pos ++;
  6.     } else {
  7.         $pos = $position;
  8.     }
  9.     $res = $db->query("INSERT INTO {$tablepre}postposition SET tid='$tid', position='$pos', pid='$pid'");
  10.     return $res;
  11. }
复制代码


X2的处理机制与dz7.2基本相同,不同之处在于取消了后台帖子优化操作, postposition表仅供抢楼帖使用,此表的position字段变成自增长字段:

  1. CREATE TABLE `NewTable` (
  2. `tid`  mediumint(8) UNSIGNED NOT NULL ,
  3. `position`  int(10) UNSIGNED NOT NULL AUTO_INCREMENT ,  -- <<<<---------
  4. `pid`  int(10) UNSIGNED NOT NULL ,
  5. PRIMARY KEY (`tid`, `position`),
  6. INDEX `pid` USING BTREE (`pid`)
  7. )
复制代码
于是 savepostposition() 函数也随之变化:

  1. function savepostposition($tid, $pid, $returnposition = false) {
  2.     $res = DB::query("INSERT INTO ".DB::table('forum_postposition')." SET tid='$tid', pid='$pid'");
  3.     if(!$returnposition) {
  4.         return $res;
  5.     } else {
  6.         return DB::insert_id();
  7.     }
  8. }
复制代码


X2.5变化巨大,引用官方的介绍
  &#8226; post表的优化 问题:
高楼贴的性能问题 limit 187460, 20 方法:
增加position字段记录楼层,修改主键为:PRIMARY KEY  (tid, `position`)联合主键,其中position为auto_increment;
pid字段保留,仍然是auto_increment(单独的一个表),保持唯一,其值在一个单独的表中记录,保留此字段的主要目的是可以让原程序的基本不用做修改;
使用方法:
SELECT * FROM pre_forum_post WHERE tid=424 AND position>=$start AND position<$end ORDER BY position;
抢楼贴和普通的盖楼贴机制统一;

http://dev.discuz.org/wiki/index ... F%E6%9E%B6%E6%9E%84

取消了 postpositin表,在 post主表中增加 position 字段保存每个帖子的楼号。

先不分析X2.5固定楼号的利弊,就谈谈从dz7.2到X2.5对楼层处理机制以及变化的优缺点。
dz7.2用 postposition表来实现抢楼楼号排序,顺便也实现了对高楼层分页性能优化;X2 postpositon表只用来抢楼,对于高楼层主题分页优化没看到相关处理;X2.5对post主表添加楼号字段来让所有帖子的楼号固定,从而解决分页性能问题,也一并解决了抢楼问题。

从抢楼为主,附加高楼层分页到所有楼层分页为主,附带实现了抢楼,这个变化说明随着discuz功能增加对于分页性能的需求也变大,另外一个显示原因是那些长期使用discuz系统的大站数据也越来越大,大于1000万帖子的不是少数,他们对分页性能提高的需求更迫切。

无论怎么变化,本质上都是以空间换时间的策略——使用额外的表或字段来保存帖子的相关信息,以某种形式的静态化来提升动态分页的效率。不过就dz7.2到X2.5所采取的具体实现都忽视了一个重要的问题——逻辑一致性
估计是没有DBA的缘故,dz开发人员在实现新功能时往往首先考虑的是应用层实现,而没考虑到数据层的底层关联:
所有对post回帖SQL查询时的默认排序是取的dataline字段,此字段决定了主题的帖子排序结果也就是逻辑楼号。当使用dz7.2/X2的抢楼功能以及X2.5的抢盖一体化功能后,此时参与楼号计算多了 postposition.position 或者 post.positon 字段,因此让逻辑变得混乱。
对于dz7.2的盖楼帖查询,先从 postpositon表中根据tid和position排序取出一个ppp页面大小的pid记录,然后用于对posts主表查询的WHERE pid  IN ()条件,然而此时的排序方便又变成了pid!

  1. $query .= $savepostposition && $cachepids
  2.     ? "WHERE p.pid in($cachepids) ORDER BY pid ".($ordertype !=1 ? 'DESC' : 'ASC')    // <<----- 这儿是盖楼帖
  3.     : ("WHERE p.tid='$tid'".($auditstatuson ? '' : " AND p.invisible='0'")." $specialadd2 $onlyauthoradd $pageadd"); // <-- 普通
复制代码
加上普通帖的排序方式,此时一共有3个字段决定了帖子分页极其最终展示的顺序!这是非常糟糕的设计。为什么这么说呢?

因为pid和dateline 两个属性在通常情况下的前后顺序是一致的,但本质上这两者是两个互不干扰的线索,在并发情况下会出现不同的顺序结果。这又牵涉到discuz时间戳处理的机制。
discuz所有入库数据的时间戳(通常是dateline字段)取值是在应用层php的date()函数获得的$timestamp变量,而不是数据库层的CURRENT_STAMP关键字。这个设计使得 posts表中pid和dateline字段变成了两条线索:
假定webserver先接收到跟帖请求A,生成时间戳100号(实际上是一个10位数值)。在处理请求A的过程中接着接收到对同一个tid的跟帖请求B,给B生成了时间戳101号。101这意味着比100的时间要完。但因为请求A的内容很大,处理过程消耗了比B更多的时间,造成请求B先入库获得了pid500,而请求A入库时pid为520。
于是,当考虑帖子A,B的先后顺序时。若以dateline考量A在前,而以pid考量则B在前。


所以,盖楼帖、抢楼帖是以pid来最终(注意最终这两个字)排序,而普通帖是以dateline排序。这是混乱之一。

对于抢盖楼来说因为savepostposition()函数设计的缺陷,会造成混乱之二:新跟帖入库在调用savepostposition()获得max(positon)从而计算出当前帖的postion楼号时没有考虑到在并发时会取得相同的楼号从而生成相同的postion记录!

而混乱之三因此也产生了:savepostposition()产生postion时遵循的是数据层的时间线索,先到者先得小号postion。但对postposition插入是在对posts写入之后,那么postpostion.positon与posts.pid又是两个线索了。取分页数据根据前者,最终(应在这儿)输出排序确是后者。

对于这种前后部分,无唯一参照体系的恶劣设计,X2取消了帖子优化只采用抢楼帖,并且改动了 postpotion表,把position字段改成自增长,避免了并发下postion重复的重大bug;而X2.5放弃(原有的)帖子优化及抢楼帖设计而是采用以解决分页性能为主的固定楼号手段来彻底一步解决了高楼,抢楼的逻辑混乱。但混乱真的解决了么?

为何dz72的盖楼设计这么糟糕,为何X2.5要引入固定楼号字段。这就是问题的根本了!

这个问题以及所有关联的问题究其根本只有一个原因:弱智的mysql对于时间类型只能精确到秒

mysql无论是timestamp还是datetime在内部实现都采用的整数(且不谈坑爹的timestamp是int4,只能在1970-1-1到2038-x-x间取值)所以discuz开发人员对 dateline 就没有采用这时间类型而是int4. 于是乎整个论坛体系的时间线索就非常的脆弱:遇上稍大一点的并发就很容易生成相同时间戳dateline的记录,而再加上相同tid条件其几率也不低。 于是对于需要按照先后排序的主题/跟帖来说就无法采用dateline作为唯一的、权威的时间参照物
而dz开发人员在做SQL查询时根本就没想到这一点,基本所有排序都几乎只使用了一个字段即dateline而没考虑当dateline重复时需要第二个排序字段参与排序,当然自然也没考虑到即便采用了(dateline, pid)双字段排序其结果是否能保证逻辑上的一致性。

说回到X2.5,它采用在post表增加postion字段来固定跟帖的楼号这个做法一并实现了对分页性能的极大提高以及抢楼帖功能的实现。但是post.position字段的自增长类型决定了其线索顺序是在数据层实现,这也就使得X2.5系统的时间参照体系有了两种:一对于主题/跟帖来说是数据层的position,二对于帖子之外的记录(比如交易、日志等等)是应用层的timestamp。这将会导致更大的混乱——你戴一块表能知道“准确”时间,你戴两块表甚至更多的可能反而不能知道哪个是正确的时间了……
并且,增加的这个position字段也要占用空间。以空间换时间走向极端也许反而得不到预期结果。反而会因为复杂的设计导致复杂的甚至稀奇古怪的bug,比如
https://discuz.dismall.com/thread-3097712-1-1.html
https://discuz.dismall.com/forum.php?mod=viewthread&tid=3026155
https://discuz.dismall.com/thread-3101290-1-1.html  这个是多web服务器时钟不同步造成的


那么,是否有个在时间与空间取得平衡,在功能与逻辑上兼顾的设计方案呢?
请听下回分解: 【从Discuz盖楼抢楼机制发展变化的思考中原创出的终极分页方案——分页静态化】




评分

3

查看全部评分

 楼主| mark35 发表于 2012-9-12 22:28:17 | 显示全部楼层
本帖最后由 mark35 于 2012-12-3 19:53 编辑

dz7.2的这个函数是不严谨的

  1. function savepostposition($tid, $pid, $position = 0) {
  2.     global $db, $tablepre;
  3.     if(!$position) {
  4.         $pos = $db->result_first("SELECT max(position) FROM {$tablepre}postposition WHERE tid='$tid'");
  5.         $pos ++;     // <<<<<< 这儿
  6.     } else {
  7.         $pos = $position;
  8.     }
  9.     $res = $db->query("INSERT INTO {$tablepre}postposition SET tid='$tid', position='$pos', pid='$pid'");
  10.     return $res;
  11. }
复制代码
当某主题第一次调用时进入第一个分支,但是
  1. $db->result_first("SELECT max(position) FROM {$tablepre}postposition WHERE tid='$tid'");
复制代码
结果是 FALSE,而 (FALSE)++ 的结果依然是 FALSE。只是更加不严谨的mysql热心地主动地帮你把bool类型的FALSE转换成0入库,同样的情况会出现在 TRUE, NULL,'' (空字符串) ,前一个TRUE转化成1, 后几个全部转为0。这种自动转换是相当不严谨的处理方式.

另外,程序中对savepostposition()函数的调用都是不带第三参数的于是就多做个查询,效率上也是个问题。

好在X2因为对position使用了自增长类型,所以改变了此函数,不存在这个语义错误了


回复

使用道具 举报

腐朽的木头 发表于 2012-9-12 11:40:18 | 显示全部楼层
金币的  看不了  路过下
回复

使用道具 举报

 楼主| mark35 发表于 2012-9-12 11:43:57 | 显示全部楼层
腐朽的木头 发表于 2012-9-12 11:40
金币的  看不了  路过下

我本来保存草稿待以后编辑,没想到就发出了。于是想避免被插楼,于是设定了金币售价,结果发现正确的应该采用设置阅读权限……

ps,你的金币比我多多了
回复

使用道具 举报

 楼主| mark35 发表于 2012-9-12 16:07:46 | 显示全部楼层
更新完毕

之前是保存草稿的,不知道为何就发出了。X2.5总是让人意外……
回复

使用道具 举报

guoysbest 发表于 2012-9-12 22:19:18 | 显示全部楼层
牛啊 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!
回复

使用道具 举报

乄恛☆憶_→ 发表于 2012-9-12 22:48:41 | 显示全部楼层
我顶楼行不  
回复

使用道具 举报

misskao 发表于 2012-9-13 01:42:01 | 显示全部楼层
大師必挺啊
回复

使用道具 举报

reo126 发表于 2012-9-13 16:09:42 | 显示全部楼层
好长,没看完,技术帝类的帖子要顶
回复

使用道具 举报

leifu1 发表于 2012-9-13 19:53:16 | 显示全部楼层
这个必须顶啊!www.nannv.info
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|Discuz! 官方站 ( 皖ICP备16010102号 )star

GMT+8, 2024-9-28 00:04 , Processed in 0.134308 second(s), 21 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表