我们有这样一个web需求:存入一批item,每个item有一个唯一的id,其他数据还包括title、pic、price等。写入目前由人工后台编辑(今后可能会被算法给出),删除也由后台人工和定时任务完成。该数据会被频繁读取,前台读取这些数据的时候,需要从一批(初始在百-千数量级)item里选出10-20个进行展示。

不想放在数据库里,一方面是出于效率考虑,再一方面是针对这种灵活需求,不想建表,而且cabinet本身的永久存储和高效读取也很好用。

我用的是php,使用TokyoTyrant扩展。有两种选择:

  1. 将所有的id存放在一个key-value对item_total中,每个item单独存放在一个key-value对item_[id]。当读取的时候,先读出item_total中的内容,shuffle,随机获取一批ids,再从item_[id]里读取具体的内容。
  2. 每个item单独存放在一个key-value对item_[id]。读取时,先使用fwmKeys获取所有以item_开头的keys,随机获取一批,再从item_[id]里读取具体内容。

测试了两者的效率:单线程。在item_total里存储了1w条ids,获取并随机100次;从item_前缀里获取1w条,再随机获取。两者的时间差不多,都在1s左右,方法1稍快。

编码的便利性方面:方法1,需要多维护一个item_total。方法2,只需要操作item_[id]即可。

最后,想看看两者的具体区别。我使用的B+tree cabinet存储类型。

当使用方法1的时候,需要调用php的fwmKeys方法。TokyoTyrant调用到tokyotyrant里的tcrdbfwmkeysimpl方法,向cabinet server发送socket请求,封装的请求命令是TTCMDFWMKEYS。server端即tokyocabinet的tcbdb.c中的tcbdbfwmkeys方法最终处理该请求:

/* Get forward matching keys in a B+ tree database object. */
  TCLIST *tcbdbfwmkeys(TCBDB *bdb, const void *pbuf, int psiz, int max){
    assert(bdb && pbuf && psiz >= 0);
    TCLIST *keys = tclistnew();
    if(!BDBLOCKMETHOD(bdb, false)) return keys;
    if(!bdb->open){
      tcbdbsetecode(bdb, TCEINVALID, __FILE__, __LINE__, __func__);
      BDBUNLOCKMETHOD(bdb);
      return keys;
    }
    tcbdbrangefwm(bdb, pbuf, psiz, max, keys);
    bool adj = TCMAPRNUM(bdb->leafc) > bdb->lcnum || TCMAPRNUM(bdb->nodec) > bdb->ncnum;
    BDBUNLOCKMETHOD(bdb);
    if(adj && BDBLOCKMETHOD(bdb, true)){
      tcbdbcacheadjust(bdb);
      BDBUNLOCKMETHOD(bdb);
    }
    return keys;
  }

这里注意到BDBLOCKMETHOD和BDBUNLOCKMETHOD方法。第一次调用的是非独占性锁,第二次调用的是写入锁。

当使用方法2的时候,由于cabinet中有多个get方法,没有具体定位到,但是大致浏览和猜想了一下,肯定不会有写入锁。

所以,综合考虑,还是使用方法2,以应用web中的大量并发访问。

Leave a Reply