请看 '技术一般般' ↓

SQL Relay

你是不是也在寻找LAMP结构下的PHP连接池?如果没找到更好的,建议你试试SQL Relay.
之前我曾将写过一篇关于SQL Relay的blog,介绍了一下怎么把SQL Realy用在PEAR DB中。事实证明,这样使用并不能有效的缓解MySQL中产生大量连接的问题,因为PHP从SQL Relay得到的还是一个真实的数据库连接,也就是说PHP连接一次,SQL Relay就要提供一个,至于这个连接什么时候还给SQL Relay,还是PHP说了算,SQL Relay没有管理权,所以运行了一段时间之后SQL Relay就会一下子增加N个连接,然后崩溃。。。。。。希望你没碰到这种状况。
最近实验了一下用另外一种方式使用SQL Relay,首先建立一个SQL Relay服务器,直接连接数据库,然后PHP端只建立到SQL Relay服务器的TCP连接,并不参与对数据库连接的使用和管理,基本的原理就是PHP连接到SQL Relay服务器,传递要运行的SQL语句,SQL Relay负责把语句通过一个空闲的连接送到MySQL服务器去执行得到结果后立即断开,然后返回给PHP,这样大量的PHP请求传递给SQL Relay,后者负责去维护一个MySQL连接池,完全拥有对数据库连接的管理权。经过实验,效果比较理想。
安装和配置运行这里就不赘述了,有问题可以mail我,说一下怎么实现PHP到SQL Relay的连接。
安装成功之后,在php.ini里设置sqlrelay.so的extension信息,然后可以按照下面的方法写一个class,你可以按照getRow方法的样子去实现其他的查询方法。

class SQLR_DB
{
private $conn;
private $cur;
private function __contruct(){}//构造函数
private function _conn($server,$port,$user,$password,$retrytime,$tries)//建立连接的私有方法(server:SQL Relay的服务器
,port:端口,user:用户名,password:密码,retrytime:隔多长时间重新连接,tries:重新连接的次数)
{
$this -> conn = sqlrcon_alloc($server,$port,"",$user,$password,$retrytime,$tries);
$this -> cur = sqlrcur_alloc($this->conn);
}

private function retrieve($sql) //传递SQL语句到SQL Relay服务器,获得返回结果的方法
{
if( !$sql) return false;
$this -> _conn("server" , "port" , "user" , "password" , "retrytime", "tries"); if(!sqlrcur_sendQuery($this->cur,$sql))
{
echo sqlrcur_errorMessage($this->cur);
sqlrcur_free($this->cur); sqlrcon_free($this->conn);
return null; }
//sqlrcon_endSession($con); for($i=0; $i < sqlrcur_rowCount($this->cur); $i++)
{ $res[] = sqlrcur_getRowAssoc($this->cur,$i);
} sqlrcur_free($this->cur);
sqlrcon_free($this->conn); return $res; }


然后在数据层通过这个class来取得数据。例如:

function getOneRecord($sql)
{
if(!$sql) return false;
$sdb = new SQLR_DB();
return $sdb -> getRow($sql);
}
public function getRow($sql)//取一行的共有方法
{
if(!$sql) return false;
$res = $this -> retrieve($sql);
if("null" == $res) return null;
return $res[0];
}
}

现在的问题是,用这种方法来对付所有的读操作是可以的(用来管理所有的对Slave服务器的读操作),但是用来管理写操作的时候却遇到了问题,在向数据库中写入一条记录,并返回插入ID的时候,以前可以使用mysql_insert_id方法,可是用SQL Relay来管理数据库连接之后,每次查询都有可能使用不同的连接,因此执行完一条insert语句,再去执行mysql_insert_id的时候,使用的很可能已经不是之前的连接了,因此也就不能返回正确的结果。不知道有哪位大侠知道解决方法的,请不吝赐教,在下感激不尽:)

Solr

