缓存穿透、并发和失效、同步中断,最佳实践及优化方案

原文摘自:

缓存穿透、并发和失效,来自一线架构师的解决方案
https://community.qingcloud.com/topic/463

在我们的实践中,原文中有部分解决方案已经过时,在原文的基础上,添加了几个我们常用的方案。
by shuhai, admin@4wei.cn


我们在用缓存的时候,不管是Redis或者Memcached,基本上会通用遇到以下三个问题:

  • 缓存穿透
  • 缓存并发
  • 缓存失效
  • 同步、复制中断

##缓存穿透

![https://pek3a.qingstor.com/community/resource/pic/2016-6-23-cache-1.png][1]
![https://pek3a.qingstor.com/community/resource/pic/2016-6-23-cache-2.png][2]
![https://pek3a.qingstor.com/community/resource/pic/2016-6-23-cache-3.png][3]

注:上面三个图会有什么问题呢?

我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。

这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就挂掉了。

那这种问题有什么好办法解决呢?

要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。有一个比较巧妙的作法是,可以将这个不存在的key预先设定一个值。比如,"key" , “&&”。

在返回这个&&值的时候,我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待继续访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是&&,则可以认为这时候key有值了,从而避免了透传到数据库,从而把大量的类似请求挡在了缓存之中。

你应该注意,这里缓存未命中的原因,更值得我们关注。

当缓存空间满了,同步失败,网络阻塞,缓存写失败等原因,会出现缓存服务器上并没有这个key。
或者因为同步中断,在主从架构中,写到主却未同步到从的悲剧,就会出现请求穿透到DB层的情况。

出现这样的情况,一定不能直接将请求穿透到DB层,避免DB当机影响其它业务。
我们的解决方案可以参考。

  • 当业务中请求量特别高,缓存未命中的情况,应该在建立DB保护的基础上,放弃一定比例的请求,直接返回空
  • 可以随机释放一些请求到DB,控制好流量的话,能保证缓存重建且DB不受极端压力
  • 后端异步定时检查缓存,主动建立这些缓存
  • 通过建立二级缓存,把之前成功获取的缓存数据放到本机缓存,文件也好,共享内存也好,接受一些过期数据

##缓存并发

有时候如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题。
我现在的想法是对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。

这种情况和刚才说的预先设定值问题有些类似,只不过利用锁的方式,会造成部分请求等待。

##缓存失效

引起这个问题的主要原因还是高并发的时候,平时我们设定一个缓存的过期时间时,可能有一些会设置1分钟啊,5分钟这些,并发很高时可能会出在某一个时间同时生成了很多的缓存,并且过期时间都一样,这个时候就可能引发一当过期时间到后,这些缓存同时失效,请求全部转发到DB,DB可能会压力过重。

那如何解决这些问题呢?

其中的一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

我们讨论的第二个问题时针对同一个缓存,第三个问题时针对很多缓存。

接下来我们将发表一些自己的缓存高可用实践,如《基于云平台的缓存集群高可用实践》,欢迎关注。

总结
1、缓存穿透:查询一个必然不存在的数据。比如文章表,查询一个不存在的id,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成影响。
2、缓存失效:如果缓存集中在一段时间内失效,DB的压力凸显。这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。

当发生大量的缓存穿透,例如对某个失效的缓存的大并发访问就造成了缓存雪崩。

精彩问答
问题:如何解决DB和缓存一致性问题?

当修改了数据库后,有没有及时修改缓存。这种问题,以前有过实践,修改数据库成功,而修改缓存失败的情况,最主要就是缓存服务器挂了。而因为网络问题引起的没有及时更新,可以通过重试机制来解决。而缓存服务器挂了,请求首先自然也就无法到达,从而直接访问到数据库。那么我们在修改数据库后,无法修改缓存,这时候可以将这条数据放到数据库中,同时启动一个异步任务定时去检测缓存服务器是否连接成功,一旦连接成功则从数据库中按顺序取出修改数据,依次进行缓存最新值的修改。

问题:问下缓存穿透那块!例如,一个用户查询文章,通过ID查询,按照之前说的,是将缓存的KEY预先设置一个值,,如果通过ID插过来,发现是预先设定的一个值,比如说是“&&”,那之后的继续等待访问是什么意思,这个ID什么时候会真正被附上用户所需要的值呢?

我刚说的主要是咱们常用的后面配置,前台获取的场景。前台无法获取相应的key,则等待,或者放弃。当在后台配置界面上配置了相关key和value之后,那么以前的key &&也自然会被替换掉。你说的那种情况,自然也应该会有一个进程会在某一个时刻,在缓存中设置这个ID,再有新的请求到达的时候,就会获取到最新的ID和value。

问题:其实用Redis的话,那天看到一个不错的例子,双key,有一个当时生成的一个附属key来标识数据修改到期时间,然后快到的时候去重新加载数据,如果觉得key多可以把结束时间放到主key中,附属key起到锁的功能。

这种方案,之前我们实践过。这种方案会产生双份数据,而且需要同时控制附属key与key之间的关系,操作上有一定复杂度。

问题:多级缓存是什么概念呢?

多级缓存就像我今天之前给大家发的文章里面提到了,将Ehcache与Redis做二级缓存,就像我之前写的文章 http://www.jianshu.com/p/2cd6ad416a5a 提到过的。但同样会存在一致性问题,如果我们需要强一致性的话,缓存与数据库同步是会存在时间差的,所以我们在具体开发的过程中,一定要根据场景来具体分析,二级缓存更多的解决是,缓存穿透与程序的健壮性,当集中式缓存出现问题的时候,我们的应用能够继续运行。

php版本的文件夹合并

#!/bin/env php
<?php
error_reporting(7);

$dir1 = $argv[1];
$dir2 = $argv[2];

if(empty($dir1) || empty($dir2))
{
    echo "php {$argv[0]} dir_merged dir_merge_save\n";
    exit(0);
}

if(! is_dir($dir1))
{
    echo "{$dir1} is not a dir\n";
    exit(0);
}
if(! is_dir($dir2))
{
    echo "{$dir2} is not a dir\n";
    exit(0);
}

merge_dir($dir1, $dir2);

function merge_dir($from, $to)
{
    $from  = rtrim($from, "/")."/";
    $to    = rtrim($to, "/")."/";
    echo "search file from {$from}";
    $files = glob($from."*");
    echo "\t\tdone\n";

    foreach($files as $f)
    {
        $relative_path = substr_replace($f, "", 0, strlen($from));
        $ff = $to . $relative_path;

        //目录不存在则直接复制目录
        if(is_dir($f))
        {
            if(!is_dir($ff))
            {
                $cmd = "cp -r '{$f}' '{$ff}'";
                `$cmd`;
                logs($relative_path);
            } else {
                merge_dir($f, $ff);
            }
        }else{
            if(!is_file($ff))
            {
                copy($f, $ff);
                logs($relative_path);
                continue;
            }

            //对特殊文件进行检查
            $ext = substr($relative_path, strrpos($relative_path, ".")+1);
            if($ext == "apk" && filesize($f) != filesize($ff))
            {
                $compare = apk_version_compare($f, $ff);
                if($compare)
                {
                    copy($f, $ff);
                    logs($relative_path);
                }
            }
        }
    }
}

function apk_version_compare($apk1, $apk2)
{
    return aapt($apk1) > aapt($apk2);
}

function aapt($apk)
{
    $apk = realpath($apk);
    $aapt = "aapt d badging {$apk} | grep versionCode | awk '{print $3}'";
    $aapt = `$aapt`;

    return substr($aapt, 13, -2);
}

function logs($file)
{
    printf("%s\n", $file);
}

基于CloudFlare + Conoha搭建的企业级廉价CDN

中国的流量很贵,流量越大越贵。

目前我负责的业务一半国内,一半海外。目前日均请求过千万,流量过10T,总成本不超过500元/天。跟传统的CDN服务相比,便宜到可以忽略。

CDN介绍
在项目早期,为了业务简单,直接使用传统CDN提供商网宿,在CDN宽带超过 5G 的时候,CDN的成本开始显现出来。
一般国内的CDN(网宿、帝联、蓝汛,还有一些如七牛、又拍这样的二手贩子)均有宽带和流量计费,新起的云平台(阿里、腾讯、uCloud)基本上使用流量计费。

国内的CDN价格,网宿的95峰值计费,一般价格在45-55元/M,根据宽带高值、公司背景、销售人员关系等等因素,合同的价格可以签到30-40元左右,其它二线的CDN可以签到25-30元左右。

其它的云服务平台,价格相对固定,业务购买一般是自助服务,价格基本上没得可谈,普遍价格在0.9元-1.2元/G。

国内服务商运营的国外CDN价格比国内高出几个数量级,网宿的报价为150元/M,其海外节点为少数自建加akamai代理。其它几家传统的CDN服务商完全代理甚至没有海外业务。

海外的CDN服务商,计费一般是按流量计费,常见的如MaxCDN, KeyCDN,价格一般为0.04-0.1美元/G。

业务介绍
我们的业务早期使用网宿,业务从0M跑到3G,业务一直比较稳定。后来业务推到海外,直到CDN的成本越来越高,CDN成本差不多占了利润的一半。

网宿现在有市场优势,价格一直砍不下来。经过跟几家CDN服务咨询、测试,从最初的七牛,MaxCDN,KeyCDN,到最后的CloudFlare+Conoha,终于实现成本与速度的平衡。

在第一次使用CloudFlare和Conoha的时候,都被其极低的价格吓得不敢相信,企业级项目中总觉得低价的服务肯定会出问题(事实上确实是这样,毕竟价格高的也会出问题)。


cloudflare.com

和国内的安全宝、百度云加速的业务类似,CloudFlare提供的安全服务是帮助网站阻止来自网络的黑客攻击、垃圾邮件等,并提升网页的浏览速度,这和一般的安全软件往往会影响网页的运行速度大相径庭。目前CloudFlare在全球拥有23个数据中心,如果用户使用了其服务,那么网络流量将通过CloudFlare的全球网络智能路由。CloudFlare会自动优化用户的网页交付,以期达到最快的页面加载时间以及最佳性能。 CloudFlare提供包括CDN、优化工具、安全、分析以及应用等服务。

2015年9月,CloudFlare正式宣布与百度合作改善外国网站在中国的可访问性。双方早在去年7月就签署了合作协议,CloudFlare将其技术转让给百度(CEO称此举是为了增加信任),而采用CloudFlare技术的百度云加速服务于去年12月开始运作。

CloudFlare称,百度在中国大陆的17个中心地区节点与 CloudFlare 全球的45个节点结合起来,提升中国国内外的访问体验,当客户激活中国网络服务后,他们的中国访问者将会访问百度节点,而CloudFlare节点则继续服务海外访问者,可将中国流量的响应时间缩短超过200毫秒。
但外国客户如果要激活中国网络服务将需要ICP备案。网站备案是中国工信部要求所有在中国大陆使用主机或CDN服务的许可证书。

via:http://www.cnbeta.com/articles/429815.htm

conoha.jp

ConoHa日本gmo.jp旗下的一个VPS主机商,成立于2014年。ConoHa提供日本、新加坡及美国机房云VPS服务。

ConoHa的业务跟国内的云服务平台类似。说是云服务器,个人感觉跟vps差不多,跟常见的Linode和digitalocean基本上相同。
ConoHa的官网支持中文,支持信用卡和支付宝,支持扩容支持按时间收费,可以按小时按月计费。

ConoHa的价格非常廉价,常见的配置如1GB、2Core、SSD50GB,每个月50元人民币。

最重要的是,ConoHa不限流量,100M宽带,可扩容,可加IP。

ConoHa有激励政策,通过我的邀请链接注册,你可以获取1000日元,相当于免费赠送一台主机,
https://www.conoha.jp/referral/?token=V3xoVa5812CYk15rhJkKiiNc5E340f3uNaNjQCiaBmnWZdA30Zk-0VJ

业务实现
在CloudFlare中,添加一个免费(我们使用付费方案是200美元/月)的域名,主要使用其提供的IP隐藏、文件缓存来实现防DDOS和CDN需求。

在我们的单个项目中,CloudFlare每天可以缓存数百万次的请求,差不多8-12TB左右的流量。按最低4美分的价格,每天的CDN成本应该在3000元左右,一个月10万元,一年超过100百万元。
采用了CloudFlare以后,只需要1200元一个月,一年只需要1万元,CloudFlare实实再再的帮老板省了一大笔钱。
老板是不是该奖励我一台特斯拉了哎?

QQ20160110-1

经过我们跟数家CDN服务商的对比和测速,给CloudFlare的节点和速度5个星,稳定性和速度让我们非常意外,点赞。
但作为一个非专业的CDN解决方案,CloudFlare目前还没有完善的数据报表。不能分析请求的URL列表,不能按常见维度分析用户和行为,对运营人员来讲是个缺憾。
同时,CloudFlare默认只对一些基本的文本、图片文件进行缓存,有特殊的文件,比如apk、exe等文件,则颇费周折。

via:https://support.cloudflare.com/hc/en-us/articles/200172516

CloudFlare中非默认缓存的文件,比如apk文件,如果需要缓存,则不能携带任何参数,否则会回源,无法缓存。
CloudFlare另外一个巨坑便是其缓存重建只能通过pull方式。回源pull方式存在严重的风险。我们这边出现过一个50M的文件清理缓存以后,源站瞬间出现近千个请求。这包括CloudFlare的全球节点的缓存请求,也有用户的的真实请求。
CloudFlare无法设置限速,所有请求全落到源站,源站的流量瞬间飙上500M。放在云上的整个路由器下的所有业务全部卡死。

源站的流量可以通过扩容的方式提升上限,为了解决巨大的回源请求,我们曾差不多把宽带提高了到1G。
云平台1.2元/G的流量价格也是贵到滴血,逼得我们必须要为回源的请求再构建一个廉价的中转服务器。

在测试过Linode和digitalocean以后,我们选中了ConoHa。主要看中其极低的价格、无限流量、多节点等优势。
我们把文件rsync到ConoHa的多台主机以后,再将CloudFlare的回源请求重定向到不同的ConoHa服务器。
虽然业务架构越来越复杂,但比起源站业务被中断,以及宽带扩容的成本,ConoHa上一个月不到一千块钱的成本实再便宜得让人心疼。

其它
在目前的架构中,所采用的方案均为比较新的服务商,稳定性存在非常大的风险,如果有一天CloudFlare或者ConoHa倒闭,则会对现在的业务造成致命的影响。

为了降低风险,我们也在采用了一些优化手段,也在积极寻找备用方案。
比如,为了避免单个域名流量太大,被CloudFlare封掉,或者要求我们使用更高的付费方案,我们把CDN拆到多个域名下,减少单个域名的请求和流量。我们也在寻找与CloudFlare相同的的平台,比如Incapsula。甚至有人建议直接购买廉价vps自建CDN。考虑到现在云计算基础服务相对完善,自建一个全球性的CDN平台也不是太大难事。

为了避免ConoHa出故障,我们也在其它几个廉价的vps服务商购买了几个备用服务器,定期将文件同步过去进行灾备。

同时,我们发现CloudFlare中添加一个在国内备案的域名,其节点尚不能确认是否已经使用了百度云加速的节点,有没有知晓内幕的朋友。

最后
经过项目的实践和数月的运营,这个廉价的CDN实现,节点数可以达到成熟商用CDN节点数的30%-80%(海外优势尤其明显)。跟同行业的一些朋友交流时发现,也有其它同行业的业务使用CloudFlare+Digitalocean实现,稳定支撑月流水数百万美元的业务,而其成本非常低廉。

云服务越来越成熟,成本越来越低廉,对于我们这样的创业公司来讲,是一大福音。而对于那些传统的基础服务商来讲,又是什么呢?

简单获取并验证client ip

function get_ip_address()
{
    foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR') as $key)
    {
        if (array_key_exists($key, $_SERVER) === true)
        {
            foreach (explode(',', $_SERVER[$key]) as $ip)
            {
                $ip = trim($ip);

                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false)
                {
                    return $ip;
                }
            }
        }
    }
}

Tail for php,php实现tail命令

监控文件变化,可以通过tail,inotify来实现,也可以通过awk转发变化的内容到外部命令中。

下面是通过popen来调用系统命令,性能内存开销相对比较小。

```php
$handle = popen("tail -f /var/log/your_file.log 2>&1", 'r');
while(!feof($handle)) {
$buffer = fgets($handle);
echo "$buffer\n";
flush();
}
pclose($handle);
```

使用 unoconv 将 doc, xls, ppt 转换成 pdf

安装 openoffice 依赖

yum install unoconv openoffice.org-core openoffice.org-headless openoffice.org-pyuno openoffice.org-sdk openoffice.org-writer openoffice.org-calc openoffice.org-draw libreoffice-langpack-zh-Hans.x86_64 -y

yum install font-chinese cabextract rpm-build -y
fc-cache -f -v

unoconv -f pdf test.docx

通过word模板实现pdf内容替换

#解压word文件
unzip test.docx -d tmp

#替换word模板
sed -i 's/{__TITLE__}/title/g' tmp/word/document.xml

#重新打包word文件
cd tmp && zip -rq ../new.docx \[Content_Types\].xml customXml/ docProps/ _rels/ word/
cd ../ && rm -rf tmp
unoconv -f pdf new.docx

草根站长创业的时代落幕

Via:http://www.zhihu.com/question/19728740

2015年4月,终于要跟所有朋友说再见了,感谢这些年支持过我的朋友,各位站长,感觉火车采集器所有的朋友。

2005年来北京,2006年开始学习Asp、PHP,到后面的各种论坛、CMS接口,到今年算起来也接近10年的时间了。这些年一直保持着这些接口的开发和升级,也努力让接口尽量简单易用,功能强大。通过这些产品,我看到过太多草根站长的成长、转变,也交到了很多朋友,天南海北,世界各地,你们让我知道,拼搏和精彩。

最近几年来,个人站长越来越少,我也从之前的新东方,到极客学院,再到现在的创业。时间、精力、用户,都无法再保证这些产品的更新和维护。现在国内的CMS、DX、PW都陷入了迷茫和停滞,越来越多的站长慢慢转型,这些接口的历史使命已经完成。

未来,这些接口都不再继续提供更新,也不提供下载,考虑到已购买用户的权益和安全,这些接口将不再出售,也不会将源码进行公开。

落幕不是结束,而是新的再见,感谢所有人。

php trait 简明教程,多重继承的实现方案

http://php.net/manual/en/language.oop5.traits.php

自 PHP 5.4.0 起,PHP 实现了代码复用的一个方法,称为 traits。

Traits 是一种为类似 PHP 的单继承语言而准备的代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用方法集。Traits 和类组合的语义是定义了一种方式来减少复杂性,避免传统多继承和混入类(Mixin)相关的典型问题。

Trait 和一个类相似,但仅仅旨在用细粒度和一致的方式来组合功能。Trait 不能通过它自身来实例化。它为传统继承增加了水平特性的组合;也就是说,应用类的成员不需要继承。

在我理解说白了就是在继承类链中隔离了子类继承父类的某些特性(就是子类“要用父类的特性的时候”,如果trait有,就优先调用trait的方法、属性等)。

<?php
trait MyTrait
{
    protected $var = "MyTrait_var";
    protected $var1 = "MyTrait_var";

    function __construct()
    {
        echo $this->var.PHP_EOL;
    }

    function a()
    {
        echo "a".PHP_EOL;
    }
}

interface MyInterface
{
    function __construct();
    function b();
}

abstract class MyAbstract
{
    protected $var2 = "MyAbstract_var";

    use MyTrait;

    function b()
    {
        echo "b".PHP_EOL;
    }
}

class MyClass extends MyAbstract implements MyInterface
{
    protected $var3 = "MyClass_var";

    //也可以在这里引用,不区分继承关系
    //use MyTrait;
    function c()
    {
        echo "c".PHP_EOL;
    }
}

$class = new MyClass();
$class->a();
$class->b();
$class->c();

输出结果
MyTrait_var
a
b
c

总结:

  • 从本质上说,trait和include文件的概念差不多
  • trait可以更加方便的实现代码复用,因为我们用继承关系实现的无法在父类中访问子类的private属性与方法,而trait就和把代码直接写在对象里效果一样。
  • 使用trait时候应该坚决避免命名冲突,尤其是同时使用多个trait时。
  • 如果产生了命名冲突,如果两者的可见性、初始值、static与否完全相同,则trait中的会覆盖掉对象中的,并抛出E_STRICT错误,否则会抛出E_COMPILE_ERROR错误,终止编译。
  • ThinkPHP下MongoDB的操作方法

    https://github.com/liu21st/thinkphp/commits/master/ThinkPHP/Library/Think/Db/Driver/Mongo.class.php?author=vus520
    向tp添加了一些mongodb操作方法,demo如下

    Model

    <?php
    namespace Home\Model;
    use Think\Model\MongoModel;
    class UserActlogModel extends MongoModel
    {
    	
    }
    

    Controller

    <?php
    namespace Home\Controller;
    use Think\Controller;
    class IndexController extends Controller
    {
    	function index()
    	{
    		echo __CLASS__ .'\\'. __FUNCTION__;
    	}
    
    	function hello()
    	{
    		echo "Hello world";
    	}
    
        public function mongo()
        {
        	$model = D("UserActLog");
    
        	//distinct查询语法,支持where、cache操作链
        	$result = $model->cache(false)->where( array('device.channelid'=>"10017") )->distinct('device.imei', array("day"=>"20140706"));
        	print_r(count($result));
        	print_r($model->_sql());
        	echo PHP_EOL;
    
        	//command查询语法
        	$data = $model->command(array('buildInfo'=>1));
        	//print_r($data);
        	print_r($model->_sql());
        	echo PHP_EOL;
    
        	$data = $model->status();
        	//print_r($data);
        	print_r($model->_sql());
        	echo PHP_EOL;
    
        	//MapReduce查询语法
        	$key    = array('device.channelid'=>1);
        	$init   = array('num'=>0);
        	$reduce = "function(obj, prev){prev.num++;}";
        	$option = array(
        			'table' => 'user_act_log',
        			'condition'=>array("day"=>"20140701"),
        	);
        	$result = $model->cache(true)->where( array("day"=>"20140708") )->group($key, $init, $reduce);
        	print_r(count($result));
        	print_r($model->_sql());
        	echo PHP_EOL;
    
        	//find查询语法,支持where、order、cache、field操作链
        	//field方法支持field("act,group"),field(array("act"=>true, "_id"=>false)),field(array("act", "group"))
        	$result = $model->where(array("day"=>"20140701"))->field(array("act"=>true, "group"=>true, "_id"=>false))->order('_id asc')->find();
        	print_r($result);
        	print_r($model->_sql());
        	echo PHP_EOL;
    
    		//子查询OR逻辑
    		$map['device.channel'] = "obx_03";
    		$map['_logic'] = 'and';
    		$map['_complex'] = array("act"=>"down", "group"=>"download", '_logic'=>"or");
    		$result = $model->field(array("act"=>true, "group"=>true, "_id"=>false))->where($map)->limit(2)->select();
    		print_r($result);
    		print_r($model->_sql());
    		echo PHP_EOL;
    
    		//子查询OR逻辑
    		$map['_complex'] = array('_logic'=>"or", array("act"=>"down"), array("group"=>"download"));
    		$result = $model->where($map)->find();
    		print_r($result);
    		print_r($model->_sql());
    		echo PHP_EOL;
    
    		//子查询OR逻辑
        	$map = array();
        	$map['_complex'] = array('_logic'=>"or", array("act"=>"down"), array("group"=>"download"));
        	$result = $model->field(array("act"=>true, "group"=>true, "_id"=>false))->where($map)->find();
        	print_r($result);
        	print_r($model->_sql());
        	echo PHP_EOL;
    
        	//子查询 AND 逻辑
        	$map = array();
        	$map['_complex'] = array('_logic'=>"and", array("act"=>"down"), array("group"=>"download"));
        	$result = $model->field(array("act"=>true, "group"=>true, "_id"=>true))->where($map)->find();
        	print_r($result);
        	print_r($model->_sql());
        	echo PHP_EOL;
    
        	//子查询 nor 逻辑
        	$map = array();
        	$map['_complex'] = array('_logic'=>"nor", array("act"=>"down"), array("group"=>"download"));
        	$result = $model->field(array("act"=>true, "group"=>true, "_id"=>true))->where($map)->find();
        	print_r($result);
        	print_r($model->_sql());
        	echo PHP_EOL;
    
        	//$not查询
        	$map = array();
        	$map['day'] = '20140710';
        	$map['act'] = array('$exists'=>true, '$not'=>array('$in'=>array("click", "listview")) );
        	$map['_complex'] = array('_logic'=>"nor", array("act"=>"down"), array("group"=>"download"));
        	$result = $model->field(array("act"=>true, "group"=>true, "_id"=>true))->where($map)->find();
        	print_r($result);
        	print_r($model->_sql());
        	echo PHP_EOL;
    
        	//$not查询
        	$map = array();
        	$map['day'] = '20140710';
        	$map['_logic'] = 'or';
        	$map['act'] = array('$exists'=>true, '$not'=>array('$in'=>array("click", "listview")) );
        	$map['_complex'] = array('_logic'=>"nor", array("act"=>"down"), array("group"=>"download"));
        	$result = $model->field(array("act"=>true, "group"=>true, "_id"=>true))->where($map)->find();
        	print_r($result);
        	print_r($model->_sql());
        	echo PHP_EOL;
        }
    }
    

    Yii2.0框架发布了!!!

    我上大学时,2.0在开发。
    我谈恋爱时,2.0在开发。
    我结婚时,2.0在开发。
    我生孩子时,2.0在开发。
    我孩子说Hello world时,2.0发布了。

    我只想说,你来晚了。我已经投入了其它妹子的怀抱。大妹子Phalcon,二妹子laravel,小情人ThinkPHP。

    Best PHP Frameworks for 2014
    http://www.sitepoint.com/best-php-frameworks-2014/

    2014 年 15 款新评定的最佳 PHP 框架
    http://www.oschina.net/news/48982/best-php-frameworks-2014

    世界框架排名,各种语言
    http://www.techempower.com/benchmarks/

    redis vs ssdb, hmset效率对比

    平台环境:
    软件 OS X 10.9.5 (13F34)
    处理器 2.6 GHz Intel Core i5
    内存 8 GB 1600 MHz DDR3
    硬盘 500G SSD

    软件环境:
    redis 2.8.13
    ssdb 1.7.0.0

    测试总结:
    使用php-redis驱动测试php连接redis和ssdb进入hmset持续写入10万个Key。
    redis用时24s,ssdb用时120s,ssdb表现相差近6倍。

    测试结果:
    redis test start
    Job start, 512.000000 kb, 2014-10-11 13:31:59
    Job 10000 done, 512.000000 kb, 2014-10-11 13:32:01
    Job 20000 done, 512.000000 kb, 2014-10-11 13:32:02
    Job 30000 done, 512.000000 kb, 2014-10-11 13:32:05
    Job 40000 done, 512.000000 kb, 2014-10-11 13:32:08
    Job 50000 done, 512.000000 kb, 2014-10-11 13:32:13
    Job 60000 done, 512.000000 kb, 2014-10-11 13:32:15
    Job 70000 done, 512.000000 kb, 2014-10-11 13:32:17
    Job 80000 done, 512.000000 kb, 2014-10-11 13:32:19
    Job 90000 done, 512.000000 kb, 2014-10-11 13:32:21
    Job 100000 done, 512.000000 kb, 2014-10-11 13:32:23
    Job start, 512.000000 kb, 2014-10-11 13:32:23

    ssdb test start
    Job start, 512.000000 kb, 2014-10-11 13:32:23
    Job 10000 done, 512.000000 kb, 2014-10-11 13:32:33
    Job 20000 done, 512.000000 kb, 2014-10-11 13:32:44
    Job 30000 done, 512.000000 kb, 2014-10-11 13:32:57
    Job 40000 done, 512.000000 kb, 2014-10-11 13:33:10
    Job 50000 done, 512.000000 kb, 2014-10-11 13:33:22
    Job 60000 done, 512.000000 kb, 2014-10-11 13:33:35
    Job 70000 done, 512.000000 kb, 2014-10-11 13:33:49
    Job 80000 done, 512.000000 kb, 2014-10-11 13:34:00
    Job 90000 done, 512.000000 kb, 2014-10-11 13:34:12
    Job 100000 done, 512.000000 kb, 2014-10-11 13:34:23
    Job start, 512.000000 kb, 2014-10-11 13:34:23

    测试代码:

    #!/usr/bin/env php
    <?php
    $test = new redis_test();
    $test->totalSize = 100000;
    $test->run();
    
    class redis_test
    {
        public $totalSize=1000000;
    
        function printf()
        {
          $args = func_get_args();
          if(count($args) == 1){
            $msg = $args[0].PHP_EOL;
          }else{
            $args[0] = $args[0].PHP_EOL;
            $msg = call_user_func_array("sprintf", $args);
          }
    
          echo $msg;
        }
    
        function run()
        {
            //测试redis->hmset()
            $this->printf("redis test start");
            $redis = new redis();
            $redis->connect('127.0.0.1', 6379);
            $this->test($redis);
    
            //测试ssdb->hmset()
            $this->printf("ssdb test start");
            $redis = new redis();
            $redis->connect('127.0.0.1', 8888);
            $this->test($redis);
        }
    
        function test($redis)
        {
            //导出一条测试记录
            $row = array (
              'id' => '1',
              'product' => '1',
              'imei' => '000000000000000',
              'model' => 'Galaxy Note 3 - 4.4.2 - API 19 - 1080x1920',
              'vcode' => '6',
              'vcoded' => '11',
              'channel' => '10001',
              'download' => '0',
              'ctime' => '1395992425',
              'year' => '2014',
              'month' => '201403',
              'week' => '201413',
              'day' => '20140328',
              'day1' => '1',
              'day2' => '1',
              'day3' => '1',
              'day4' => '1',
              'day5' => '1',
              'day6' => '1',
              'day7' => '1',
              'day14' => '1',
              'day30' => '0',
              'day60' => '0',
              'vcode_1' => '0',
              'vcode_2' => '0',
              'vcode_3' => '0',
              'vcode_6' => '20140710',
              'vcode_7' => '20140331',
              'vcode_8' => '0',
              'vcode_9' => '20140414',
              'vcode_10' => '0',
              'vcode_11' => '20140710',
              'vcode_12' => '0',
              'vcode_13' => '0',
              'vcode_14' => '0',
              'vcode_15' => '0',
              'vcode_16' => '0',
              'vcode_17' => '0',
              'vcode_18' => '0',
              'vcode_19' => '0',
              'last_act_year' => '2014',
              'last_act_month' => '201407',
              'last_act_week' => '201428',
              'last_act_day' => '20140710',
              'offday' => '77',
              'lose' => '1',
            );
    
            $this->printf("Job start, %s, %s", $this->memory_get_usage(), date("Y-m-d H:i:s"));
            for($i=1; $i<=$this->totalSize; $i++)
            {
                $row["id"]    = $i;
                $row["imei"]  = md5(rand(). microtime(true));
                $row["model"] = rand();
    
                $redis->hmset($row["imei"], $row);
    
                if($i % 10000 === 0)
                    $this->printf("Job %s done, %s, %s", $i, $this->memory_get_usage(), date("Y-m-d H:i:s"));
            }
    
            $this->printf("Job start, %s, %s", $this->memory_get_usage(), date("Y-m-d H:i:s"));
        }
    
        function memory_get_usage()
        {
            $size = memory_get_usage(true);
            $unit=array('b ','kb','mb','gb','tb','pb');
            return sprintf("%02f", @round($size/pow(1024,($i=floor(log($size,1024)))), 2)).' '.$unit[$i];
        }
    }
    

    issue反馈:https://github.com/ideawu/ssdb/issues/448

    PHP非阻塞模式

    让PHP不再阻塞当PHP作为后端处理需要完成一些长时间处理,为了快速响应页面请求,不作结果返回判断的情况下,可以有如下措施:

    一、若你使用的是FastCGI模式,使用fastcgi_finish_request()能马上结束会话,但PHP线程继续在跑。

    echo "program start.";
    
    file_put_contents('log.txt','start-time:'.date('Y-m-d H:i:s'), FILE_APPEND);
    fastcgi_finish_request();
    sleep(1);
    echo 'debug...';
    file_put_contents('log.txt', 'start-proceed:'.date('Y-m-d H:i:s'), FILE_APPEND);
    
    sleep(10);
    file_put_contents('log.txt', 'end-time:'.date('Y-m-d H:i:s'), FILE_APPEND);
    

    这个例子输出结果可看到输出program start.后会话就返回了,所以debug那个输出浏览器是接收不到的,而log.txt文件能完整接收到三个完成时间。

    二、使用fsockopen、cUrl的非阻塞模式请求另外的网址

    $fp = fsockopen("www.example.com", 80, $errno, $errstr, 30);
    if (!$fp) die('error fsockopen');
    stream_set_blocking($fp,0);
    $http = "GET /save.php  / HTTP/1.1\r\n";    
    $http .= "Host: www.example.com\r\n";    
    $http .= "Connection: Close\r\n\r\n";
    fwrite($fp,$http);
    fclose($fp);
    

    利用cURL中的curl_multi_*函数发送异步请求

    $cmh = curl_multi_init();
    $ch1 = curl_init();
    curl_setopt($ch1, CURLOPT_URL, "http://localhost:6666/child.php");
    curl_multi_add_handle($cmh, $ch1);
    curl_multi_exec($cmh, $active);
    echo "End\n";
    

    三、使用Gearman、Swoole扩展
    Gearman是一个具有php扩展的分布式异步处理框架,能处理大批量异步任务;
    Swoole最近很火,有很多异步方法,使用简单。(尘缘注:号称重新定义PHP,把NodeJS喷得体无完肤。Swoole工具虽好,却感觉是扩展本身跟NodeJS没可比性)

    四、使用redis等缓存、队列,将数据写入缓存,使用后台计划任务实现数据异步处理。
    这个方法在常见的大流量架构中应该很常见吧

    五、极端的情况下,可以调用系统命令,可以将数据传给后台任务执行,个人感觉不是很高效。

    $cmd = 'nohup php ./processd.php $someVar >/dev/null  &';
    `$cmd`
    

    六、外国佬的大招,没看懂,php原生支持
    http://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html

    七、安装pcntl扩展,使用pcntl_fork生成子进程异步执行任务,个人觉得是最方便的,但也容易出现zombie process。

    if (($pid = pcntl_fork()) == 0) {
        child_func();    //子进程函数,主进程运行
    } else {
        father_func();   //主进程函数
    }
    
    echo "Process " . getmypid() . " get to the end.\n";
    
    function father_func() {
        echo "Father pid is " . getmypid() . "\n";
    }
    
    function child_func() {
        sleep(6);
        echo "Child process exit pid is " . getmypid() . "\n";
        exit(0);
    }
    

    php中的数种依赖注入

    经常看到却一直不甚理解的概念,依赖注入(DI)以及控制器反转(Ioc),找了几篇好的文章,分享一下。 自己理解的,依赖注入就是组件通过构造器,方法或者属性字段来获取相应的依赖对象。

    举个现实生活中的例子来理解, 比如我要一把菜刀 如何获得
    1.可以自己造一把,对应new一个。
    2.可以找生产菜刀的工厂去买一把,对应工厂模式。
    3.可以打电话 让店家送货上门,对应依赖注入。

    再比如我是一个演员,我不可能要求某个导演,我要演某某剧的男一号,相反,导演可以决定让谁来演。而我们的object就是这个演员。

    注入的几个途径:
    1.construct注入

    <?php
    class Book {
       private $db_conn;
    
       public function __construct($db_conn) {
           $this->db_conn = $db_conn;
       }
    }
    

    但是如果依赖过多,那么在构造方法里必然传入多个参数,三个以上就会使代码变的难以阅读。

    2.set注入

    <?php
      $book = new Book();
      $book->setdb($db);
      $book->setprice($price);
      $book->set_author($author);
    ?>
    

    代码很清晰,但是当我们需要注入第四个依赖时,意味着又要增加一行。

    比较好的解决办法是 建立一个class作为所有依赖关系的container,在这个class中可以存放、创建、获取、查找需要的依赖关系

    <?php
    class Ioc {
       protected $db_conn;
       public static function make_book() {
           $new_book = new Book();
           $new_book->set_db(self::$db_conn);
           //...
           //...
           //其他的依赖注入
           return $new_book;
       }
    }
    

    此时,如果获取一个book实例,只需要执行$newone = Ioc::makebook();

    以上是container的一个具体实例,最好还是不要把具体的某个依赖注入写成方法,采用registry注册,get获取比较好。

    <?php
    class Ioc {
    /**
    * @var 注册的依赖数组
    */
    
       protected static $registry = array();
    
       /**
        * 添加一个resolve到registry数组中
        * @param  string $name 依赖标识
        * @param  object $resolve 一个匿名函数用来创建实例
        * @return void
        */
       public static function register($name, Closure $resolve)
       {
          static::$registry[$name] = $resolve;
       }
    
       /**
         * 返回一个实例
         * @param  string $name 依赖的标识
         * @return mixed
         */
       public static function resolve($name)
       {
           if ( static::registered($name) )
           {
              $name = static::$registry[$name];
              return $name();
           }
           throw new Exception('Nothing registered with that name, fool.');
       }
       /**
        * 查询某个依赖实例是否存在
        * @param  string $name id
        * @return bool 
        */
       public static function registered($name)
       {
          return array_key_exists($name, static::$registry);
       }
    }
    

    现在就可以通过如下方式来注册和注入一个依赖

    <?php
    $book = Ioc::registry('book', function(){
    $book = new Book;
    $book->setdb('...');
    $book->setprice('...');
    return $book;
    });
    
    //注入依赖
    $book = Ioc::resolve('book');
    ?>
    

    http://net.tutsplus.com/tutorials/php/dependency-injection-huh

    http://scriptogr.am/mattsah/post/dependencies-in-php

    http://martinfowler.com/articles/injection.html

    http://www.potstuck.com/2009/01/08/php-dependency-injection/

    http://www.potstuck.com/2010/09/09/php-dependency-a-php-dependency-injection-framework/

    http://www.cnblogs.com/Seekr/archive/2012/06/20/2556463.html

    数据统计系统需求整理及实现笔记

    现在的项目,有一个比较精准的数据统计系统的需求,应用场景是统计不同渠道的app带来的下载量,统计app登陆用户的常见行为,如周月活跃比,使用时长等。

    目前团队只有php资源,对Mongo并不熟;
    现在预估的用户量在半年内有30万左右,产生的行为数据大约在2亿条;
    暂不考虑扩展;
    目前使用的Nginx+CDN,通过日志分析写入数据库,忽略CDN日志下载业务;
    有五层用户权限,纬度较多,可以交叉查询父子渠道和不同版本的数据。

    目前我们基于mysql实现了第一版,跑通了整个流程,计划对一些业务实现和底层架构部分进行重构。
    考虑的db有mongodb,mysql myisam, mysql infinidb,mysql infobright,skydb,求有经验的同学分享架构思路。

    PHP使用memcache存储session

    < ?php ini_set('session.save_handler', 'memcache'); ini_set('session.save_path', 'tcp://127.0.0.1:12345?persistent=1&weight=1&timeout=1&retry_interval=15,tcp://127.0.0.2:12345?persistent=1&weight=1&timeout=1&retry_interval=15'); session_start(); ?>

    /* SESSION */
    'SESSION_AUTO_START' =>true,
    'SESSION_OPTIONS' =>array('type'=>'memcache','path'=>'cp://127.0.0.1:12345?persistent=1&weight=1&timeout=1&retry_interval=15,tcp://127.0.0.2:12345?persistent=1&weight=1&timeout=1&retry_interval=15','domain'=>'.a.com'),