点赞其实是一个很有意思的功能。基本的设计思路有大致两种, 一种自然是用mysql(写了几百行的代码都还没写完,有毒)啦。数据库直接落地存储, 另外一种就是利用点赞的业务特征来扔到redis(或memcache)中, 然后离线刷回mysql等。
我这里所讲的功能都是基于我之前的项目去说的,所以有些地方可以不用管的,我主要是记录这个功能的实现思路,当你理解了,基本想用什么鬼语言写都一样的。
直接写入Mysql是最简单的做法。
做三个表即可,
数据库读写压力大
热门文章会有很多用户点赞,甚至是短时间内被大量点赞, 直接操作数据库从长久来看不是很理想的做法
redis主要的特点就是快, 毕竟主要数据都在内存嘛;
另外为啥我选择redis而不是memcache的主要原因在于redis支持更多的数据类型, 例如hash, set, zset等。
下面具体的会用到这几个类型。
其实上面第二点缺点是可以避免的,这就涉及到redis 的一些设计模式,不懂没关系,我尽量详细的写,后面我会给出如何解决这个缺点。
大家都知道,文章获得点赞数越高,文章的热度就越高,那么怎么判断呢?不就直接记录点赞数就行啦,但是对于最新的文章怎么办?例如有一篇文章一年前发布的,获得50个赞,有篇最新文章获得49个赞,但是按照上面所说的一年前的文章热度还比最新的高,这就不合理了,文章都是时效性,谁都想看最新最热的。
so!我们要换个方法去处理这个时效性,绝大部分语言都有 时间戳 生成的方法,时间戳随着时间越新,数字越大,直接将时间戳初始化赋值给文章的score,这样最新的文章相比以前的文章就会靠前了。接着是点赞对score的影响,我们假设一天得到20个赞算是一天最热,一天606024=86400秒,然后得到一个赞就是得到86400 / 20 = 4320分。具体数字看自己的业务需求定,我只是举例子而已。点hate当然也会减去相应的数字。
redis = new Redis();
$this->redis->connect($this->redis_host,$this->redis_port);
$this->redis->auth($this->redis_pass);
}
/**
* @param int $user_id 用户id
* @param int $type 点击的类型 1.点like,2.点hate
* @param int $comment_id 文章id
* @return string json;
*/
public function click($user_id,$type,$comment_id)
{
//判断redis是否已经缓存了该文章数据
//使用:分隔符对redis管理是友好的
//这里使用redis zset-> zscore()方法
if($this->redis->zscore("comment:like",$comment_id))
{
//已经存在
//判断点的是什么
if($type==1)
{
//判断以前是否点过,点的是什么?
//redis hash-> hget()
$rel = $this->redis->hget("comment:record",$user_id.":".$comment_id);
if(!$rel)
{
//什么都没点过
//点赞加1
$this->redis->zincrby("comment:like",$this->num,$comment_id);
//增加分数
$this->redis->zincrby("comment:score",$this->score,$comment_id);
//记录上次操作
$this->redis->hset("comment:record",$user_id.":".$comment_id,$type);
$data = array(
"state" => 1,
"status" => 200,
"msg" => "like+1",
);
}
else if($rel==$type)
{
//点过赞了
//点赞减1
$this->redis->zincrby("comment:like",-($this->num),$comment_id);
//增加分数
$this->redis->zincrby("comment:score",-($this->score),$comment_id);
$data = array(
"state" => 2,
"status" => 200,
"msg" => "like-1",
);
}
else if($rel==2)
{
//点过hate
//hate减1
$this->redis->zincrby("comment:hate",-($this->num),$comment_id);
//增加分数
$this->redis->zincrby("comment:score",$this->score+$this->score,$comment_id);
//点赞加1
$this->redis->zincrby("comment:like",$this->num,$comment_id);
//记录上次操作
$this->redis->hset("comment:record",$user_id.":".$comment_id,$type);
$data = array(
"state" => 3,
"status" => 200,
"msg" => "like+1",
);
}
}
else if($type==2)
{
//点hate和点赞的逻辑是一样的。参看上面的点赞
$rel = $this->redis->hget("comment:record",$user_id.":".$comment_id);
if(!$rel)
{
//什么都没点过
//点hate加1
$this->redis->zincrby("comment:hate",$this->num,$comment_id);
//减分数
$this->redis->zincrby("comment:score",-($this->score),$comment_id);
//记录上次操作
$this->redis->hset("comment:record",$user_id.":".$comment_id,$type);
$data = array(
"state" => 4,
"status" => 200,
"msg" => "hate+1",
);
}
else if($rel==$type)
{
//点过hate了
//点hate减1
$this->redis->zincrby("comment:hate",-($this->num),$comment_id);
//增加分数
$this->redis->zincrby("comment:score",$this->score,$comment_id);
$data = array(
"state" => 5,
"status" => 200,
"msg" => "hate-1",
);
return $data;
}
else if($rel==2)
{
//点过like
//like减1
$this->redis->zincrby("comment:like",-($this->num),$comment_id);
//增加分数
$this->redis->zincrby("comment:score",-($this->score+$this->score),$comment_id);
//点hate加1
$this->redis->zincrby("comment:hate",$this->num,$comment_id);
$data = array(
"state" => 6,
"status" => 200,
"msg" => "hate+1",
);
return $data;
}
}
}
else
{
//未存在 (这里需要从数据库获取最新的点赞数)
if($type==1)
{
//点赞加一 (数据库存在则加上数据库的值)
$this->redis->zincrby("comment:like",$this->num,$comment_id);
//分数增加
$this->redis->zincrby("comment:score",$this->score,$comment_id);
$data = array(
"state" => 7,
"status" => 200,
"msg" => "like+1",
);
}
else if($type==2)
{
//点hate加一
$this->redis->zincrby("comment:hate",$this->num,$comment_id);
//分数减少
$this->redis->zincrby("comment:score",-($this->score),$comment_id);
$data = array(
"state" => 8,
"status" => 200,
"msg" => "hate+1",
);
}
//记录
$this->redis->hset("comment:record",$user_id.":".$comment_id,$type);
}
//判断是否需要更新数据
$this->ifUploadList($comment_id);
return $data;
}
public function ifUploadList($comment_id)
{
date_default_timezone_set("Asia/Shanghai");
$time = strtotime(date('Y-m-d H:i:s'));
if(!$this->redis->sismember("comment:uploadset",$comment_id))
{
//文章不存在集合里,需要更新
$this->redis->sadd("comment:uploadset",$comment_id);
//更新到队列
$data = array(
"id" => $comment_id,
"time" => $time,
);
$json = json_encode($data);
$this->redis->lpush("comment:uploadlist",$json);
}
}
}
//调用
$user_id = 100;
$type = 1;
$comment_id= 99;
$good = new Good();
$rel = $good->click($user_id,$type,$comment_id);
var_dump($rel);
1.上面代码只是一个实现的方法之一,里面的代码没精分过,适合大部分小伙伴阅读。用心看总有收获。
2.对于第三方接口,应该在外面包装多一层的,但是边幅有限,我就不做这么详细,提示,大家可以作为参考。
3.剩下的将数据返回数据的方法,等下篇再继续了。欢迎大家来交流心得。
里面还会涉及到如何将错误信息以及提示信息保存到文件里,方便以后的运维,再有就是如何使用PHP写进程BAT。
首先针对上篇提到的关于redis刷回数据库的安全性的设计模式,因为我们使用的是list来做数据索引,所以在我们将list数据提取出来的时候,一旦redis在这时候出现异常,就会导致刚提取出来的数据丢失!有些小伙伴就说,丢失就丢失呗,才一点数据。但是我们做程序,就应该以严谨为基础,所以下面就来说下Redis List这位大佬给我们提供了什么帮助。
index.php:
open($txt,"a+");
while($rel)
{
$redis = new RedisCtrl();
//开始干活
if($num==0){
//这里就是将信息输出到文件里记录,下面很多地方都是一样的。
$text = "start ".$name."\n";
echo $text;
$output->write($test,$text);
}
//获取备份队列的长度
$copylistlength = $redis->llen("comment:uploadcopylist");
//我这里展示的是第一数据回滚到mysql,小伙伴想批量回滚的,自己改装下就可以用了。
//自己动手丰衣足食!
if($copylistlength>1)
{
//由于是单一数据回滚,所以我要判断它是否超过我设定的值,小伙伴们最好也自己定一个阈值。
//report error
echo $now." ->false\n";
$rel = false;
return;
}
else if($copylistlength==1)
{
//这里判断防止上次redis出现错误,导致数据没有及时回到mysql
$data = $redis->rpop("comment:uploadcopylist");
$rel = $redis->UpdateClickGoodDataToMysql($data);
$text = $rel."\n";
echo $text;
$output->write($test,$text);
}
else
{
//获取主要队列的长度
$listlength = $redis->llen("comment:uploadlist");
if ($listlength>0) {
//使用之前说到的设计模式
$data = $redis->rpoplpush("comment:uploadlist","comment:uploadcopylist");
$rel = $redis->UpdateClickGoodDataToMysql($data);
$text = $rel."\n";
echo $text;
$output->write($test,$text);
}else{
// 队列为空
// 打印关闭信息,这里的写法算是维持进程窗口不关闭,需要手动关闭
// 如果想让它执行完自动关闭的,
// 把下面改写成$rel = false;
if($num<=3){
$text = $now." -> please close .\n";
echo $text;
$output->write($test,$text);
$num++;
}
else
{
$output->close($test);
}
}
}
}
redis操作类 Redis.class.php:
redis = new Redis();
$this->redis->connect(self::$redisIp,self::$redisPort);
$this->redis->auth(self::$redisPass);
}
public function llen($key)
{
$rel = $this->redis->llen($key);
return $rel;
}
public function rpop($key)
{
$rel = $this->redis->rpop($key);
return $rel;
}
public function rpoplpush($source,$destination)
{
$rel = $this->redis->rpoplpush($source,$destination);
return $rel;
}
public function UpdateClickGoodDataToMysql($data)
{
//get id and time from redis list
$result = json_decode($data,true);
$id = $result['id'];
$time = $result['time'];
$arr = array();
//like
$like = $this->redis->zscore("comment:like",$id);
$like = $like?$like:0;
//hate
$hate = $this->redis->zscore("comment:hate",$id);
$hate = $hate?$hate:0;
$sql = "update comment_info set like_count=".$like.", hate_count=".$hate." where id=".$id;
$arr[] = $sql;
//update sql
$mysql = new MysqlCtrl();
$mysql->saveMySQL($arr);
//更新完,将set集合里需要更新的id去掉
$this->redis->srem("comment:uploadset",$id);
//更新完毕,将备份队列里的数据去掉
$this->redis->lrem("comment:uploadcopylist",$data);
return $sql."\n";
}
}
mysql类 Mysql.class.php
dsn = self::$dbms.":host=".self::$host.";dbname=".self::$database;
//return $dsn;
try {
$this->dbh = new PDO($this->dsn, self::$user, self::$pass);
echo "Connected\n";
} catch (Exception $e) {
echo $this->dsn;
die("Unable to connect: " . $e->getMessage());
}
}
//保存数据到数据库
//PDO
public function saveMySQL($arr)
{
try {
$this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->dbh->beginTransaction();
$count = count($arr);
for ($i=0; $i < $count; $i++) {
$this->dbh->exec($arr[$i]);
}
$this->dbh->commit();
} catch (Exception $e) {
$this->dbh->rollBack();
echo "Failed: " . $e->getMessage()."\n";
$json = json_encode($arr);
echo "False-SQL: ".$json."\n";
exit();
}
}
}
输出信息到文件的类 Output_Log.class.php
clickgood_log.txt 这里是保存输出的信息,里面是空白的。
hellO world
上面这些就是整套数据保存到mysql的操作,这是本人源码copy过来的,所以细分程度比较高,但是可扩展性也很高。有什么错误的地方希望小伙伴们能提出,谢谢。