Solr是基于Lucene Java搜索库的企业级全文搜索引擎,目前是apache的一个项目。它的特点包括支持XML/HTTP和JSON API,支持高亮显示,支持缓存机制以及支持分布式等等。
下载和安装自己去看文档好了。我们主要研究一下它的配置和使用以及如何实现对中文分词的支持。
假设你现在已经安装好了一个带有Jetty的Solr服务器,那么它的目录结构应该是这样的:

$mySolr
|– etc
| |– LICENSE.javax.servlet.txt
| |– LICENSE.javax.xml.html
| |– LICENSE.jsse.txt
| |– admin.xml
| |– jetty-jmx.xml
| |– jetty.xml
| `– webdefault.xml
|– ext
| |– ant.jar
| |– commons-el.jar
| |– commons-logging.jar
| |– jasper-compiler.jar
| |– jasper-runtime.jar
| |– mx4j-remote.jar
| |– mx4j-tools.jar
| `– mx4j.jar
|– lib
| |– javax.servlet.jar
| |– jsp
| |– org.mortbay.jetty.jar
| `– org.mortbay.jmx.jar
|– solr
| |– README.txt
| `– conf
| |– admin-extra.html
| |– protwords.txt
| |– schema.xml
| |– scripts.conf
| |– solrconfig.xml
| |– stopwords.txt
| |– synonyms.txt
| `– xslt
| `– example.xsl
|– start.jar
`– webapps
`– mySolr
|– META-INF
| |– LICENSE.txt
| |– MANIFEST.MF
| `– NOTICE.txt
|– WEB-INF
| |– lib
| | |– apache-solr-1.2-dev-incubating.jar
| | |– commons-fileupload-1.1.1.jar
| | |– commons-io-1.2.jar
| | |– lucene-core-nightly.jar
| | |– lucene-highlighter-nightly.jar
| | |– lucene-snowball-nightly.jar
| | `– xpp3-1.1.3.4.O.jar
| `– web.xml
`– index.html

Solr是用xml文件进行配置的,其中最主要的配置文件在solr/conf/下面。

首先是schema.xml,它是用来设置你要加入索引的数据的数据类型的,首先你要定义一个FieldType,包括name,class,positionIncrementGap等等一些参数,name就是这个FieldType的名称,class指向org.apache.solr.analysis包里面对应的class名称,用来定义这个类型的行为。在FieldType定义的时候最重要的就是定义这个类型的数据在建立索引和进行查询的时候要使用的分析器analyzer,包括分词和过滤。你会看到在例子中text这个FieldType在定义的时候,在index的analyzer中使用solr.WhitespaceTokenizerFactory这个分词包,就是空格分词,然后使用solr.StopFilterFactory,solr.WordDelimiterFilterFactory,solr.LowerCaseFilterFactory,solr.EnglishPorterFilterFactory,solr.RemoveDuplicatesTokenFilterFactory这几个过滤器。在向索引库中添加text类型的索引的时候,Solr会首先用空格进行分词,然后把分词结果依次使用指定的过滤器进行过滤,最后剩下的结果才会加入到索引库中以备查询。定义完FieldType以后就是定义具体的字段(类似数据库中的字段),就是filed,filed定义包括name,type(就是之前定义过的各种FieldType),indexed(是否被索引),stored(是否被储存),multiValued(是否有多个值)等等。除此之外,你还可以定义动态字段,所谓动态字段就是不用指定具体的名称,只要定义字段名称的规则,例如定义一个dynamicField,name为*_i,定义它的type为text,那么在使用这个字段的时候,任何以_i结尾的字段都被认为是符合这个定义的,例如name_i,gender_i,school_i等。

还有solrconfig.xml,这个文件用来定义Solr的一些基本特性,最重要的一个就是可以通过更改其中的dataDir属性来指定索引文件的存放位置。

这些都定义好之后,你可以重新启动你的服务器,然后向其中添加数据。

添加数据是通过向服务器的update服务POSTxml格式的数据来实现的,xml结构是这样的add中间有很多个doc,每个doc中有很多个field。添加到索引库中的每条记录都必须指定唯一的数字id来唯一标识这条索引。建立好xml文件(例如a.xml)之后,在exampledocs目录下执行:java -jar post.jar a.xml来添加索引数据。根据实验,每个xml文件最好不要超过30M,不然会添加失败。

