PHP+Redis 实例【一】点赞 + 热度 上篇
这次的开篇,算是总结下这段时间来的积累吧,废话不多说,直接干!
前言
点赞其实是一个很有意思的功能。基本的设计思路有大致两种, 一种自然是用mysql(写了几百行的代码都还没写完,有毒)啦
数据库直接落地存储, 另外一种就是利用点赞的业务特征来扔到redis(或memcache)中, 然后离线刷回mysql等。
我这里所讲的功能都是基于我之前的项目去说的,所以有些地方可以不用管的,我主要是记录这个功能的实现思路,当你理解了,基本想用什么鬼语言写都一样的。
直接写入Mysql
直接写入Mysql是最简单的做法。
做三个表即可,
-
comment_info
记录文章的主要内容,主要有like_count,hate_count,score这三个字段是我们本次功能的主要字段。
-
comment_like
记录文章被赞的次数,已有多少人赞过这种数据就可以直接从表中查到;
-
user_like_comment
记录用户赞过了哪些文章, 当打开文章列表时,显示的有没有赞过的数据就在这里面;
缺点
-
数据库读写压力大
热门文章会有很多用户点赞,甚至是短时间内被大量点赞, 直接操作数据库从长久来看不是很理想的做法
redis存储随后批量刷回数据库
redis主要的特点就是快, 毕竟主要数据都在内存嘛;
另外为啥我选择redis而不是memcache的主要原因在于redis支持更多的数据类型, 例如hash, set, zset等。
下面具体的会用到这几个类型。
优点
-
性能高
-
缓解数据库读写压力
其实我更多的在于缓解写压力, 真的读压力, 通过mysql主从甚至通过加入redis对热点数据做缓存都可以解决,
写压力对于前面的方案确实是不大好使。
缺点
-
开发复杂
这个比直接写mysql的方案要复杂很多, 需要考虑的地方也很多;
-
不能保证数据安全性
redis挂掉的时候会丢失数据, 同时不及时同步redis中的数据, 可能会在redis内存置换的时候被淘汰掉;
不过对于我们点赞而已, 稍微丢失一点数据问题不大;
其实上面第二点缺点是可以避免的,这就涉及到redis 的一些设计模式,不懂没关系,我尽量详细的写,后面我会给出如何解决这个缺点。
设计功能前知识准备
1.将要用到的redis数据类型(具体的类型说明,请看底部链接,有详细说明):
- zset 这个类型主要用来做排序或者数字的增减,这里被用作like 和hate的数字记录,以及热度的记录。
- set 这个是无序集合,主要用来记录今天需不需要更新,将今天被点赞(包括点讨厌)过的文章id记录下来,方便晚上或者有时间对这部分数据更新。
- hash 这个是散列,主要用来存储数据以及索引。这里被用来记录用户对哪个文章点了什么,方便下次判断(我看过一些网上的介绍使用set来记录,那个也可以,但是本人觉得这样做更省空间,以及方便管理,再有就是hash的速度快)。
- list 这个是队列大佬,我们的数据能不能 安全 回到mysql就靠它了。
2.关于热度如何去判断:
大家都知道,文章获得点赞数越高,文章的热度就越高,那么怎么判断呢?不就直接记录点赞数就行啦,但是对于最新的文章怎么办?例如有一篇文章一年前发布的,获得50个赞,有篇最新文章获得49个赞,但是按照上面所说的一年前的文章热度还比最新的高,这就不合理了,文章都是时效性,谁都想看最新最热的。
so!我们要换个方法去处理这个时效性,绝大部分语言都有 时间戳 生成的方法,时间戳随着时间越新,数字越大,直接将时间戳初始化赋值给文章的score,这样最新的文章相比以前的文章就会靠前了。接着是点赞对score的影响,我们假设一天得到20个赞算是一天最热,一天60*60*24=86400秒,然后得到一个赞就是得到86400 / 20 = 4320分。具体数字看自己的业务需求定,我只是举例子而已。点hate当然也会减去相应的数字。
激动时刻!直接上代码了!里面有详细注释!
1 <?php 2 3 class Good 4 { 5 public $redis = null; 6 7 //60*60*24/20=4320,每个点赞得到的分数,反之即之。 8 public $score = 4320; 9 10 //点赞增加数,或者点hate增加数 11 public $num = 1; 12 13 //init redis 14 public $redis_host = "127.0.0.1"; 15 public $redis_port = "6379"; 16 public $redis_pass = ""; 17 18 public function __construct() 19 { 20 $this->redis = new Redis(); 21 $this->redis->connect($this->redis_host,$this->redis_port); 22 $this->redis->auth($this->redis_pass); 23 } 24 25 /** 26 * @param int $user_id 用户id 27 * @param int $type 点击的类型 1.点like,2.点hate 28 * @param int $comment_id 文章id 29 * @return string json; 30 */ 31 public function click($user_id,$type,$comment_id) 32 { 33 //判断redis是否已经缓存了该文章数据 34 //使用:分隔符对redis管理是友好的 35 //这里使用redis zset-> zscore()方法 36 if($this->redis->zscore("comment:like",$comment_id)) 37 { 38 //已经存在 39 //判断点的是什么 40 if($type==1) 41 { 42 //判断以前是否点过,点的是什么? 43 //redis hash-> hget() 44 $rel = $this->redis->hget("comment:record",$user_id.":".$comment_id); 45 if(!$rel) 46 { 47 //什么都没点过 48 //点赞加1 49 $this->redis->zincrby("comment:like",$this->num,$comment_id); 50 //增加分数 51 $this->redis->zincrby("comment:score",$this->score,$comment_id); 52 //记录上次操作 53 $this->redis->hset("comment:record",$user_id.":".$comment_id,$type); 54 55 $data = array( 56 "state" => 1, 57 "status" => 200, 58 "msg" => "like+1", 59 ); 60 } 61 else if($rel==$type) 62 { 63 //点过赞了 64 //点赞减1 65 $this->redis->zincrby("comment:like",-($this->num),$comment_id); 66 //增加分数 67 $this->redis->zincrby("comment:score",-($this->score),$comment_id); 68 $data = array( 69 "state" => 2, 70 "status" => 200, 71 "msg" => "like-1", 72 ); 73 } 74 else if($rel==2) 75 { 76 //点过hate 77 //hate减1 78 $this->redis->zincrby("comment:hate",-($this->num),$comment_id); 79 //增加分数 80 $this->redis->zincrby("comment:score",$this->score+$this->score,$comment_id); 81 //点赞加1 82 $this->redis->zincrby("comment:like",$this->num,$comment_id); 83 //记录上次操作 84 $this->redis->hset("comment:record",$user_id.":".$comment_id,$type); 85 86 $data = array( 87 "state" => 3, 88 "status" => 200, 89 "msg" => "like+1", 90 ); 91 } 92 } 93 else if($type==2) 94 { 95 //点hate和点赞的逻辑是一样的。参看上面的点赞 96 $rel = $this->redis->hget("comment:record",$user_id.":".$comment_id); 97 if(!$rel) 98 { 99 //什么都没点过 100 //点hate加1 101 $this->redis->zincrby("comment:hate",$this->num,$comment_id); 102 //减分数 103 $this->redis->zincrby("comment:score",-($this->score),$comment_id); 104 //记录上次操作 105 $this->redis->hset("comment:record",$user_id.":".$comment_id,$type); 106 107 $data = array( 108 "state" => 4, 109 "status" => 200, 110 "msg" => "hate+1", 111 ); 112 } 113 else if($rel==$type) 114 { 115 //点过hate了 116 //点hate减1 117 $this->redis->zincrby("comment:hate",-($this->num),$comment_id); 118 //增加分数 119 $this->redis->zincrby("comment:score",$this->score,$comment_id); 120 121 $data = array( 122 "state" => 5, 123 "status" => 200, 124 "msg" => "hate-1", 125 ); 126 return $data; 127 } 128 else if($rel==2) 129 { 130 //点过like 131 //like减1 132 $this->redis->zincrby("comment:like",-($this->num),$comment_id); 133 //增加分数 134 $this->redis->zincrby("comment:score",-($this->score+$this->score),$comment_id); 135 //点hate加1 136 $this->redis->zincrby("comment:hate",$this->num,$comment_id); 137 138 $data = array( 139 "state" => 6, 140 "status" => 200, 141 "msg" => "hate+1", 142 ); 143 return $data; 144 } 145 } 146 } 147 else 148 { 149 //未存在 150 if($type==1) 151 { 152 //点赞加一 153 $this->redis->zincrby("comment:like",$this->num,$comment_id); 154 //分数增加 155 $this->redis->zincrby("comment:score",$this->score,$comment_id); 156 $data = array( 157 "state" => 7, 158 "status" => 200, 159 "msg" => "like+1", 160 ); 161 } 162 else if($type==2) 163 { 164 //点hate加一 165 $this->redis->zincrby("comment:hate",$this->num,$comment_id); 166 //分数减少 167 $this->redis->zincrby("comment:score",-($this->score),$comment_id); 168 169 $data = array( 170 "state" => 8, 171 "status" => 200, 172 "msg" => "hate+1", 173 ); 174 } 175 //记录 176 $this->redis->hset("comment:record",$user_id.":".$comment_id,$type); 177 } 178 179 //判断是否需要更新数据 180 $this->ifUploadList($comment_id); 181 182 return $data; 183 } 184 185 public function ifUploadList($comment_id) 186 { 187 date_default_timezone_set("Asia/Shanghai"); 188 $time = strtotime(date('Y-m-d H:i:s')); 189 190 if(!$this->redis->sismember("comment:uploadset",$comment_id)) 191 { 192 //文章不存在集合里,需要更新 193 $this->redis->sadd("comment:uploadset",$comment_id); 194 //更新到队列 195 $data = array( 196 "id" => $comment_id, 197 "time" => $time, 198 ); 199 $json = json_encode($data); 200 $this->redis->lpush("comment:uploadlist",$json); 201 } 202 } 203 } 204 205 //调用 206 $user_id = 100; 207 $type = 1; 208 $comment_id= 99; 209 $good = new Good(); 210 $rel = $good->click($user_id,$type,$comment_id); 211 var_dump($rel);
温馨提示:
1.上面代码只是一个实现的方法之一,里面的代码没精分过,适合大部分小伙伴阅读。用心看总有收获。
2.对于第三方接口,应该在外面包装多一层的,但是边幅有限,我就不做这么详细,提示,大家可以作为参考。
3.剩下的将数据返回数据的方法,等下篇再继续了。欢迎大家来交流心得。
redis手册中文版传送门:http://www.cnblogs.com/zcy_soft/archive/2012/09/21/2697006.html#string_INCR;