分类目录归档:程序生活

sg工作吐槽和总结

上一段工作终于结束了。(实际上已经快结束一个月了)

走的时候,群里面的小伙伴这么说我“再也看不到他和老大之间的博弈了”,咋一看到这个评论我还是挺吃惊的。

虽然确实我和部门老大之间没有什么多余的话来说,也没有任何的阿谀奉承。只是纯粹的工作说明,问题讲解等。要是说有什么的话,

那就是我确实不太喜欢这样的一个人。其次再就是我要求离职时,三番五次的找他确定离职日期,估计他比较烦这个。然后在其他同事的眼中,

我看起来就是这样的不停的和部门老大“博弈”了。

 

其实吧,在近半年的工作时间中,个人一直没有100%投入到工作中,其中有环境的原因,有工作内容的因素等。导致这样的问题。

主要原因在于我个人。当初13年离职之后,女朋友刚好到了这家公司来上班,而我满心欢喜的全想的是怎么和女朋友更近一些。

刚好之前的公司同事也调到这家公司了,然后他和人事引荐了一下,我就直接面试然后过来了。问题已经出现情侣之间最好最好不要在一间公司!

就算是不同的楼层也不行,影响真的会很大。比如吃饭你们腻着一起吃,与同事不能形成圈子。然后走着碰到了,还生怕同事看到。躲躲闪闪,用女朋友的话

就是“我不喜欢这样被人看到”,在我看来其实就是她内心敏感,怕面对其他人的眼神。

这是工作圈子方面的问题,其次就是找工作之前必须要理解你要做的这份工作到底是做什么的?!

我的工作经历已经3年多了,可是还是认知错误!

面试工作确实很糟糕,那个时候已经失去判断力了,(总是想和女朋友近一点,严重的不区分私人与工作的关系)

明知道这份工作不是web端方面,而是偏向数据库系统分析端的工作,却最终还是留了下来,从一开始就认定这份工作是13年半年的过渡期了。

这样就导致了后面的工作并不能100%投入,与同事之间的关系也没有用心建立。然后一切都是非正常的工作环境了,但是个人保证所有的工作

方面的任务,以及计划安排等等,都是用心去做的。需求开发也都是尽心尽力的完成。但是个人真的不喜欢这工作(有关这个工作的可以看看这里)。

 

由于对于一份工作几乎没有热情投入,所以对于同事,都处于一种相互不能理解的状态,这就导致了我们之间不是相处不来,而是从来没有尝试相互了解过。

这一点也是我快要离职时,所需要反思的。他们也没有想象中的那么死板和无聊。但是隔阂终究是还在那。

 

工作中有很多想让我觉得这是一份什么样的工作啊,我觉得我每天上班想想三件事情用来安慰自己,一个就是我在忍一下就可以辞职了,第二个就是马上就到周五了,三就是能和女朋友进些。

其他的很多事情都会让我觉得这样的工作简直是破我最低的底线了。

工作中遇到了很多奇葩的事情,比如上班第一天,个人感觉机器很烂键盘很渣显示器很小,机器烂显示器小都不说了。但是键盘简直不如网吧的那种破烂键盘,

这就不能忍了,于是rtx问运维能否换一个。运维答复“键盘坏了么”,我说没坏,他说“那不用换了,要是人人都像你这样没坏就要求换东西,那公司的资产还怎么管理”

真是,多么敬业啊,公司资产。掷地有声啊,于是我就答知道了。第二天对面位子没人,直接过去把键盘替换了。对于这样的sb就不用继续搭理了。

好像过了一周左右,运维路过看到我键盘换了,问了一声“你把键盘换了”,我答了一个字“恩”。不想多说任何啊

 

还有一个因素是,上一级的程序员X,我并不认可。X比我早来一个月左右吧,算作是我的上一级。主要来说是一个任务派发人。说实话,个人认为X这样一个人,有想法能写出东西,基础也不错。

但是沟通上很难讲清楚一个事情,很多方面都要靠我问是还是不是,再次作为一个程序员几乎没有代码组织和程序结构方面的意识

比如一个Controller从参数获取到数据库调用,在到模板赋值一路写到黑。我真心觉得要不是Thinkphp做了一个模板分层,用$this->display();调用。他一定觉得可以直接在模板上写上

大段大段的业务逻辑和函数穿插调用。X就是这样的人,一切都已实现目标完成任务,丝毫不考虑扩展以及后来的维护。在为数不多的修改查看他的代码中,看到4、5次的foreach调用,

大段大段的重复代码我真是受不了啊!!详细代码请查看我的这篇。在后来我新写一个项目,全部的重新搭建,结构分层。一个Controller里面至多只有两行代码,就能实现一个业务。

充分的利用了Model的作用以及保持View的干净。于是他也直接这样利用我的这些代码来了,我和他讲“我跟你提一个建议,为什么你不用数据分层,避免代码冗余和后期好维护呢?”

他答“我知道你的那些做法,我觉得那些没有什么用”,我语塞。不知道怎么说了,之后就是各负责各的项目,各有各的代码风格,没有交流。还有一点要吐槽的就是,你用Editplus我不反对,但是

代码组织的结构简直是稀巴烂啊,各种table缩进不对齐,大括号之间各种空格,各种奇怪的换行,或者说是一行几百个字符。

也就是这样一个人,我真心觉得在代码层次上,真的做的不够好。还有很多程序理念上的冲突,我坚持要清洁干净有序的逻辑。他坚持简单直接,完成功能就行。

矛盾很多,不过后来我尽量的避免和他争论这些,因为负责项目不同各写各的罢了。不过X这个人的动手实现的能力还是很不错的,这点我还是需要学习的。

哦,对了X也离职了,他的离职原因是“这个工作太无聊了”。那段时间我也离职了,我在想X一个人每天在那琢磨出来的代码,后来的人怎么维护和开发呢。

同时我也在庆幸,不用去读那些冗余枯燥的代码了。

 

同样的这个因素也是因为一个人,运营部门老大Z,我觉得Z的几种方式我很不喜欢,第一个用加班时间长短来认可你的工作效率,第二个注重形式化,三不重视程序

其中第一大点就是我极其不喜欢是下班了,然后坐那两个小时就算是工作积极上班努力了。但是恰恰这个Z就是喜欢这种方式,由于是运营部,客服,分析人员下班后

在那坐一两个小时,刷两个小时的网页游戏,就认为是工作积极表现优秀了。而像我这样的每天准点下班的人则是不积极不优秀了,而X不同,他会经常多待一个小时了

之后才会中,某天X找我聊,大意就是说要我每天下班了要多留一个小时,不要准时走。我心中呵呵,除非临时有事情,其他时间还是准时走人。还有周一开会,那是我

刚去,我问X,我需要去么,X说不用,开会内容对我没什么意思,于是我不用去开会。个人本来就觉得这种会议毫无用处。后来试用期过了我依旧不去开会。

我听说这种会议7点开始,有时9点结束,有时尽然到11点才结束!!当然啦,这么晚回去公司是报销的士费的,很好是不是?

我初次听到真是难以想象,有什么很重要的事情需要开这么长的会?在我看来这些会议都是浪费时间,庆幸我还好没参加。

有趣的是,今年年初sg公司大幅度裁员,打车费也没有了,只保留了加班盒饭。于是我就看到部门周一例会取消了,改为每天早上十分钟几个同事汇报事情就好了。

这真是有意思啊,本来三五句就能说清楚的,偏偏要坐桌子而谈,搞得那么的形式化。

Z对于程序一概不懂,这没关系,但是感觉运营部门对于很重要的日志程序几乎是一点都不重视,但是却很重视日志分析系统的数据内容。也就是只要结果不要过程,

这点在代码中体现的淋漓尽致,在我看来运营部门的日志分析程序员,工作最长的也就是一年吧。大部分都是半年左右时间就离开了。这样代码结构混乱,几乎来一批

人是一种写法,又来一批人是另一种写法。可能Z觉得程序员没什么大不了的,客服,分析人员很好招啊,程序员也很好招的。所以可以预见的是,数据分析的CMS系统永远

只会停留在能用的基础上,不可能会好用或者是系统化模块化。

 

还有这部门属性或者说是页游公司属性,我个人觉得真的与运营这样的部门合不来,运营里面从Z到客服每天都把游戏网页打开,不停地在里面打怪,跑。每天讨论的就是

元宝,消耗,礼包,金币,各种各样的道具名词。等等。看着他们那样沉浸其中,我真是显得格格不入啊。我觉得他们的工作都是无聊到爆啊,但是他们却满目开心,在

RPG的世界里自娱自乐。确实是很难以理解啊,就像女朋友每次跟我说一样,你管它什么意义啊,有钱有工资不就好了吗。我还是觉得吧,钱是要赚的,但是工作还是要开心。

 

从一开始我就确定要离开的,算是为自己的错误买单了。

 

 

 

 

 

 

 

 

 

 

 

些许吐槽

今年换了一个新的工作,主要工作内容还是写代码,不过工作性质由之前的移动应用服务器后端api转变为日志数据内容的读取和分析。

实话来说,这个是换糟糕了。这个等过些时段在详细记录一下,其中有着各种错误。

 

暂且不说工作的对或错的问题,还是吐槽一下这个分析系统的代码。

这套系统(说是系统不如说就是cms)主要目的是供内部人员或者合作方的客服查看数据,分析数据使用。这个cms采用的是ThinkPHP3.1 Release框架开发,开发完成后的代码量也不多,代码结构和逻辑也复杂。

但我所认为槽点确是相当的多啊。

1、业务实现结构混乱

ThinkPHP怎么说也是PHP5,ORM面向对象的开发框架。不过在这系统里面,Controller的业务处理作用几乎是100%,Model几乎被闲置,最最关键的是开发人员说“我觉得Model没有什么用”,我只能表示黑线了。而现在我尽可能的使用Model构建业务,Controller做转发和调用。

 

2、代码结构不分块,几乎没有组织

是的你没看错,foreach一共嵌套了5层!!!对于能写出这样代码的我深感佩服,你有信心读下去吗?!假如你刚好还要维护这样的代码呢??你一定会崩溃。所幸我目前还不用维护这一部分。我认为不论多么复杂的逻辑以及实现方式,代码业务分块编写,简洁调用这是应该的,不管是维护修改和重新设计都会有好处。尽管这只是一个特例但是有很多Controller里面就是这样的写法,入switch case 块里面做 foreach ,foreach里面进行if else判断等等。

3、代码非常冗余


//导出excel列表
$import_excel=$params['import_excel'];
if ($import_excel == 1)
{
C ( 'SHOW_PAGE_TRACE', false );
header ( 'Content-Type: application/vnd.ms-excel' );
header ( 'Content-Disposition: attachment;filename=excel.xls' );
header ( 'Cache-Control: max-age=0' );
}

 

这是一段简单的excel导出代码,几乎每一个Controller里面都可能有这个需求,但是这段代码居然是每创建一个Controller就重新复制一遍。天哪你崩溃了吗,很简单的一个解决办法在父控制器类里面加入一个方法,子控制器有需要的调用该方法即可。