添加成功之后就可以通过web服务来进行查询了,查询方法是把“字段名称:要查询的值”这样的字符串传递给服务器,服务器返回xml格式的结果。例如要在一个名为user_name的字段上查询值为”test”的数据,那么传递给服务器的就是”user_name:test”.多个查询之间用”&&”连接。

返回的结果包括查询所用时间,找到的符合条件的记录数,以及根据指定的start和limit确定的部分记录(这个可以用来实现分页)。可以根据自己的需要对结果xml进行解析。查询速度还不错(当然这个和运行环境和网络环境等有关)。在同一个查询进行第二次的时候,Solr会从缓存中取得数据,只要几毫秒。

下面说说索引的删除和更新,更新很容易,只要把新的数据按照对应的唯一id重新添加一次就可以了,删除有几种情况,一是删除指定id的一条记录,那么就建立一个简单的xml片段:《delete》id:21《/delete》,如果删除某一类字段,例如删除所有user_name是john的记录,那么可以建立这样的xml片段:《delete》《query》user_name:john《/query》《/delete》。因此,如果要删除所有的索引的话,你可以把其中的user_name:john替换成*.*。然后把这个xml用添加索引同样的办法POST到服务器就可以了。当然如果想删除所有的索引,你也可以先停止服务,然后删除index目录,然后再重新启动服务:)

好了,现在可以自由的管理索引,也可以方便的进行查询了,接下来说说怎么实现对中文分词的支持。

根据前面说的,分词是在添加索引和进行查询的时候实现的,就是在定义FieldType的时候进行对应分词方法的定义。Solr默认是不支持中文分词的,但是lucene中提供了用来实现中文分词的包,按照Solr的方法包装成ChineseFilterFactory.java和ChineseTokenizerFactory.java两个类,然后在定义FieldType的时候指定使用这两个类来进行分词和过滤就可以了(这里只是思路,具体方法自己琢磨,嘿嘿)。

好了,现在可以用它来代替以前的weblucene了。hoho。

ubuntu下播放mp3

用ubuntu下自带的Rhythmbox播放器播放mp3文件的时候报错,说找不到GStreamer解码器,你要这么做:

sudo apt-get install gstreamer0.10-pitfdll gstreamer0.10-ffmpeg gstreamer0.10-plugins-bad gstreamer0.10-plugins-bad-multiverse gstreamer0.10-plugins-ugly gstreamer0.10-plugins-ugly-multiverse gxine libxine-main1 libxine-extracodecs ogle ogle-gui

那么就可以了。

如果还不行,去找爱因斯坦吧。我就知道这么多。

基于鼠标点击分析用户行为

你想知道你的网站设计是否合理吗?你确定你的那些按钮和链接是放在该放的位置了吗?你想知道用户到底对页面上的哪些地方感兴趣吗?
没有人能猜出来用户到底喜欢什么,那些“我以为”“我觉得”云云都是扯淡。你有数据吗?
前一阵看到车东的《基于鼠标点击跟踪的用户点击行为分析》,觉得是个很不错的主意,可以跟踪点击行为来分析用户在自己网站上的行为特点,给设计部门一个直观的科学的参考,进而进行有针对性的改进。自己试验了一下,效果非常very相当的goood。
以下是我试验的具体过程:
首先在需要跟踪用户点击行为的页面加入下面的javascript脚本(注意,下面的脚本是在车东的脚本基础上做的改进,使之兼容Firefox和IE):
<script language="javascript">
function clickStat(evt) {
//下面的try-catch语句兼容不同浏览器下创建element的方法;
try{
var image = document.createElement("<img></img>");
}catch(err){
var image = document.createElement('img');
}

//这里不用document.body.scrollLeft方法来获得滚动条的值,因为在w3c标准的网页中返回值始终为0;
var tempX = evt.clientX + document.documentElement.scrollLeft;
var tempY = evt.clientY + document.documentElement.scrollTop;

image.src = "/click_stats.php?width=" + screen.width + "&x=" + tempX + "&y=" + tempY;
image.height = 0;
image.width = 0;

//下面的try-catch语句屏蔽Firefox下的报错信息;
try{
document.body.insertBefore(image);
}catch(err){}

return true;
}
</script>

