项目组里一直没有使用Redis,经过对Redis的简单了解,我就觉得这么神奇好用的玩意儿,不去用它真的是与IT社会脱轨。再者,看看外面的PHP面试,对Redis、Memcached的要求可不仅仅是说会用就行了,要掌握原理、数据结构、两者不同应用场景,特别是高流量下的优化处理。
聊聊自己曾经开发的小功能,现在回想,用了Redis,不管是效率还是代码的简洁度,都会有个大提升。
① 同步返回结果,异步请求第三方
图1
图2
需求是:APP发起请求,由于第三方通道的同步返回特别慢,避免用户等待时间过久,所以同步返回APP请求已提交的信息,通过另外的页面记录告知此次请求的结果。异步请求第三方通道,判断同步结果,若成功继续进行查询,查询结果可能会是等待,需要一直查询直到结果为成功或失败,结果都正常则更新A表的指定字段。
在初步请求成功的前提下,第三方会有个异步回调,这里当时有点疑问,既然有回调,为什么要搞那么麻烦前面还要查询,后来发现因为要更新失败的情况啊~接着继续调另外的第三方接口,同样判断同步结果,再进行查询,一直查一直查。
听Java的开发同事说,他是用死循环一直查询,直到有结果。当然我不知道Java的内部机制是怎么样的,如果PHP死循环查询,要是对方服务器出问题了,刚好这时候用户并发请求也多,这不是要让服务器崩溃嘛,我们可不能因为别人的问题搞蹦自己呢,后来就设置查个20次好了。
那么在这个初步请求过程里,异步逻辑必不可少,由于另外一套代码是Java开发的,内部使用一个子线程非常方便。而PHP不管使用哪种运行模式,感觉都没有有效的方法。
查找相关资料,发现CURL拓展可以通过HTTP请求的方式,实现另类的伪异步。设置CURLOPT_TIMEOUT这个参数可以让请求脚本忽略等待。这存在不少问题,比如增加一个HTTP请求,相当于用户请求一次占用了两次请求的资源,要是并发大,直接把性能砍了一半;而且必须最小等待1秒钟,既然想用异步,这1秒的等待是毫无意义的;虽然说服务器HTTP请求本身出错几率极低,同样的在并发大的情况下,要是运行出错,没有任何补救机制。
另外考虑过,使用crontab命令定时执行PHP脚本,查询表内是否存在需要处理的数据。但是要查询的是流水表,数据量相当大,也要考虑定时器时间和请求未执行完成,下个请求冲突等等情况。
后来慢慢了解Redis之后,使用list结构,可以比较方便的解决这个问题。在每次APP请求时,将某个指定字段值添加在尾部,取出数据时先取头部,类似队列的方式,先进先出。创建一个或多个PHP的常驻脚本(这也是个学问),用while(true)走循环,内部使用BLPOP方法取LIST的元素,保证了没有元素的时候,能够堵塞不至于占用太多系统资源。
② 使用Redis生成唯一字段和统计数据
前段时间公司有个外包项目,刚开始多个人一起参与,这种情况下自己写代码,相对要参考他人的想法,后来在比较赶巧的情况下这项目派我去出差了,在后续的新需求和改bug过程中,也就慢慢转移到我一个人这。
在出差途中,甲方对API接口进行压测检验,在TPS仅仅20-30笔/秒的情况下,订单号出现重复。其实这个问题我有担心过,因为各种随机数也好,代码实现的唯一字符串也好,大部分是基于某个时间因子来生成,那么重复是非常可能的事情。
最之前订单号有用时间戳+rand随机数的方式,可能一直没出过问题,也就没有重视,有次就发现由于相同的订单号,导致状态被异常修改,这里说下rand的规则也是用时间作为随机因子来生成随机数。后来建议了使用用户某个唯一性的字段切割一部分填充订单号。
在新的项目中,考虑过这问题,使用了如下代码:
public static function getOrderNo(){
$orderNo = date('YmdHis') . substr(implode(NULL, array_map('ord',str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
$tot = 9;
for ($i=1 ;$i<=5; $i++) {
srand(self::getMicroseconds());
$orderNo = substr_replace($orderNo, rand(0, $tot--), -$i, 1);
}
return $orderNo;
}
//获取毫秒数
public static function getMicroseconds() {
$utimestamp = sprintf("%.10f", microtime(true)); // 带微秒的时间戳
$timestamp = floor($utimestamp); // 时间戳
$microseconds = round(($utimestamp - $timestamp) * 10000000000); // 微秒
return $microseconds;
}
第一句代码参考了网上的文章,本质在于uniqid()方法。
uniqid():基于以微秒计的当前时间,生成一个唯一的 ID。
当时也有点怀疑,就像刚刚说的,只要是基于时间生成,即时是微妙,都会有几率重复,只是这个几率被逐步的降低了。然后本地测试简单测试了下,还真发现就重复了,几率并没有预想的低,于是参考了另外非后端同事意见,对单个字符用随机数去做替换,每次rand前新定义随机因子,微妙的多位数,这几率总该低了吧。
解释了这么多,其实没啥用,因为压测一进行就重复了。寻找解决的方法倒不复杂,网上查查,他人意见参考,那就上Redis呗。往常由于自己对新技术的深层次原理、优化了解不深等其他原因,不一定有机会使用很多东西,而这次嘛,业务需求给予机会,也推动我进步。
过程其实就很简单,光使用key、value方式就好,设置一个自增序列,每次使用incr方法,获取自增后的值,同时设置自定义的到期时间和到达指定值的初始化,只要每次请求能够得到一个唯一的值,那么订单号什么的都是小意思。在我认识中,redis是单进程的,并发应该不会产生重复,(瞎猜的,等待后续研究)。根据结果得到结论,到目前为止,订单号没有任何一次重复了,如果到了某天并发量增了一个数量级发现重复,那我应该也能基于Redis或其他技术想出新办法了。
在其他的请求中,要统计不少的数据结果做判断。有个主要功能业务,接口每次请求,要查询流水表来统计某个数据做限制,这咋说呢,肯定不靠谱了。后来兜兜转转,又加上点新需求,我说既然都用了Redis,全部让棒棒的PHP处理了吧~
总结一下来说呢,Redis的初步使用,并发情况下的唯一性很好,key到期时间失效很好,数据高性能存储和获取很好。另外作为初学者其实要担心的问题不多,在流量慢慢增涨的情况下,可能要开始考虑高可用、资源占用等问题,在这个从低到高过程中,要有自信自己能够掌握会Redis深层次的知识。
后续担心PHP连接会不会太多,占用太多进程数;pconnect和connect又有啥区别等等,这又是另外的学习故事了。