/**
* 查询list导出为excel
* @param int $import_excel
* @param string $outname
* @author Lei
* @time 2013-10-10 13:56:30
*/
protected function headerExcel($import_excel, $outname){
if ($import_excel == 1) {
C ( 'SHOW_PAGE_TRACE', false );
header ( 'Content-Type: application/vnd.ms-excel' );
header ( 'Content-Disposition: attachment;filename='.$outname.'.xls' );
header ( 'Cache-Control: max-age=0' );
}
}

 

同样还有很多地方的代码相当冗余,几乎每一个类文件里面都包含那几个方法,差别的只有对应的参数值。

4、条件分支被大量滥用

$table_name="";
if($this->partition=='1'){
$table_name=C("DB_PREFIX").$this->tablename.date("Ymd",strtotime($date));
if(empty($mksql_array[$table_name])){
$_makesql=str_replace("{TABLE_NAME}",$table_name,$this->_makesql);
$sql = "select TABLE_NAME from INFORMATION_SCHEMA.TABLES where `TABLE_SCHEMA`='".C("DB_NAME")."' and  TABLE_NAME ='".$table_name."'";
$tablelist = $model->query($sql);
if(empty($tablelist)){
$model->query($_makesql);
}
$mksql_array[$table_name]=1;
}

}elseif($this->partition=='2'){
$table_name=C("DB_PREFIX").$this->tablename.date("Ym",strtotime($date));
if(empty($mksql_array[$table_name])){

$_makesql=str_replace("{TABLE_NAME}",$table_name,$this->_makesql);

$sql = "select TABLE_NAME from INFORMATION_SCHEMA.TABLES where `TABLE_SCHEMA`='".C("DB_NAME")."' and  TABLE_NAME ='".$table_name."'";
$tablelist = $model->query($sql);
if(empty($tablelist)){
$model->query($_makesql);
}
$mksql_array[$table_name]=1;
}
}else{
$table_name=C("DB_PREFIX").$this->tablename;
if(empty($mksql_array[$table_name])){
$_makesql=str_replace("{TABLE_NAME}",$table_name,$this->_makesql);
$sql = "select TABLE_NAME from INFORMATION_SCHEMA.TABLES where `TABLE_SCHEMA`='".C("DB_NAME")."' and  TABLE_NAME ='".$table_name."'";
$tablelist = $model->query($sql);
if(empty($tablelist)){
$model->query($_makesql);
}
$mksql_array[$table_name]=1;
}
$table_name=C("DB_PREFIX").$this->tablename;
}

 

你会看到这里的if else 里面的块代码就是一模一样的,只是上面的一个参数差别。显而易见的是我们可以写一个函数把参数传递进去即可。


/**
* 检测以及创建对应的logtable
*
* @param string $date
* @return string
* @author Lei.Lu
*/
protected function createLogTable($date){
$model = M ();
static $mksql_array = array ();
$table_name = "";

$dbprefix = C ( "DB_PREFIX" );

switch ($this->partition){
case '1':
$table_name = $dbprefix . $this->tablename . date ( "Ymd", strtotime ( $date ) );
break;
case '2':
$table_name = $dbprefix . $this->tablename . date ( "Ym", strtotime ( $date ) );
break;
default:
$table_name = $dbprefix . $this->tablename;
}

if (empty ( $mksql_array [$table_name] )) {
$_makesql = str_replace ( "{TABLE_NAME}", $table_name, $this->_makesql );
$sql = "select TABLE_NAME from INFORMATION_SCHEMA.TABLES where `TABLE_SCHEMA`='" . C ( "DB_NAME" ) . "' and  TABLE_NAME ='" . $table_name . "'";
$tablelist = $model->query ( $sql );
if (empty ( $tablelist )) {
$model->query ( $_makesql );
}
$mksql_array [$table_name] = 1;
}

return $table_name;
}

5、各种不规范的地方

代码逻辑不用了,注释起来。不删除

这样的变量赋值

╮(╯▽╰)╭

魔法数字的滥用,写到哪个就是哪个

代码格式的不规范;变量、方法名的命名一会拼音,一会英文;单点登录的用户信息数据传输不加密。。。等等

 

总体而言,代码作者的完成意识一切以功能出来为主,在controller里想到哪写到哪,没有代码分块,系统MVC分层的概念,Model、module更不会用。更不用说那些规范方面的问题了,让人有各种糟糕的感觉。

 

尽管如此,但是我还是得坚持。

不过也不能说明这个cms就是一无所用的,存在即为合理,看来确实如此,所在部门包括我就两个程序,其他都是小白,如一个网页缓存没刷开页面都不知道怎么解决的那些人是不考虑这些问题的,既然在这里要求他人改变,他人不改变那么我也只好自己保持自我,直到不用忍耐的那天。

 

solr 4.3的一些错误解决方法

在去年时候学习使用了solr4.0,现在solr版本最新已经到了4.3了,前两天因为工作需要在一台服务器上面新安装solr,但是生产环境是4.0,不过想到是内部测试用的,且主要功能就是写入,删除,搜索,与程序上面没有太多的深入开发,于是还是安装了最新的4.3版本

解压安装启动后,就可以了;这时需要添加collection,添加的collection配置需要与生产环境保持一致,于是复制默认的collection1 的配置信息作为新的collection

复制完成,新的collection events 也添加完成;但是加载时总是报错不能正确加载solrconfig.xml信息,也知道schema.xml等数据肯定是要修改的,schema.xml配置信息修改完成后还是有这样的问题,在往solr写入数据时又再次报错

undefined filed message,但是message字段确实已经配置好了;检查之后再次重启solr,查看刚才的events 直接显示 “There exists no core with name “events””

这时去查看日志,显示信息

[javascript]

Caused by: org.apache.solr.common.SolrException: Error initializing QueryElevationComponent.
at org.apache.solr.handler.component.QueryElevationComponent.inform(QueryElevationComponent.java:218)
at org.apache.solr.core.SolrResourceLoader.inform(SolrResourceLoader.java:616)
at org.apache.solr.core.SolrCore.<init>(SolrCore.java:816)
… 34 more
Caused by: java.lang.NumberFormatException: For input string: "MA147LL/A"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
… 36 more
ERROR – 2013-05-22 16:07:06.752; org.apache.solr.common.SolrException; org.apache.solr.common.SolrException: Error CREATEing SolrCore ‘events’: Unable to create core: events
at org.apache.solr.handler.admin.CoreAdminHandler.handleCreateAction(CoreAdminHandler.java:524)

[/javascript]

 

[javascript]
ERROR – 2013-05-22 16:48:46.272; org.apache.solr.common.SolrException; org.apache.solr.common.SolrException: ERROR: [doc=100946] unknown field ‘message’
at org.apache.solr.update.DocumentBuilder.toDocument(DocumentBuilder.java:313)
at org.apache.solr.update.AddUpdateCommand.getLuceneDocument(AddUpdateCommand.java:73)
[/javascript]

正在排查中是,同事Y看到了,直接把solr 默认的collection1 改名为 events ,再次刷新直接挂了

There are no SolrCores running. Using the Solr Admin UI currently requires at least one SolrCore.

再次去查找这个问题,很快找到

http://stackoverflow.com/questions/13295208/i-unloaded-the-default-solr-collection-by-mistake-from-the-solr-admin-ui

编辑example/solr/solr.xml配置文件

可以看到已经变为

<core name="events" instanceDir="events" />

改为

<core name="collection1" instanceDir="collection1" />

保存,重启solr即可;

分析为什么出现这个问题,events collection的配置是错误的,solr初始化所有collection时跳过了events,而默认的collection1又改为了events则使用的是events目录下的配置信息了,

而这个配置信息又是错误的,所以solr admin 默认为没有 cores 的;修复过程中需要手动去修改配置文件,同事Y打呼用户体验太不友好了,不过我说Solr Admin用户体验很好啊,很早就不支持IE6了

现在又回到了上面的那个错误,org.apache.solr.common.SolrException: Error initializing QueryElevationComponent.

不能初始化,也不能添加events collection core,继续google,终于找到一篇提示的文章

https://coderwall.com/p/kwvxhq

Note that if you have enabled the QueryElevationComponent in solrconfig.xml it requires the schema to have a uniqueKey of typeStrField. It cannot be, for example, an int field.

Otherwise, you will get exception like:

java.lang.NumberFormatException: For input string: "MA147LL/A"

大意就是如果你开启了 QueryElevationComponent 功能,但是schema 的uniqueKey类型又不是 string,则报如下错误

java.lang.NumberFormatException: For input string: "MA147LL/A" 这个不就是我的日志里面的那个错误信息么, 于是编辑example/solr/events/conf/solrconfig.xml配置文件

搜索QueryElevationComponent关键字,可以看到如下,果然有这个信息

[javascript]
<!– Query Elevation Component

http://wiki.apache.org/solr/QueryElevationComponent

a search component that enables you to configure the top
results for a given query regardless of the normal lucene
scoring.
–>
<searchComponent name="elevator" >
<!– pick a fieldType to analyze queries –>
<str name="queryFieldType">string</str>
<str name="config-file">elevate.xml</str>
</searchComponent>
[/javascript]

http://wiki.apache.org/solr/QueryElevationComponent 查看一下,类似于关键字搜索后,一些项的配置置顶显示 比如百度搜索某个关键字时,搜索框下面的推广,广告相关信息总是被置顶显示

要配置启用这项组件,需要配置elevate.xml,同样是位于example/solr/events/conf/目录下

Elevated query results are configured in an external .xml file determined by the config-file argument. An elevate.xml file may look like this:

<elevate>

 <query text="AAA">
  <doc id="A" />
  <doc id="B" />
 </query>

 <query text="ipod">
  <doc id="A" />

  <!-- you can optionally exclude documents from a query result -->
  <doc id="B" exclude="true" />
 </query>

</elevate>

For the above configuration, the query “AAA” would first return documents A and B, then whatever normally appears for the same query. For the query “ipod”, it would first return A, and would make sure that B is not in the result set.

Note: The uniqueKey field must currently be of type string for the QueryElevationComponent to operate properly. 这就是答案了,uniquekey必须是string类型;目前我们项目中没有用到这项功能,所以可以选择注释不启用

[javascript]
<!–
<searchComponent name="elevator" >
<!– pick a fieldType to analyze queries –>
<str name="queryFieldType">string</str>
<str name="config-file">elevate.xml</str>
</searchComponent>
–>
[/javascript]

重启之后,没有初始化失败的错误了,再次往solr加入数据又有一个错误信息 undefined field text

[javascript]

ERROR – 2013-05-22 17:59:51.107; org.apache.solr.common.SolrException; org.apache.solr.common.SolrException: undefined field text
at org.apache.solr.schema.IndexSchema.getDynamicFieldType(IndexSchema.java:1211)
at org.apache.solr.schema.IndexSchema$SolrQueryAnalyzer.getWrappedAnalyzer(IndexSchema.java:425)
at org.apache.lucene.analysis.AnalyzerWrapper.initReader(AnalyzerWrapper.java:81)
at org.apache.lucene.analysis.Analyzer.tokenStream(Analyzer.java:132)