然后在<body>标签中加入onmousedown=”clickStat(event);”方法。这样做也是为了浏览器兼容(tmd,这些烂标准啥时候能统一)。
在服务端用click_stats.php来接收传递过来的参数,并记录到指定的文件中。如果对多个页面进行统计,还可以在参数中增加页面信息,在服务器端根据页面参数把数据保存到不同的文件中。
接收到的参数包括客户端的屏幕分辨率和点击时的具体坐标,可以根据自己的需要对采集到的数据进行分析。例如,可以确定页面上每个按钮或链接的具体坐标范围,来统计每个按钮或链接的点击次数以及占所有点击次数的百分比。
为了更加直观,我把完整的页面截了一个图作为背景,然后用gd在图上把采集到的数据画成一个个的点,这样就可以清晰的看到页面的点击情况,被点击次数多的按钮或链接,点分布的非常密集,反之就非常稀疏,或者根本没有。直观的反映了受欢迎的程度。
还有需要注意的是,在进行分析的时候一定要考虑到分辨率的问题,例如如果用来画点的背景截图是1024的,而客户端的分辨率是1280的,在画这个坐标点的时候就要在横坐标上减去128。
看到统计结果,你就会发现很多地方和你想象的很不一样。没有为什么,真实的数据才能真实的反映用户的行为,而我们作为网站的设计和实现者,完全不能代表普通用户,往往一厢情愿的得到错误的结论。

谨慎使用php的ip2long函数

把ip数据保存在数据库(MySQL)中时候,我们习惯用ip2long函数生成整型,然后存放在一个int(11)类型的字段中,但是,在不同的系统平台上,ip2long函数得到的值是不同的,因此可能造成在从数据库中读出数据,用long2ip得到ip的时候产生错误,说一下我们碰到的情况:
我们用一个int(11)类型(范围-2147483648 - 2147483647)来保存把一个ip地址用ip2long处理得到的结果,例如ip是’202.105.77.179′,那么在32位机器上得到的结果是:-899068493,而在64位机器上却得到3395898803.然后把它写入数据库,由于超过int(11)的范围,因此64位机器上的结果被保存为int(11)的最大值:2147483647.于是在从数据库中取出的时候,便得到了错误的结果,会得到”127.255.255.255″这个ip地址.
解决的办法很多,比如可以用mysql的函数:INET_ATON和INET_NTOA来处理ip地址;或者把保存ip地址的字段改为bigint类型,这样在64位机器上虽然保存的是3395898803,使用long2ip函数仍能得到正确的结果.
仅供参考.

MySQL的新引擎

叫做Falcon,猎鹰的意思,名字够酷.
昨天看到这个消息的时候还着实使劲的期待了一下,不过看到了Peter Zaitsev的一篇文章,一下子就浇灭了我那可怜的盲目热情.这篇文章是对InnoDB,MyISAM和Falcon三种引擎性能的一个对比.从对比结果可以看出,Falcon和社会主义一样,还处于初级阶段.除了在READ_KEY_POINT这一项上有微弱的优势以外,几乎都严重的处于下风.不过另我欣慰的是,InnoDB的表现基本都要好于MyISAM,看来InnoDB确实是个不错的选择,除了支持事务和外键等特性,查询性能也至少不输给MyISAM,可以放心大胆的使用.

PS:今天看到Firefox有了3.0测试版本,虽然激动,还是忍住了没试用,怕的是用不了那些可爱的插件.如果没有插件,我宁可用IE了.

MySQL4的一个小bug