[/javascript]

google 得到结果,就是默认字段需要替换的问题,编辑 example/solr/events/conf/solrconfig.xml 检索到text内容

 <lst name="defaults">
 <str name="echoParams">explicit</str>
 <int name="rows">10</int>
 <str name="df">text</str>
 </lst>

因为solrconfig.xml等配置文件时从collection1复制过来的,默认的default字段匹配是text,所以目前改为我们项目所用到的字段值message 保存文件,重启solr,写入数据没有问题了,search也正常的有数据内容返回了

版本的不同,配置文件内容也会做一些变动与修改;所以可能需要修改的配置不仅仅只是与项目search有关的内容,还有版本与版本之间,新版本默认启用的模块所需的配置有关;还有一点,多看日志,多用Google!

MongoDB实现mysql主键autoincrement

MongoDB新建表默认有一个_id字段来作为autoincrement自增实现,而这个_id字段类型是objectid类型(objectid 是12字节的BSON类型)。ObjectId的详细解释。

而现在因为数据库迁移,将项目中原mysql的一些locations表移植到mongodb上面。对于locations 使用mongodb 2d loc 索引能够更快的进行数据检索,同时避免原mysql需要经过复杂的经纬度查询。

那么数据迁移后主要的要求有

1、数据库表的迁移不会影响表与表之间的关系,继续保持表之间的主外键关系。

2、lat 与 lng 字段在mongodb 新表中合并为一个字段loc,同时对loc加以2d索引。

对于第二点主要影响是在代码之中,可以看看这里。就不做详细描述了。

那么现在主要要做的事情就是,维持表与表的关系与主外键关系。我们以event 与 对应的 event_locations为例

event中有一个外键location_id,location_id就是event_locations的主键id;而现在mongodb中新建event_locations表,同时要保持event_locations与event的主外键引用关系。

因为mongodb表中默认有着_id这样一个主键,那现在我们的数据迁移以及mongodb event_locations表结构构建有两种选择

1、event_locations 主键以_id,即将原mysql event_locations 的id 替换为 _id。也就是如下表结构


{ "_id" : ObjectId("51243bdb383ca83413000000"),  "topic_id" : 103874, "name" : "xixi aaahah", "address" : "xxxbbbb cccccc", "loc" : [ 30.11, 112.02 ], "created" : 1361329114, "updated" : 0 }

优缺点分析

优点就是 使用默认的_id主键值,速度快检索方便。_id自增不必维护,有mongodb自行完成。

缺点很明显 event中的location_id外键,原来类型是int,现在改用_id objectid类型作为主键id,location_id类型必须要更改为char(32)了。且程序代码还需要一定的更改。

最大的缺点就是目前项目已经上线,直接更改数据库表可能会带来不可预知的问题;对于运营维护可能带来很大压力。且对于目前event已有的location_id数据需要进行同步更新。

2、保持原来的event_locations表结构,使用id继续维持与event location_id的主外键关系。


{ "_id" : ObjectId("51243bdb383ca83413000000"), "id" : 2354, "topic_id" : 103874, "name" : "xixi aaahah", "address" : "xxxbbbb cccccc", "loc" : [ 30.11, 112.02 ], "created" : 1361329114, "updated" : 0 }

优缺点分析

优点 不必去更改event location_id的数据类型,且location_id的数据不必修改;只用将event_locations数据完整导入到新表中即可。

缺点 要维持id的自增属性,而mongodb默认是_id才有自增属性的,所以我们要用某种方法实现id的自增实现。_id会占用存储空间(默认的_id原本可以删除的,但考虑到以后可能会用到就保存了)。

我现在采用的是第2种方式,主要原因如下

1、项目已上线,且已经有了用户数据;对于直接更改数据表与操作数据会带来很大的风险。强烈不推荐。

2、event_locations数据几乎不用同步,只需要单向导入到mongodb 新表就好。

3、程序代码变动最小,目前程序代码更改就是新的loc(lng+lat)存储与id的自增实现。而第一种除了这些,还需要把所有的id替换为_id,数据类型也要从int变为string。

在确定了解决方法之后那么现在主要的难题就是怎么实现id的自增?

目前也有两种方法

1、传统实现,每次在event_locations insert之前,全表id desc排序得到最大的id值,对id值+1既得到了最新的id值从而实现了自增。

2、使用mongodb的findAndModify()方法实现,独立一个ids表用来记录所有可能需要id自增的值。

同样简要说明下这两种实现id自增的方法优劣。

第一种方法

优点 不用构建ids表(工具表),免于维护

缺点 每次都要进行全表索引排序且查找到最大id那条记录,在进行+1

第二种

优点 findandmodify() 原子操作(了解原子操作请查看这里),查找并修改,直接返回当前操作的这个文档内容。

缺点 要构建维护ids表,即项目中所有表可能需要自增的表都有一条记录存在于其中。

无论采用哪种方法都要修改程序代码,即id autoincrement部分需要程序实现。我采用的是第二种方法。

主要考虑还是性能问题,避免每次insert之前都要对event_locations进行全部的索引排序。而维护ids这样的表会很简单且性能上忽略不计,详细表结构如下


{ "_id" : ObjectId("5147ca1a08c2c1bc12abdb4c"), "id" : 2356, "tablename" : "topic" }

// _id 默认主键,mongodb自增

// id  表中自增主键值

// tablename 表名

获取下一个id的核心代码,继承了mongodb的ids Model。其中一个方法专门来做这个事情。


/**
* 构建ids用于存储所有表中需要自增的id值,结合findandmodify()来返回对应表最新的id
* @param string $tablename 表名
* @return int
*/
public function get_next_id($tablename = 'topic'){

// db.ids.findAndModify({update:{$inc: {id:1}},query:{tablename:'topic'},new:true});
$data = array(
'findAndModify' => 'ids',
'update' => array( '$inc' => array( 'id' => 1)),
'query' => array('tablename' => $tablename),
'new' => true
);
$idrecord = $this->db()->command( $data );
$newid = $idrecord['value'];

return $newid['id'];
}

// 等同于db.ids.findAndModify({query: {tablename: 'topic'}, update: {$inc: {id: 1}}});

这样在获取到最新的id值后,insert event_locations 时需要将id加入到sql语句中(原先id是autoincrement,id字段没有在sql中)。之后也就是同样的业务流程了。

扩展:

有时候可能会遇到一些奇怪的需求,比如一张表里面有两个自增id要实现。。。如此,上面的表结构就不能实现了。

<pre>// _id 默认主键,mongodb自增
// fieldname  表中自增字段名
// fieldval 表中自增id值
// tablename 表名</pre>

如此就行了哦

有时候也会碰到一些奇怪的需求,比如要从指定的值如200开始id自增。

那么其实用这样一句就可以了

db.ids.findAndModify({query: {tablename: 'topic'}, update: {id: 200,tablename: 'topic'}});
代码实现上就是注意要update时要把tablename也要传递进去了,因为update会更新所以的记录,如果不写上tablename那么返回的文档就只有一个id字段了。。。有需要的自己扩展下吧

需要注意的是event_locations id现在不是主键也没有任何索引,除非你加上了;所以这里还是强烈推荐加上索引

db.event_locations.ensureIndex({id: 1});

参考

在MongoDB中模拟Auto Increment

Create an Auto-Incrementing Sequence Field

findandmodify命令

mysql表分区创建使用学习

表分区的测试使用,主要内容来自于其他博客文章以及mysql5.1的参考手册

mysql测试版本:mysql5.5.28

mysql物理存储文件(有mysql配置的datadir决定存储路径)格式简介

数据库engine为MYISAM

frm表结构文件,myd表数据文件,myi表索引文件。

INNODB engine对应的表物理存储文件

innodb的数据库的物理文件结构为:

.frm文件

.ibd文件和.ibdata文件:

这两种文件都是存放innodb数据的文件,之所以用两种文件来存放innodb的数据,是因为innodb的数据存储方式能够通过配置来决定是使用共享表空间存放存储数据,还是用独享表空间存放存储数据。

独享表空间存储方式使用.ibd文件,并且每个表一个ibd文件

共享表空间存储方式使用.ibdata文件,所有表共同使用一个ibdata文件

创建分区

分区的一些优点包括:

·         与单个磁盘或文件系统分区相比,可以存储更多的数据。

·         对于那些已经失去保存意义的数据,通常可以通过删除与那些数据有关的分区,很容易地删除那些数据。相反地,在某些情况下,添加新数据的过程又可以通过为那些新数据专门增加一个新的分区,来很方便地实现。

通常和分区有关的其他优点包括下面列出的这些。MySQL 分区中的这些功能目前还没有实现,但是在我们的优先级列表中,具有高的优先级;我们希望在5.1的生产版本中,能包括这些功能。

·         一些查询可以得到极大的优化,这主要是借助于满足一个给定WHERE 语句的数据可以只保存在一个或多个分区内,这样在查找时就不用查找其他剩余的分区。因为分区可以在创建了分区表后进行修改,所以在第一次配置分区方案时还不曾这么做时,可以重新组织数据,来提高那些常用查询的效率。

·         涉及到例如SUM() 和 COUNT()这样聚合函数的查询,可以很容易地进行并行处理。这种查询的一个简单例子如 “SELECT salesperson_id, COUNT(orders) as order_total FROM sales GROUP BY salesperson_id;”。通过“并行”, 这意味着该查询可以在每个分区上同时进行,最终结果只需通过总计所有分区得到的结果。

·         通过跨多个磁盘来分散数据查询,来获得更大的查询吞吐量。

简而言之就是 数据管理优化,查询更快,数据查询并行

检测mysql是否支持分区


mysql&gt; show variables like
&quot;%partition%&quot;;
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| have_partitioning | YES   |
+-------------------+-------+
1 row in set

RANGE 分区:基于属于一个给定连续区间的列值,把多行分配给分区。


DROP TABLE IF EXISTS `p_range`;