今天习惯的在命令行下输入”\s”查看数据库状态的时候,发现”Queries per second avg”这个值由原来的5k左右变成了只有几百,吓了一跳,以为数据库出了啥问题,就算优化的再好,也不可能下降到如此地步啊,可是查看前端服务和数据库都没有异常,难道MySQL计算这个数的时候出了问题?
于是查看MySQL的源代码,发现这个值是用总的Questions除以Uptime,就是用总的查询数除以服务器运行的时间,在源代码中,Questions被定义为无符号的长整型(ulong,范围是0~4294976295).所以当Questions超过4294976295时,就会重新回0,把当前的Questions数量加上4294976295,再除以Uptime,就会得到正确的”Queries per second avg”.
当然了,如果你看着很不爽,你可以重启MySQL来搞定它,hoho~~

PS:这个小bug在MySQL5中已经被修正
http://lists.mysql.com/commits/6833

已知的InnoDB与MyISAM表之间的差别

InnoDB和MyISAM是在使用MySQL最常用的两个表类型,各有优缺点,视具体应用而定。下面是已知的两者之间的差别,仅供参考。
1.InnoDB不支持FULLTEXT类型的索引。
2.InnoDB中不保存表的具体行数,也就是说,执行select count(*) from table时,InnoDB要扫描一遍整个表来计算有多少行,但是MyISAM只要简单的读出保存好的行数即可。注意的是,当count(*)语句包含where条件时,两种表的操作是一样的。
3.对于AUTO_INCREMENT类型的字段,InnoDB中必须包含只有该字段的索引,但是在MyISAM表中,可以和其他字段一起建立联合索引。
4.DELETE FROM table时,InnoDB不会重新建立表,而是一行一行的删除。
5.LOAD TABLE FROM MASTER操作对InnoDB是不起作用的,解决方法是首先把InnoDB表改成MyISAM表,导入数据后再改成InnoDB表,但是对于使用的额外的InnoDB特性(例如外键)的表不适用。

另外,InnoDB表的行锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表,例如update table set num=1 where name like “%aaa%”

任何一种表都不是万能的,只用恰当的针对业务类型来选择合适的表类型,才能最大的发挥MySQL的性能优势。

从MySQL4到MySQL5

新版本总是意味着新的feature,但同时也意味着新的venture.
我们目前的数据库架构为读写分开的Master(1)+Slave(2),最近希望把版本由目前的4.1.18升级到5.0,为了稳妥起见,首先把其中一台Slave进行了升级,不久,问题就出现了,很多有unsigned属性的字段的值在Master和Slave上出现了不同。比如smallint(5) unsigned类型,Master上是0,Slave上却是65535。仔细一看,65535原来就是无符号类型的-1。测试一下,在Master上建立一个smallint(5) unsigned类型的字段,初始值为0,对它进行update操作,update test set test_unsigned=test_unsigned-1 where id=1;在Master(4.1.18)上,得到的值仍然为0,同步到Slave(5.0)上以后,值果然变成了65535。faint!
Google之,发现在MySQL5的manual中,详细的描述了MySQL4.1和MySQL5的区别,看来想全面升级到MySQL5,在操作unsigned类型字段时要十分小心,或者提前判断是否会产生小于0的值,或者使用类似下面的语句:UPDATE t SET i = CAST(i - 1 AS SIGNED);否则结果可能不是你想要的。
另我非常faint的是,在MySQL手册关于Master-Slave类型的replication的描述中,建议版本相同或者Master的版本低于Slave的版本,可是在上面的例子中,这明显有问题,或者怪我们程序判断的不严格?

给有重复记录的表添加唯一索引

不希望数据表中有重复记录的时候我们可以给表添加一个联合唯一索引
例如,user表中有user_id,user_name两个字段,如果不希望有两条一摸一样的的user_id和user_name,我们可以给user表添加两个字段的联合唯一索引:
alter table user add unique index(user_id,user_name);
这样当向表中添加相同记录的时候,会返回1062的添加失败信息。
但是有一种情况是表中已经有n个重复的记录,这时候我们才想起来要添加唯一索引,再执行上面的操作时,数据库会告诉你已经有重复的记录了,建立索引失败,这时候,我们可以用下面的操作:
alter ignore table user add unique index(user_id,user_name);
它会删除重复的记录(别怕,会保留一条),然后建立唯一索引,高效而且人性化。