CREATE TABLE `p_range` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` char(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY RANGE (id)
(PARTITION p0 VALUES LESS THAN (8) ENGINE = MyISAM) */;

range分区就是 partition by range(id) 表示按id 1-7的数据存储在p0分区;如果id大于7了则数据不能写入了,因为没有对应的数据分区来存储;
所以这时在创建分区时需要使用maxvalues关键字了

PARTITION BY RANGE (id)
(
PARTITION p0 VALUES LESS THAN (8),
PARTITION p1 VALUES LESS THAN MAXVALUE)

这样就表示,所有id大于7的数据记录存在在p1分区里。

RANGE分区在如下场合特别有用:

·         当需要删除“旧的”数据时。如果你使用上面最近的那个例子给出的分区方案,你只需简单地使用 “ALTER TABLE employees DROP PARTITION p0;”来删除所有在1991年前就已经停止工作的雇员相对应的所有行。对于有大量行的表,这比运行一个如“DELETE FROM employees WHERE YEAR(separated) <= 1990;”这样的一个DELETE查询要有效得多。

·         想要使用一个包含有日期或时间值,或包含有从一些其他级数开始增长的值的列。

·         经常运行直接依赖于用于分割表的列的查询。例如,当执行一个如“SELECT COUNT(*) FROM employees WHERE YEAR(separated) = 2000 GROUP BY store_id;”这样的查询时,MySQL可以很迅速地确定只有分区p2需要扫描,这是因为余下的分区不可能包含有符合该WHERE子句的任何记录。

LIST 分区:类似于按RANGE分区,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择。

list分区可以理解为按一个键的id区间进行数据存储,比如类型表 1,2,3,4的所有记录存储在p0里面,5,6,7,8存在在p1分区里面

这里与range分区一样,如果现在有条记录typeid是9,那么这条记录是不能存入的;

需要注意的是:LIST分区没有类似如“VALUES LESS THAN MAXVALUE”这样的包含其他值在内的定义。将要匹配的任何值都必须在值列表中找到。


DROP TABLE IF EXISTS `p_list`;

CREATE TABLE `p_list` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`typeid` mediumint(10) NOT NULL DEFAULT '0',
`typename` char(20) DEFAULT NULL,
PRIMARY KEY (`id`,`typeid`)
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY LIST (typeid)
(PARTITION p0 VALUES IN (1,2,3,4) ENGINE = MyISAM,
PARTITION p1 VALUES IN (5,6,7,8) ENGINE = MyISAM) */;

HASH分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL 中有效的、产生非负整数值的任何表达式。

HASH分区主要用来确保数据在预先确定数目的分区中平均分布。在RANGE和LIST分区中,必须明确指定一个给定的列值或列值集合应该保存在哪个分区中;而在HASH分区中,MySQL 自动完成这些工作,你所要做的只是基于将要被哈希的列值指定一个列值或表达式,以及指定被分区的表将要被分割成的分区数量。

要使用HASH分区来分割一个表,要在CREATE TABLE 语句上添加一个“PARTITION BY HASH (expr)”子句,其中“expr”是一个返回一个整数的表达式。它可以仅仅是字段类型为MySQL 整型的一列的名字。此外,你很可能需要在后面再添加一个“PARTITIONS num”子句,其中num 是一个非负的整数,它表示表将要被分割成分区的数量。如果没有包括一个PARTITIONS子句,那么分区的数量将默认为1。


DROP TABLE IF EXISTS `p_hash`;

CREATE TABLE `p_hash` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`storeid` mediumint(10) NOT NULL DEFAULT '0',
`storename` char(255) DEFAULT NULL,
PRIMARY KEY (`id`,`storeid`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY HASH (storeid)
PARTITIONS 4 */;

InnoDB引擎

简单点说就是数据的存入可以按 partition by hash(expr); 这里的expr可以是键名也可以是表达式比如YEAR(time),如果是表达式的情况下

“但是应当记住,每当插入或更新(或者可能删除)一行,这个表达式都要计算一次;这意味着非常复杂的表达式可能会引起性能问题,尤其是在执行同时影响大量行的运算(例如批量插入)的时候。 ”

在执行删除、写入、更新时这个表达式都会计算一次。

数据的分布采用基于用户函数结果的模数来确定使用哪个编号的分区。换句话,对于一个表达式“expr”,将要保存记录的分区编号为N ,其中“N = MOD(expr, num)”。

比如上面的storeid 为10;那么 N=MOD(10,4) ;N是等于2的,那么这条记录就存储在p2的分区里面。

如果插入一个表达式列值为’2005-09-15’的记录到表中,那么保存该条记录的分区确定如下:MOD(YEAR(‘2005-09-01’),4)  =  MOD(2005,4)  =  1 ;就存储在p1分区里面了。

“MySQL 5.1 还支持一个被称为“linear hashing(线性哈希功能)”的变量,它使用一个更加复杂的算法来确定新行插入到已经分区了的表中的位置。

线性哈希分区和常规哈希分区在语法上的唯一区别在于,在“PARTITION BY” 子句中添加“LINEAR”关键字;线性哈希功能使用的一个线性的2的幂(powers-of-two)运算法则

按照线性哈希分区的优点在于增加、删除、合并和拆分分区将变得更加快捷,有利于处理含有极其大量(1000GB)数据的表。

它的缺点在于,与使用常规HASH分区得到的数据分布相比,各个分区间数据的分布不大可能均衡。”

KEY 分区:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且MySQL 服务器提供其自身的哈希函数。必须有一列或多列包含整数值。


DROP TABLE IF EXISTS `p_key`;

CREATE TABLE `p_key` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`keyname` char(20) DEFAULT NULL,
`keyval` varchar(1000) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=12 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY KEY (id)
PARTITIONS 4 */;

按照KEY进行分区类似于按照HASH分区,除了HASH分区使用的用户定义的表达式,而KEY分区的 哈希函数是由MySQL 服务器提供。MySQL 簇(Cluster)使用函数MD5()来实现KEY分区;对于使用其他存储引擎的表,服务器使用其自己内部的 哈希函数,这些函数是基于与PASSWORD()一样的运算法则。

“CREATE TABLE … PARTITION BY KEY”的语法规则类似于创建一个通过HASH分区的表的规则。它们唯一的区别在于使用的关键字是KEY而不是HASH,并且KEY分区只采用一个或多个列名的一个列表。

与hash的区别就是,hash使用用户定义的表达式如YEAR(time) ;而key分区则是由mysql服务器提供的。同样KEY也是可以使用linear线性key的,与hash linear是相同的算法。

子分区:是分区表中每个分区的再次分割。


DROP TABLE IF EXISTS `p_subpartition`;

CREATE TABLE `p_subpartition` (
`id` int(10) DEFAULT NULL,
`title` char(255) NOT NULL,
`createtime` date NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8
/*!50100

PARTITION BY RANGE (YEAR(createtime))
SUBPARTITION BY HASH (MONTH(createtime))
(PARTITION p0 VALUES LESS THAN (2012)
(SUBPARTITION s1 ENGINE = MyISAM,
SUBPARTITION s2 ENGINE = MyISAM),
PARTITION p1 VALUES LESS THAN (2013)
(SUBPARTITION s3 ENGINE = MyISAM,
SUBPARTITION s4 ENGINE = MyISAM),
PARTITION p2 VALUES LESS THAN MAXVALUE
(SUBPARTITION s5 ENGINE = MyISAM,
SUBPARTITION s6 ENGINE = MyISAM)) */;

可以看到p_subpartition有三个分区p0,p1,p2;而这三个分区每一个又进一步分为2个分区。那么整个表都就分为6个小分区;

可以看到代表p_sobpartitionp0.myd的文件消失了,取代的是p_subpartition#p#p0#sp#s1.myd

在MySQL 5.1中,对于已经通过RANGE或LIST分区了的表再进行子分区是可能的。

子分区是分区表中每个分区的再次分割,子分区既可以使用HASH希分区,也可以使用KEY分区。这 也被称为复合分区(composite partitioning)。

1,如果一个分区中创建了子分区,其他分区也要有子分区

2,如果创建了了分区,每个分区中的子分区数必有相同

3,同一分区内的子分区,名字不相同,不同分区内的子分区名子可以相同(5.1.50不适用)

分区注意点

1、重新分区时,如果原分区里面存在maxvalue则新的分区里面也必须包含maxvalue否则就错误。
alter table p_range2x
reorganize partition p1,p2
into (partition p0 values less than (5), partition p1 values less than maxvalue);

[Err] 1520 – Reorganize of range partitions cannot change total ranges except for last partition where it can extend the range

2、分区删除时,数据也同样会被删除

alter table p_range drop partition p0;

3、如果range分区列表里面没有maxvalue则如有新数据大于现在分区range数据值那么这个数据是无法写入到数据库表的。

4、修改表名不需要 删除分区后在进行更改,修改表名后分区存储myd myi对应也会自动更改。

如果希望从所有分区删除所有的数据,但是又保留表的定义和表的分区模式,使用TRUNCATE TABLE命令。(请参见13.2.9节,“TRUNCATE语法”)。

如果希望改变表的分区而又不丢失数据,使用“ALTER TABLE … REORGANIZE PARTITION”语句。参见下面的内容,或者在13.1.2节,“ALTER TABLE语法” 中参考关于REORGANIZE PARTITION的信息。

5、对表进行分区时,不论采用哪种分区方式如果表中存在主键那么主键必须在分区列中。表分区的局限性。

6、list方式分区没有类似于range那种 less than maxvalue的写法,也就是说list分区表的所有数据都必须在分区字段的值列表集合中。

7、在MySQL 5.1版中,同一个分区表的所有分区必须使用同一个存储引擎;例如,不能对一个分区使用MyISAM,而对另一个使用InnoDB。

8、分区的名字是不区分大小写的,myp1与MYp1是相同的。

分区的管理

range与list分区的改变动作不能适用于hash与key方式的分区。删除与添加动作是都能使用的。

以下面的例子


DROP TABLE IF EXISTS `p_list`;

CREATE TABLE `p_list` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`typeid` mediumint(10) NOT NULL DEFAULT '0',
`typename` char(20) DEFAULT NULL,
PRIMARY KEY (`id`,`typeid`)
) ENGINE=MyISAM AUTO_INCREMENT=9 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY LIST (typeid)
(PARTITION p0 VALUES IN (1,2,3,4) ENGINE = MyISAM,
PARTITION p1 VALUES IN (5,6,7,8) ENGINE = MyISAM) */;

range与list分区的管理
删除分区
ALTER TABLE tr DROP PARTITION p1;
需要注意的是删除分区后,该分区的所有数据都没有了。同时删除后存在一个重大影响也就是typeid为5,6,7,8的记录是不能写入到该表了的!

清空数据

如果想要保留表结构与分区结构可以使用 TRUNCATE TABLE 清空表

更改分区保留数据

ALTER TABLE tbl_name REORGANIZE PARTITION partition_list INTO (partition_definitions);

如果想保留数据进行分区的更改

ALTER TABLE p_list REORGANIZE PARTITION p0 INTO (
 PARTITION s0 VALUES IN(1,2),
 PARTITION s1 VALUES IN(3,4),
);

这样就能进行分区的合并了,那怎么进行拆分呢

ALTER TABLE p_list REORGANIZE PARTITION s0,s1 INTO (
 PARTITION p0 VALUES IN(1,2,3,4),
); 使用 REORGANIZE PARTITION进行数据的合并与拆分,数据是没有丢失的。
在使用REORGANIZE进行重新分区时,需要注意几点:
1、用来确定新分区模式的PARTITION子句使用与用在CREATE TABLE中确定分区模式的PARTITION子句相同的规则。(partition 分区子句必须与创建原分区时的规则相同)
2、partition_definitions 列表中分区的合集应该与在partition_list 中命名分区的合集占有相同的区间或值集合。 (不管是合并还是拆分,s0,s1到p0;p0到s0,s1 里面的区间或者值都必须相同)
3、对于按照RANGE分区的表,只能重新组织相邻的分区;不能跳过RANGE分区。(比如按range年份 p0 1990,p1 2000 ,p2 2013三个分区;在合并时partition p0,p2 into()
   这样是不行的,因为这两个分区不是相邻的分区;)
4、不能使用REORGANIZE PARTITION来改变表的分区类型;也就是说,例如,不能把RANGE分区变为HASH分区,反之亦然。也不能使用该命令来改变分区表达式或列。

增加分区
ALTER TABLE p_list ADD PARTITION (PARTITION p2 VALUES IN (9, 10, 11));
但是不能使用
ALTER TABLE p_list ADD PARTITION (PARTITION p2 VALUES IN (9, 14));
这样mysql 会产生错误1465 (HY000): 在LIST分区中,同一个常数的多次定义
hash与key分区的管理在改变分区设置方面,按照HASH分区或KEY分区的表彼此非常相似,但是它们又与按照RANGE或LIST分区的表在很多方面有差别。
关于添加和删除按照RANGE或LIST进行分区的表的分区
不能使用与从按照RANGE或LIST分区的表中删除分区相同的方式,来从HASH或KEY分区的表中删除分区。但是,可以使用“ALTER TABLE ... COALESCE PARTITION”命令来合并HASH或KEY分区。
DROP TABLE IF EXISTS `p_hash`;

CREATE TABLE `p_hash` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`storeid` mediumint(10) NOT NULL DEFAULT '0',
`storename` char(255) DEFAULT NULL,
PRIMARY KEY (`id`,`storeid`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY HASH (storeid)
PARTITIONS 4 */;

如p_hash的分区数为4个; 要减少分区数为2个 ALTER TABLE p_hash COALESCE PARTITION 2 对于按照HASH,KEY,LINEAR HASH,或LINEAR KEY分区的表, COALESCE能起到同样的作用。

COALESCE不能用来增加分区的数量,如果你尝试这么做,结果会出现类似于下面的错误:

mysql> ALTER TABLE clients COALESCE PARTITION 18;
错误1478 (HY000): 不能移动所有分区,使用DROP TABLE代替

要增加顾客表的分区数量从12到18,使用“ALTER TABLE … ADD PARTITION”,具体如下:

ALTER TABLE clients ADD PARTITION PARTITIONS 18;

注释:“ALTER TABLE … REORGANIZE PARTITION”不能用于按照HASH或HASH分区的表。

分区维护

重建分区

这和先删除保存在分区中的所有记录,然后重新插入它们,具有同样的效果。它可用于整理分区碎片。

ALTER TABLE t1 REBUILD PARTITION (p0, p1);

优化分区

如果从分区中删除了大量的行,或者对一个带有可变长度的行(也就是说,有VARCHAR,BLOB,或TEXT类型的列)作了许多修改,

可以使用“ALTER TABLE … OPTIMIZE PARTITION”来收回没有使用的空间,并整理分区数据文件的碎片。

ALTER TABLE t1 OPTIMIZE PARTITION (p0, p1);

分析分区
读取并保存分区的键分布
ALTER TABLE t1 ANALYZE PARTITION (p3);

修补分区: 修补被破坏的分区。
ALTER TABLE t1 REPAIR PARTITION (p0,p1);

检查分区
可以使用几乎与对非分区表使用CHECK TABLE 相同的方式检查分区。
ALTER TABLE trb3 CHECK PARTITION (p1);
这个命令可以告诉你表t1的分区p1中的数据或索引是否已经被破坏。如果发生了这种情况,使用“ALTER TABLE ... REPAIR PARTITION”来修补该分区。

获取分区信息

在mysql服务器信息数据库里面的partitions存放着服务器所有表的分区信息。


-- explain partitions命令
explain partitions select * from p_hash
+----+-------------+--------+-------------+------+---------------+------+---------+------+------+-------+
| id | select_type | table  | partitions  | type | possible_keys | key  | key_len | ref  | rows | Extra |
+----+-------------+--------+-------------+------+---------------+------+---------+------+------+-------+
|  1 | SIMPLE      | p_hash | p0,p1,p2,p3 | ALL  | NULL          | NULL | NULL    | NULL |   10 |       |
+----+-------------+--------+-------------+------+---------------+------+---------+------+------+-------+

-- 获取到p_list表的分区详细信息。

select * from information_schema.`PARTITIONS` where TABLE_NAME = 'p_list';
-- 分区的创建信息
show create table p_list;

 

更多阅读:

mysql5.5 reference manmul partition

mysql表分区
mysql分区功能详细介绍以及实例

discuzx2系列之session的实现机制

discuzx2系列文章

discuzx2系列之群组积分排行

我们知道discuzx是没有采用php默认的session机制的,而是自己采用数据库表自己进行了构建一套session用户状态识别机制;而不是简单的把php session采用存入数据库的方式。

直接看代码

几个顶级的control入口是引用了下面的class_core.php;也就是discuzx核心类文件。


require './source/class/class_core.php';
$discuz = & discuz_core::instance();

$discuz->init();

可以再discuz_core类里面的,初始化了一系列模块


function init() {
if(!$this->initated) {
$this->_init_db();
$this->_init_memory();
$this->_init_user();
$this->_init_session();    // 初始化session
$this->_init_setting();
$this->_init_mobile();
$this->_init_cron();
$this->_init_misc();
$this->_init_connect();
}
$this->initated = true;
}

那就来看看_init_session()  这个函数的主要作用初始化session,检测已有session或者创建新的session作为当前连接的唯一标识
其中附加功能包括,当前连接用户ip的白名单,黑名单检测;确定用户的详细属性信息,如是新登陆的用户则更新用户的状态信息和在线统计信息


function _init_session() {
$this->session = new discuz_session();  // 实例化数据库session类

if($this->init_session)    {
// $this->var 就是超全局变量$_G 可以看到$this->var = & $_G;是在 _init_env()中引用赋值的
// 将目前cookie为sid的值和用户个人信息传入init;discuz_session 的init方法始终会返回一个实例对象,来表示当前的用户或游客
$this->session->init($this->var['cookie']['sid'], $this->var['clientip'], $this->var['uid']);

$this->var['sid'] = $this->session->sid;
$this->var['session'] = $this->session->var; // 当前用户对象的session数据

// 如果当前返回sid 不等于浏览器中名为sig cookie的值,则设置新的sid值1天后过期
if($this->var['sid'] != $this->var['cookie']['sid']) {
dsetcookie('sid', $this->var['sid'], 86400);
}
// $isnew 表示是否为网站新的用户,也就是新创建的session
if($this->session->isnew) {
// 是新来访用户,检测用户的ip是否在系统的可访问名单($_G['setting']['ipaccess'])或是否在系统的黑名单(common_banned禁止访问表;cache_ipbanned.php)中
if(ipbanned($this->var['clientip'])) {
// ip不在可访问名单或者ip在系统的黑名单中;则将用户移入 groupid=6的“禁止ip”组;游客的groupid是7
$this->session->set('groupid', 6);
}
}

// 如果是“禁止ip”组,显示系统提示信息
// "抱歉,您的 IP 地址不在被允许,或您的账号被禁用,无法访问本站点"
if($this->session->get('groupid') == 6) {
$this->var['member']['groupid'] = 6;
sysmessage('user_banned');
}
// 存在当前登录用户;用户是新登录的或者lastactivity最后活动时间在十分钟以前了
if($this->var['uid'] && ($this->session->isnew || ($this->session->get('lastactivity') + 600) &lt; TIMESTAMP)) {
// 重新设置lastactivity,接下来在footer.html中调用updatesession();时更新当前用户session记录信息 主要作用就是更新lastactivity;避免按在线时间$_G['setting']['onlinehold']过期被清除掉
$this->session->set('lastactivity', TIMESTAMP);
if($this->session->isnew) {
// 新的登录用户,更新用户的状态
DB::update('common_member_status', array('lastip' => $this->var['clientip'], 'lastvisit' => TIMESTAMP), "uid='".$this->var['uid']."'");
}
}

}
}

discuz_session类;主要作用对session进行操作,对应的是common_session记录表。其中$discuz->session 返回的就是浏览器当前连接用户的session实例对象了。($discuz就是discuz_core的引用实例)


class discuz_session {

var $sid = null;
var $var;
var $isnew = false;
var $newguest = array('sid' => 0, 'ip1' => 0, 'ip2' => 0, 'ip3' => 0, 'ip4' => 0,
'uid' => 0, 'username' => '', 'groupid' => 7, 'invisible' => 0, 'action' => 0,
'lastactivity' => 0, 'fid' => 0, 'tid' => 0, 'lastolupdate' => 0, 'country' => 0, 'email' => ''); //add country ,email

var $old =  array('sid' =>  '', 'ip' =>  '', 'uid' =>  0);

// 实例化,初始化session默认数据
function discuz_session($sid = '', $ip = '', $uid = 0) {
$this->old = array('sid' =>  $sid, 'ip' =>  $ip, 'uid' =>  $uid);
$this->var = $this->newguest;
if(!empty($ip)) {
$this->init($sid, $ip, $uid);
}
}

function set($key, $value) {
if(isset($this->newguest[$key])) {
$this->var[$key] = $value;
} elseif ($key == 'ip') {
$ips = explode('.', $value);
$this->set('ip1', $ips[0]);
$this->set('ip2', $ips[1]);
$this->set('ip3', $ips[2]);
$this->set('ip4', $ips[3]);
}
}

function get($key) {
if(isset($this->newguest[$key])) {
return $this->var[$key];
} elseif ($key == 'ip') {
return $this->get('ip1').'.'.$this->get('ip2').'.'.$this->get('ip3').'.'.$this->get('ip4');
}
}
// 初始化返回session实例对象
function init($sid, $ip, $uid) {
$this->old = array('sid' =>  $sid, 'ip' =>  $ip, 'uid' =>  $uid);
$session = array();
// cookie sid存在;读取记录
if($sid) {
$session = DB::fetch_first("SELECT * FROM ".DB::table('common_session').
" WHERE sid='$sid' AND CONCAT_WS('.', ip1,ip2,ip3,ip4)='$ip'");
}
// 不存在或存在session记录但与新的uid值不匹配
if(empty($session) || $session['uid'] != $uid) {
// 重新创建session记录
$session = $this->create($ip, $uid);
}

$this->var = $session;
$this->sid = $session['sid'];
}

// 返回新设置的session对象
function create($ip, $uid) {

$this->isnew = true;
$this->var = $this->newguest;
$this->set('sid', random(6));
$this->set('uid', $uid);
$this->set('ip', $ip);

//写入区域信息
include_once libfile('function/misc');
$ipaddress = convertip($ip);
//$ipaddress = mb_convert_encoding($ipaddress, 'UTF-8', 'GB2312');
$country = mb_substr($ipaddress, 2, 2, 'UTF-8');  // 获取两个字存入数据库

include_once libfile('local', 'include');
if (array_key_exists($country, $local_name)) { //如果数据在数组中
$country = $local_name[$country];
} else {// 如果不是则随机获取一个
$country = $local_name[array_rand($local_name)];
}
$this->set('country', $country);

$uid && $this->set('invisible', getuserprofile('invisible'));
$this->set('lastactivity', time());
$this->sid = $this->var['sid'];

return $this->var;
}

// 清除session记录,也就是清除掉已过期的连接
function delete() {

global $_G;
// onlinehold 系统设置的用户在线时间(分钟) 15
// 这里的$_G['setting']['onlinehold']已经换算成秒了;build_cache_setting()
$onlinehold = $_G['setting']['onlinehold'];
// 游客活动时间 1分钟
$guestspan = 60;
$onlinehold = time() - $onlinehold;
$guestspan = time() - $guestspan;

$condition = " sid='{$this->sid}' ";
$condition .= " OR lastactivity&lt;$onlinehold ";  // 活动时间距离现在超过了15分钟了
$condition .= " OR (uid='0' AND ip1='{$this->var['ip1']}' AND ip2='{$this->var['ip2']}' AND ip3='{$this->var['ip3']}' AND ip4='{$this->var['ip4']}' AND lastactivity>$guestspan) "; // 同一ip,访问活动时间在1分钟以内
$condition .= $this->var['uid'] ? " OR (uid='{$this->var['uid']}') " : ''; // 存在uid 也可按uid删除
DB::delete('common_session', $condition);
}

// 更新记录
function update() {
global $_G;
if($this->sid !== null) {

$data = daddslashes($this->var);
// 新创建用户session记录;且对符合提交的session记录进行清除
if($this->isnew) {
$this->delete();
DB::insert('common_session', $data, false, false, true);
} else {
// 更新session记录
DB::update('common_session', $data, "sid='$data[sid]'");
}
$_G['session'] = $data;
// 更新sid过期时间,等于是每次刷新都会重新更新
dsetcookie('sid', $this->sid, 86400);
}
}

// 在线人数统计 登录用户/游客统计
function onlinecount($type = 0) {
$condition = $type == 1 ? ' WHERE uid>0 ' : ($type == 2 ? ' WHERE invisible=1 ' : '');
return DB::result_first("SELECT count(*) FROM ".DB::table('common_session').$condition);
}

}

对应的php session中的有效期,回收机制就是delete()方法;写入与读取对应的就是update();那用户在浏览器端刷新后是怎么进行session的验证与返回的呢?session开启后,用户每次请求服务器都会进行数据验证与返回的;所以discuzx每次请求都会执行这个update()

discuzx采用的是页面载入模板的形式,对应的 template/default/common/footer.htm是每个页面都可能会加载的;有一些公共内容在这里,其中就有用户的session更新函数调用

<!–{eval updatesession();}–>

updatesession()函数


function updatesession($force = false) {

global $_G;
static $updated = false;

if(!$updated) {
// 登录用户
if($_G['uid']) {
// 获取当前用户最后活动时间且authcode编码后写入到浏览器cookie中
if($_G['cookie']['ulastactivity']) {
$ulastactivity = authcode($_G['cookie']['ulastactivity'], 'DECODE');
} else {
$ulastactivity = getuserprofile('lastactivity');
// 有效期长达一年
dsetcookie('ulastactivity', authcode($ulastactivity, 'ENCODE'), 31536000);
}
}
// 实例化核心类
$discuz = & discuz_core::instance();
// $discuz->session 当前这个用户的session对象

// 用户在线时间更新时长(分钟) 也就是在线用户多久更新一次自己的“在线时间”
// Discuz! 可统计每个用户总共和当月的在线时间,本设置用以设定更新用户在线时间的时间频率。例如设置为 10,则用户每在线 10 分钟更新一次记录。本设置值越小,则统计越精确,但消耗资源越大。建议设置为 5~30 范围内,0 为不记录用户在线时间
$oltimespan = $_G['setting']['oltimespan'];
// session记录中的“用户在线时间更新”的最后时间
$lastolupdate = $discuz->session->var['lastolupdate'];

// 登录用户 且存在“在线时间更新”频率 且时间段已过则进行用户的个人在线时间更新。
// 等于是用户在线时间是按每次10分钟,这样叠加的;间隔越小越准确。
if($_G['uid'] && $oltimespan && TIMESTAMP - ($lastolupdate ? $lastolupdate : $ulastactivity) > $oltimespan * 60)      {
DB::query("UPDATE ".DB::table('common_onlinetime')."
SET total=total+'$oltimespan', thismonth=thismonth+'$oltimespan', lastupdate='" . TIMESTAMP . "'
WHERE uid='{$_G['uid']}'");
if(!DB::affected_rows()) {
DB::insert('common_onlinetime', array(
'uid' => $_G['uid'],
'thismonth' => $oltimespan,
'total' => $oltimespan,
'lastupdate' => TIMESTAMP,
));
}
// 重新设置 “用户在线时间更新”的最后更新时间
$discuz->session->set('lastolupdate', TIMESTAMP);
}
foreach($discuz->session->var as $k => $v) {
if(isset($_G['member'][$k]) && $k != 'lastactivity') {
$discuz->session->set($k, $_G['member'][$k]);
}
}

foreach($_G['action'] as $k => $v) {
$discuz->session->set($k, $v);
}

// 这里进行session状态更新
$discuz->session->update();

$updated = true;
// 更新当前登录用户的统计信息与用户状态信息
if($_G['uid'] && TIMESTAMP - $ulastactivity > 21600) {
if($oltimespan && TIMESTAMP - $ulastactivity > 43200) {
$total = DB::result_first("SELECT total FROM ".DB::table('common_onlinetime')." WHERE uid='$_G[uid]'");
// 换算为分钟写入用户个人统计信息表
DB::update('common_member_count', array('oltime' => round(intval($total) / 60)), "uid='$_G[uid]'", 1);
}
dsetcookie('ulastactivity', authcode(TIMESTAMP, 'ENCODE'), 31536000);
DB::update('common_member_status', array('lastip' => $_G['clientip'], 'lastactivity' => TIMESTAMP, 'lastvisit' => TIMESTAMP), "uid='$_G[uid]'", 1);
}
}
return $updated;
}

session在程序中的流程。用户访问页面,初始化discuz_session实例;读取客户端cookie sid 查找对应的session;已存在则返回记录信息,设置cookie sid的过期时间;不存在则创建新的session记录,也就是一个新的用户,在创建新用户前,对session记录表按系统设置的用户在线时间、sid、uid、同一ip1分钟内活动记录这几个条件进行清理;(很关键的一点是只有在创建新用户session记录时,才会进行session记录的删除整理) 登陆用户与session的关系这里是一个很简单的实现,也就是

_init_session(); // 也就是$this->var[‘uid’] 是否是有值得,也就是$_G[‘uid’]是否存在。uid等用户个人信息赋值的过程是在login成功后调用setloginstatus($member,$lifetime);函数(function_member.php)

$this->session->init($this->var[‘cookie’][‘sid’], $this->var[‘clientip’], $this->var[‘uid’]);

这样用户uid就直接与session记录关联起来了。同时登陆用户会进行“个人在线时间信息”“用户个人状态”的数据更新处理。

common_session表结构

CREATE TABLE pre_common_session (
sid char(6) NOT NULL default ” COMMENT ‘sid’,
ip1 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
ip2 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
ip3 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
ip4 tinyint(3) unsigned NOT NULL default ‘0’ COMMENT ‘IP段’,
uid mediumint(8) unsigned NOT NULL default ‘0’ COMMENT ‘会员id’,
username char(15) NOT NULL default ” COMMENT ‘会员名’,
groupid smallint(6) unsigned NOT NULL default ‘0’ COMMENT ‘会员组’,
invisible tinyint(1) NOT NULL default ‘0’ COMMENT ‘是否隐身登录’,
`action` tinyint(1) unsigned NOT NULL default ‘0’ COMMENT ‘当前动作’,
lastactivity int(10) unsigned NOT NULL default ‘0’ COMMENT ‘最后活动时间’,
lastolupdate int(10) unsigned NOT NULL default ‘0’ COMMENT ‘在线时间最后更新’,
fid mediumint(8) unsigned NOT NULL default ‘0’ COMMENT ‘论坛id’,
tid mediumint(8) unsigned NOT NULL default ‘0’ COMMENT ‘主题id’,
UNIQUE KEY sid (sid),
KEY uid (uid)
) ENGINE=MEMORY DEFAULT CHARSET=utf8 COMMENT=’会员认证表’;

需要注意的是session表采用的是memory引擎,优点是常驻内存存取速度快,方便操作与管理;缺点则是如果服务器重启则数据都被清空,只读或读为主的访问模式(短时间用户激增写入量会很大)

更多了解:

MySQL Memory存储引擎:优势及性能测试

一天一笔记

上班也有三年多了,大概是从10年时候有一个对当天工作情况记录的习惯;都是记录在笔记本上面的,去年写完了一本,今年也买了一个新的笔记本,在9月份的时候突然笔没有墨水了,又多次搞忘记了买笔芯;所以转念一想为什么不自己搭建一个网上笔记记录的平台呢?

目前部署在sina sae平台上面想看的请点http://daynote.sinaapp.com

首页(图片都还是本地测试数据)

于是就计划开始做了,要最快的做出来;所以使用的是同事@ljyf5593用kohanaframework写的一个cms,后台功能很完善,用户、权限、后台工具等等都直接采用,也就是只需要做下前台以及数据的展示就ok了。

要说记笔记这样的事情,其实有很多现成的软件可用;各种云平台,云笔记等等,同事就是用的盛大麦库云笔记,如果你还想极客一点可以使用各种云平台的开放api开发一个自己的云笔记应用。不过我觉得目前自己用的这个就挺好;直接web页面访问,也不用安装各种客户端,也不想去折腾云平台api了。所以就先用着呗。

要说自己认为的亮点,我觉得有这么几个

#标记管理

不用搞成每一个note笔记前都放置一个checkbox,在建一个checkbox submit进行删除、导出、发送等其他操作。且用户点击note即选择,取消则再点击一次就ok。结果集操作nav可以随着用户的结果记录是否存在而自动的显示与隐藏。js很简单,看源码ok。

#导出,email发送

在用户选择标记后,note集合可以用来删除,导出,email发送。其中导出就是导出为excel到线下。

导出

本来想把同一天的所有不同时段内容进行分组的,不过我觉得phpexcel进行数据处理真的太难搞了。所以就直接一条笔记对应一条记录。kohana 使用phpexcel真的很方便,直接加module就好了。github上就有,开源的贡献者真伟大。

email发送

标记标签后发送到邮箱,就是着过样子啦。。。ps: title 上面已经修改好了striptags

吐槽:sae其实真的很强大了,但是也为了安全运维等因素导致sae环境与lamp环境还是有区别的。比如禁止本地io,.htaccess采用了config.yaml。所以导致一些类库代码在 sae上面跑不起来,也没有一个具体的详细错误,对于排除起来真的好麻烦。所以目前显线上sae平台上面,导出,email发送都是空白空白。。。还得找个时间单独的把phpexcel与email模块进行sae测试下,进行具体的代码排查。

=== 2013年1月4日21:34:54 更新===

phpexcel在sae上能正常数据导出了,原因是这样的,phpexcel默认是采用Excel2007进行数据导出的,所以也是采用的Excel2007实现类。而Excel2007实现类需要zip扩展来实现的(也就是zipArchive类,不过可以用zlib来实现对于zip读写,不过成本就太大了)。官方也考虑到了可能不会支持zip扩展的情况,于是就使用xml实现了PHPExcel_Settings::PCLZIP的PHPExcel_Shared_ZipArchive类,不过在sae环境上测试时提示”XMLWriter class  not found “也就是需要xmlwriter的扩展支持(PHP 5 >= 5.1.2, PECL xmlwriter >= 0.1.0) sae上面没有支持。

//writer/Excel2007.php
$zipClass = PHPExcel_Settings::getZipClass();
$objZip = new $zipClass();

本来想替换另一种实现或者是直接csv导出的,又看到phpexcel里面还可用Excel5进行数据导出;抱着试一试的态度看看,测试正常数据导出!这说明Excel5没有采用xml进行excel创建,详细还没看代码。

改为Excel5数据导出很简单


$objWriter = PHPExcel_IOFactory::createWriter($this->_spreadsheet, 'Excel5'); // 第二参数把Excel2007改为Excel5就行
$objWriter->save('php://output');
exit;

#支持markdown语法

markdown很流行,开源平台,大公司各种支持。不过markdown听闻版本分化严重啊,希望能有一个统一标准。确实很好用,如果没有一个格式体现,全是一段段文字看起来很累。https://github.com/michelf/php-markdown

#笔记note编写自动保存

这个很简单,直接setIntval 15秒自动保存,就是要区分创建与修改的动作。如果是创建则自动写入数据库,把note创建成功的noteid返回到表单,这时点击发布时实际上市修改了。如果你没有点击发布而是刷新了页面,那么这时还只是草稿,草稿数则+1;

#note笔记归档

用户笔记可以按照月份、年份进行排序实现。便于用户进行自我梳理查找。我觉得这里的作用应该大于笔记搜索;让用户可以看看自我的成长经历。个人就这挫设计,其实有更好的ui 设计,比如现在很流行的时间线。

其他还想实现的功能,比如可以让textarea支持table的(tabIndent.js)不过有时候我经常在touch,移动设备上面写,table这时现的很多余所以暂时没有加上去。还有同步发送到微博,不过貌似笔记是比较私密的事情,当然也有可以公开的暂时也没有加。

有喜欢的可以注册用下哟。

一个有关当前时间的sql排序实现

在项目中遇到了这样的一个需求

“在用户参与的一个活动列表中,获取当前时间中刚刚开始了(也就是刚才开始的,不是最先开始)的一条活动;如果当前时间没有活动开始,则获取将要开始的一条活动记录”且必须在一次数据库查询操作中完成。

也就是必须要在数据库中做活动的状态区分。

现在具象化一点的理解这个需求,有这样一个表t_time

里面只有三个字段

id,starttime,endtime

我们假定现在每一条记录就是一个活动。

现在可以把需求抽象的分析为“当前时间大于starttime且小于endtime的按desc到序排列获取第一条记录,当前时间小于starttime且小于endtime的按asc正序排列获取第一条记录”且同样是要在一条sql语句中完成。

于是开始进行sql编写,在sql要完成两种状态的辨别,那就肯定是需要条件分支类似的语句控制。

mysql中的条件语句分支,常用的就是if ,case when 语句。

在找到if then 的用法后,开始第一次if then语句的测试


if (SELECT * FROM `t_time` where starttime < UNIX_TIMESTAMP() and endtime >UNIX_TIMESTAMP())
then ( SELECT * FROM `t_time` where starttime < UNIX_TIMESTAMP() and endtime >UNIX_TIMESTAMP() order by starttime desc )
else if(SELECT * FROM t_time WHERE starttime > UNIX_TIMESTAMP() and endtime > UNIX_TIMESTAMP())
then ( SELECT * FROM `t_time` where starttime > UNIX_TIMESTAMP() and endtime >UNIX_TIMESTAMP() order by starttime asc )
end if;

结果是;华丽丽的报错啊!!!

Err] 1064 – You have an error in your SQL syntax;

考虑到语法没有问题啊。mysql真是有待学习啊,在网上搜索后了解到“if then endif只能在procedure或是function里用”,手册也没看,就乱来。而且不能为了这么一个处理还单独去创建一个存储过程或函数。

 

再查看case when语句,这次是能在sql中直接执行的。


select * from t_time where endtime > UNIX_TIMESTAMP()
and (case when starttime < UNIX_TIMESTAMP() then starttime < UNIX_TIMESTAMP() else starttime > UNIX_TIMESTAMP() end) order by starttime desc;

结果

但是还是没有达到预期效果,case when 没有起到作用,加上case when语句别名后的结果

可以看到case when 返回的是boolean结果;再次无效

 

于是继续查看手册还有没有办法,if()函数也能进行分支判断,可以一试。

IF(expr1,expr2,expr3)
如果 expr1 是TRUE (expr1 <> 0 and expr1 <> NULL),则 IF()的返回值为expr2; 否则返回值则为 expr3。IF() 的返回值为数字值或字符串值,具体情况视其所在语境而定。

if()函数可以返回数值或者是字符


SELECT *,if(starttime < UNIX_TIMESTAMP(), 'starttime < UNIX_TIMESTAMP()', 'starttime > UNIX_TIMESTAMP()') as t
FROM `t_time` where endtime > UNIX_TIMESTAMP() and t order by starttime desc;

 

 

结果还是语法错误,[Err] 1054 – Unknown column ‘t’ in ‘where clause’

把 and t去掉;sql语句是能正常执行的。结果如下

if()函数确实是执行了,但是返回的字符 别名为t,sql语句还是不认识。这个没搞明白。
无奈,只好求教于公司同事w;在简单说明后,他说项目里面有一处地方用到了这个,已经有了一个sql语句实现了;原来这个项目里面已经处理过这样的业务需求了,他说那时候第一次研究出来的,因为这个项目我是半路进来的,前面的开发都没参与;但隐约记得那一段时间他们组里的人都在说“写出一个牛x的sql排序了”,不过因为各种原因,我没去了解。哎,总归还是遇上了。

于是请w去讲解下这个处理方式。

在查看到项目源代码后,他说简而言之就是一个排序的实现,sql里面还是要进行两个状态的判断;

就是通过if()函数构建一个线性的排序


SELECT *,if( starttime < UNIX_TIMESTAMP(), starttime - UNIX_TIMESTAMP(), (starttime - UNIX_TIMESTAMP()) + 2000000) as t
FROM `t_time` where endtime > UNIX_TIMESTAMP() order by t asc;

分析if()函数的作用。

为true
也就是已经开始记录的排序,执行 starttime – UNIX_TIMESTAMP() 按得到的值;starttime asc排序这样就得到了“最先开始的活动记录,也就是按starttime asc排序”列表
为false
执行 (starttime – UNIX_TIMESTAMP())+20000000
starttime – UNIX_TIMESTAMP()这样得到了 “starttime距离现在最近的,也就是马上将要开始的starttime”记录列表;由于 “未开始记录的”starttime – UNIX_TIMESTAMP() 得到的值可能小于“已开始记录的”starttime – UNIX_TIMESTAMP() 的值,这种情况下排序就混乱了,所以要加上一个很大的值20000000避免这种情况,这样就能保证未开始记录的starttime永远大于已开始记录的 starttime值,这样未开始的值记录排在了已开始记录之后了。

( 图上的时间是假定的,就是为了说明已经开始的记录是哪几条。其实执行的时间也是算得出来的:-) )

这里我们的需求就快要实现了,可以看到已开始记录是按starttime  asc排序的。也就是得到的是“最先开始的活动记录”,而我们要的是“刚刚开始的活动记录”,实现这个其实很简单了

starttime < UNIX_TIMESTAMP() ,那么starttime – UNIX_TIMESTAMP() 则是一定为负数的;starttime越大,得出的绝对值越小。反之starttime越小,得出的绝对值则越大,但加上负号,则是starttime越小的,得出的负值越小了,最后sql语句都是asc排序的。结果集里面已开始的记录也就是asc排序的。

 

所以如果要starttime 正序desc排序,那就是 UNIX_TIMESTAMP() – starttime 表达式了,starttime越大的,得到的值越小;starttime越小的,得到值越大;因为最后sql语句是asc排序,结果集里面已开始的记录从排列上来说是desc倒序的。


SELECT *,if( starttime < UNIX_TIMESTAMP(), UNIX_TIMESTAMP() - starttime, (starttime - UNIX_TIMESTAMP()) + 20000000) as t
FROM `t_time` where endtime > UNIX_TIMESTAMP() order by t asc;

可以看到id为2的记录排到第一了。

 

这个业务需求在与当前时间有关且记录是否过期进行排序的列表中常应用到,一次请求后数据处理过程中;可以用endtime与当前时间进行对比,对于已过期的记录进行标记,这样显示时可以很明了了。

 

这个之前在ppc上面提问题了的,查看可以点击这里

 

Apache Solr使用简介

在一个项目中需要使用全文检索功能,架构w推荐了Apache Solr一个独立的企业级别搜索服务器。于是就顺势了解下这个solr搜索服务器。

简介

官网地址:http://lucene.apache.org/solr/

solr是基于lucene的一个搜索服务器,lucene也是Apache的一个开源项目;

Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,即它不是一个完整的全文检

索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。

Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起

完整的全文检索引擎。–来自百度百科

可以简单的说Lucene就是一个搜索引擎架构,而Solr则是一个搜索服务器的实现。

solr是使用java开发实现,目前已经到4.0版本了,运行需要java1.6或以上版本;基于http get进行xml/json/cvs/binary等格式类型的数据传输。solr的数据都是以document对象进行存储,每一个document都由许多field组成,且每一个document都有一个默认的uniqueid,默认的是id。

solr的安装很简单,下载完成后解压,进入solr/example目录

user:~/solr/example java -jar start.jar

输入http://ip:8983/solr/即可看到solr服务器的管理界面。

solr需要一个java服务器,solr服务器里面自带了jetty服务器所以不需要另立服务器,如果有tomcat也可以直接新开一个端口用来运行。

Dashboard是solr服务器的搭建路径,版本,运行环境,机器的物理内存,File Descriptor Count ,jvm内存使用情况等

Logging 则是运行的记录,包括错误,警告等

core admin则是 Schema 的管理界面,可以查看某个Schema的情况,同事还能新建,删除,优化等

java properties 即运行solr的java的所有详细参数

thread dump 运行线程的详细情况,各种数据跟踪

collection1 则是建立的Schema,可以查看Schema的记录数,版本数;还有如ping,query,config等管理操作

solr与php的结合实现

php的solr接口实现已现有代码实现了。项目地址:http://code.google.com/p/solr-php-client/

不过这里需要注意的是因为solr现在是4.0版本了,该php client实现里面还有几个需要注意的,如service.php 中实现add接口的addDocument方法,4.0版本的add属性参数已有改变,废弃了其中的allowDups,overwritePending,overwriteCommitted这几个参数所以需要将这几个参数去掉,否则solr服务器会报错。

一个很简单的例子

<?php

require_once 'Apache/Solr/Service.php';

$transportIntance = new Apache_Solr_HttpTransport_Curl();
//$transportIntance = new Apache_Solr_HttpTransport_CurlNoReuse();
// 如果这里有不同的Schema如ASchema与BSchema,则传入的第三个参数就是'/solr/ASchema/'与'/solr/BSchema/'
$solr = new Apache_Solr_Service('192.168.1.10', '8983', '/solr/');

// 检测solr服务是否可用
if($solr->ping()){
$solr->deleteByQuery('*:*');

//
// Create two documents to represent two auto parts.
// In practice, documents would likely be assembled from a
//   database query.
// 这里需要注意的是,使用的field必须存在于Schema中,否则document写入不到solr服务器,且检索时会产生
  // org.apache.solr.common.SolrException: ERROR: [doc=50cdce141f792] unknown field 'partno'的错误
  // 这是需要将partno等schema里面没有的field全部都加入到apache-solr-4.0.0/example/solr/collection1/conf/schema.xml <fields>里面
  // 写入到schema的 field 需要注意name与attribute,详细请看http://wiki.apache.org/solr/SchemaXml
  // 保存完成后重启服务器
$parts = array(
'spark_plug' => array(
'partno' => 1,
'name' => 'Spark plugdw',
//      'model' => array( 'Boxster', '924' ),
'year' => array( 1999, 2000 ),
'price' => 25.00,
'inStock' => true,
),
'windshield' => array(
'partno' => 2,
'name' => 'Windshield',
//      'model' => array('911', '111'), //multiple values encountered for non multiValued field model:
'year' => array( 1999, 2000 ),
'price' => 15.00,
'inStock' => false,
)
);

$documents = array();

foreach ( $parts as $item => $fields ) {
$part = new Apache_Solr_Document();
$part->id = uniqid();
foreach ( $fields as $key => $value ) {
if ( is_array( $value ) ) {
foreach ( $value as $datum ) {
$part->setMultiValue( $key, $datum );
}
}
else {
$part->$key = $value;
}
}

$documents[] = $part;
}

//var_dump($documents);

// Load the documents into the index

try {
$solr->addDocuments( $documents );
    // 这里需要注意在加入document后必须要commit提交到检索索引里面,否则最新写入的内容不会被检索到
$solr->commit();
   // optimize索引优化这里会重建索引,每次adddocument时不必要进行,应该放到用户搜索很少的时刻定时执行
   // 如每周末的24点执行,crontab也是一个选择
$solr->optimize();
}
catch ( Exception $e ) {
echo $e->getMessage();
}

// Run some queries. Provide the raw path, a starting offset
//   for result documents, and the maximum number of result
//   documents to return. You can also use a fourth parameter
//   to control how results are sorted and highlighted,
//   among other options.
//
$offset = 0;
$limit = 10;

$queries = array(
'partno:1',
'name:*w*'
);

foreach ( $queries as $query ) {
$response = $solr->search( $query, $offset, $limit );

if ( $response->getHttpStatus() == 200 ) {
// print_r( $response->getRawResponse() );

if ( $response->response->numFound > 0 ) {
echo "$query <br /><br/>";

foreach ( $response->response->docs as $doc ) {
echo "$doc->partno $doc->name <br />";
}

echo '<br />';
}
}
else {
echo $response->getHttpStatusMessage();
}
}

}else {
die('no connection solr service...');
}

?>

solr的query语法

根据不同的需要进行选择。

/**
* query语法
* 查询语法 http://lucene.apache.org/core/3_6_0/queryparsersyntax.html
* 1) title:”The Right Way” AND text:go // 标题的值匹配,AND text:go 表示对title默认的值为go的项进行筛选,但不是必须的
* 2) mod_date:[20020101 TO 20030101] //mod_date日期在20020101与20030101之间的
* 3) title:{Aida TO Carmen}     // 标题在Aida 与Carmen之间的集合,但不包括Aida Carmen
* 4) content:”jakarta apache” AND “Apache Lucene” // 搜索的字段必须包含 jakarta apache与Apache Lucene;可以使用&&代替AND
* 5) content:+jakarta lucene     // 必须包含jakarta 或可能包含lucene
* 6) content:”jakarta apache” jakarta // 结果中包含jakarta apache或jakarta ;或使用content:”jakarta apache” OR jakarta。可以使用||代替OR
* 7) content:”jakarta apache” NOT “Apache Lucene” // 包含jakarta apache,但不包含Apache Lucene;可以使用! 代替NOT
* 8) content:”jakarta apache” -“Apache Lucene” // 包含jakarta apache,但不包括Apache Lucene;与NOT区别还没明白
* 9) content:(jakarta OR apache) AND website // 使用括号实现子查询,包含jakarta OR apache以及website
* 10) title:(+return +”pink panther”)        // 使用括号将多个子句合并为一个字段,标题中必须包含单词return以及短语”pink panther”
* 11) title:tes? // 使用? * 来实现内容的搜索
* 12) 支持转移字符的查询,需转移字符包括+ – && || ! ( ) { } [ ] ^ ” ~ * ? : \ 可以使用\来转义字符,如title:\(1\+1\)\:2
* 13) title:roam~ // ~表示模糊查询,romam~将查找到foam或roams得单词;roam~0.8表示将得到roam 80%的相似度单词
* 14) content:”jakarta apache”~10 // 临近搜索,检索相隔10个单词的”apache”和”jakarta”
*/

solr的搜索是自动进行分词的,当然这是指英文的空格分词;如果中文需要分词怎么办呢?当然也有有心人已经做出来了,就是这个mmseg4j 作者博客上的简介 作者对于lucene solr很有研究,值得一看。

更多阅读

Solr Tutorial

Apache Solr Wiki

淘宝网综合业务平台团队博客 solr文章

Lucene/Solr Optimize相关总结

Solr监控

为什么每个程序员都要学C语言的五个理由

为什么要学习c语言【转】 我想如果要继续走下去,这个是必须的。

本文翻译自文章:Five more reasons why every programmer should learn。为传递信息之用,原文链接:http://www.jubuu.com/?id=13
1,C 语言不是面向对象语言。因为这一点,任何学习C语言的人必须学会用函数思考问题。当你真正的去学习一门面向对象语言时,就会有C语言的函数学习基础去对 比。这会使学习面向对象编程更容易理解和更有乐趣。


2,用C编程就像进行智力体操。你要顾及到每一件事情,而这在很多其他语言中他会为你处理。你要管理指针,内存分配,和内存回收。你要明白如何处理串,还有动态数据。在其他语言中,所有这些对你来说是隐藏的。你不知道他们如何运行,更不用说改变他们的运行方式了。你得到了使用的便利,失却了对程序的控制和速度。很多情况下,这是笔值得的买卖。其他的情况下就不是这样了。


3,不同情况下你会使用不同的编程语言。某些情况下要求使用Lisp,其他的要Java,还有别的要C++。但是C是你坚实的基础。你可能不会使用它做一 些项目,但是它会帮助提高你的能力,熟练的用其他语言编程。

4,除了汇编语言之外,C代码生成的程序比其他任何语言生成的程序来得更小和运行更快。那么为什么不学汇编语言呢?汇编是一门学了很有用的语言,但是它没有C那样的可移植性,并且其他流行的语言比如JAVA的语法是基于C的,而不是汇编。你仍然应该学汇编,但它并不会证明它会像学C那样有用。

5,如果你想要写一个视频游戏引擎或操作系统,你会需要C。你不能使用C#, Java, 或 Basic来完成这些编程任务。

GET与POST可传递的最大值到底是多少?

       前日,看到这个问题了。 没有深入了解。我的常识里面get最大传递的值为256b,post 是2M。这是很久以前不知在哪看到的。
于是又百度一下。看到两篇文章装过来看看:

浅谈 HTTP中Get与Post的区别

GET方式传值的最大长度

都有些道理。引用其中一些观点:

get
首先是"GET方式提交的数据最多只能是1024字节",因为GET是通过URL提交数据,那么GET可提交的数据量就跟URL的长度有直接关系 了。而实际上,URL不存在参数上限的问题HTTP协议规范没有对URL长度进行限制。这个限制是特定的浏览器及服务器对它的限制。IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持。   注意这是限制是整个URL长度,而不仅仅是你的参数值数据长度。

【get方式提交数据的大小,http协议并没有硬性限制;而是与浏览器及服务器、操作系统有关。第二篇文章里,就有谈到用apache测试,使用get方式,url最长可达8167b。其中跟帖留言中还有谈到代理的因素在里面,可能url太长还没到服务,就已经被代理拒绝掉了。第一篇的留言讨论很能学到东西哦~ 】

post
理论上讲,POST是没有大小限制的HTTP协议规范也没有进行大小限制,说“POST数据量存在 80K/100K的大小限制”是不准确的,POST数据是没有限制的,起限制作用的是服务器的处理程序的处理能力。

  对于ASP程序,Request对象处理每个表单域时存在100K的数据长度限制。但如果使用Request.BinaryRead则没有这个 限制。

  由这个延伸出去,对于IIS 6.0,微软出于安全考虑,加大了限制。我们还需要注意:

     1).IIS 6.0默认ASP POST数据量最大为200KB,每个表单域限制是100KB。
2).IIS 6.0默认上传文件的最大大小是4MB。
3).IIS 6.0默认最大请求头是16KB。
IIS 6.0之前没有这些限制。[见参考资料5]

  所以上面的80K,100K可能只是默认值而已(注:关于IIS4和IIS5的参数,我还没有确认),但肯定是可以自己设置的。由于每个版本的 IIS对这些参数的默认值都不一样,具体请参考相关的IIS配置文档。

【post方式,传送数据最大理论上没有限制,取决于服务器设置和内存大小。】

ftp上传533错误解决方案

用Vm装了个ubuntu练习了下,在用flash Fxp上传文件上去时。报这样“533 Could not create file”一个错。于是乎谷歌下。找到了解决答案 即用户的文件夹权限问题。解决方法如下

sudo chmod 770 /var/www     #设置新的目录权限为770。其中原文所写的这一句 chown root:ftp /home/ftp不是很明白。我也没有执行这一句。修改后,重启vsftpd。然后就可以上传了。

原帖链接:bbs.chinaunix.net/archiver/tid-577563.html