Redis学习笔记2:简易的twitter版本PHP+redis

1. Data layout

    当我们使用redis时候,我们并没有表。所以我们需要key来标识对象,而value来存储所需要的值。对于retwis(twitter的简易版本)来说,我们可以通过唯一的ID来标识用户,通过INCR操作即可:

127.0.0.1:6379> set next_user_id 1000
OK
127.0.0.1:6379> INCR next_user_id
(integer) 1001
127.0.0.1:6379> HMSET user:1001 username lcj password p1pp2
OK
127.0.0.1:6379> HSET users lcj 1001
(integer) 0
127.0.0.1:6379> INCR next_user_id
(integer) 1002
127.0.0.1:6379> HMSET user:1002 username voler password pp2pp3
OK
127.0.0.1:6379> HSET users voler 1002
(integer) 0
    我们通过不断的INCR,来得到唯一的ID。这里user:1001为哈希表,用来标识一个用户,而我们通过哈希表users,来关联用户和ID。

2. Followers,following, and updates

    一个用户应该有粉丝,并且他也应该是其他人的粉丝。我们可以使用集合(set)来标识粉丝。但是我们可以使用sorted set来标识它们,用创建粉丝的unix时间当做排序字段,而用户的ID作为实际的值。而粉丝可用以下表示:

followers:1000 =>sorted set of uids of all the followers users
following:1000 =>sorted set of uids of all the following users
    其中,1000为用户的ID,则我们可以通过以下的代码达到互粉:
127.0.0.1:6379> ZADD followers:1001 1401267618 1002
(integer) 1
127.0.0.1:6379> ZADD followers:1002 1401267618 1001
(integer) 1
    那么,我们如何显示用户发表的140个字的微博呢?我们可以使用list结构。而如果整页显示用户发表的内容,我们可以使用LRANGE来读取list的所有内容:
posts:1000 => a list of post ids - every new post is LPUSHed here

3. Authentication

    我们可以使用一个随机不可猜测的字符串来验证用户,并用来设置cookie,我们甚至要把用户ID和其字符串关联起来,存储于一个哈希表中:

127.0.0.1:6379> HSET user:1000 auth fea5e81ac8ca77622bed1c2132a021f9
(integer) 1
127.0.0.1:6379> HSET auths fea5e81ac8ca77622bed1c2132a021f9 1000
(integer) 1
    为了进行验证我们需要进行以下几个步骤:

1. 得到用户名和密码(登录时候获知)

2. 检查用户名是否存在“用户哈希表”中

3. 如果存在,则得到其用户ID(如1000)

4. 检查用户user:1000的密码是否匹配。如果不匹配则报错。

5. 如果匹配则生成一个随机不可预测的字符串“fea5e81ac8ca77622bed1c2132a021f9”作为认证的cookie。

    简单的PHP代码如下:

include("retwis.php");

# Form sanity checks
if (!gt("username") || !gt("password"))
    goback("You need to enter both username and password to login.");

# The form is ok, check if the username is available
$username = gt("username");
$password = gt("password");
$r = redisLink();
$userid = $r->hget("users",$username);
if (!$userid)
    goback("Wrong username or password");
$realpassword = $r->hget("user:$userid","password");
if ($realpassword != $password)
    goback("Wrong useranme or password");

# Username / password OK, set the cookie and redirect to index.php
$authsecret = $r->hget("user:$userid","auth");
setcookie("auth",$authsecret,time()+3600*24*365);
header("Location: index.php");
    当然,我们也要检查用户是否已经登录:

1. 得到此用户的认证cookie,如果没有cookie,则此用户未登录。如果有的话,我们则得到此cookie的用户ID。

2. 判断此用户的ID和登录的用户是否匹配,如果匹配,则提示信息:

function isLoggedIn() {
    global $User, $_COOKIE;

    if (isset($User)) return true;

    if (isset($_COOKIE['auth'])) {
        $r = redisLink();
        $authcookie = $_COOKIE['auth'];
        if ($userid = $r->hget("auths",$authcookie)) {
            if ($r->hget("user:$userid","auth") != $authcookie) return false;
            loadUserInfo($userid);
            return true;
        }
    }
    return false;
}

function loadUserInfo($userid) {
    global $User;

    $r = redisLink();
    $User['id'] = $userid;
    $User['username'] = $r->hget("user:$userid","username");
    return true;
}
    而为什么这里要多判断一次:用户ID是否已经匹配呢?不是只要判断是否存在cookie则可以知道用户是否登录了吗?因为可能存在BUG产生多个cookie指向一个用户,但是ID却是唯一的(实际上是通过ID来验证用户)。那么当用户注销时候,我们需要删除旧的认证,给用户赋值一个新的认证即可。(这里有一点个人推测是:用户的认证cookie是唯一的,即是通过算法关联用户名,密码等生成的一个不可预测的字符串。所以我们判断用户是否登录需要cookie和ID,因为用户退出时也会生成一个认证字符串):
include("retwis.php");

if (!isLoggedIn()) {
    header("Location: index.php");
    exit;
}

$r = redisLink();
$newauthsecret = getrand();
$userid = $User['id'];
$oldauthsecret = $r->hget("user:$userid","auth");

$r->hset("user:$userid","auth",$newauthsecret);
$r->hset("auths",$newauthsecret,$userid);
$r->hdel("auths",$oldauthsecret);

header("Location: index.php");

4. Updates

    用户发表微博的代码类似这样:

127.0.0.1:6379> set next_post_id 1000
OK
127.0.0.1:6379> INCR next_post_id 
(integer) 1001
127.0.0.1:6379> HMSET post:1001 user_id $owner_id time $time body "I am happy"
OK
    我们如果更新了微博,则需要将内容呈现给自己的粉丝:
include("retwis.php");

if (!isLoggedIn() || !gt("status")) {
    header("Location:index.php");
    exit;
}

$r = redisLink();
$postid = $r->incr("next_post_id");
$status = str_replace("\n"," ",gt("status"));
$r->hmset("post:$postid","user_id",$User['id'],"time",time(),"body",$status);
$followers = $r->zrange("followers:".$User['id'],0,-1);
$followers[] = $User['id']; /* Add the post to our own posts too */

foreach($followers as $fid) {
    $r->lpush("posts:$fid",$postid);
}
# Push the post on the timeline, and trim the timeline to the
# newest 1000 elements.
$r->lpush("timeline",$postid);
$r->ltrim("timeline",0,1000);

header("Location: index.php");

5. Paginating updates

    此段的原理不懂,最好通过搭建实际的框架运行,并调试代码,即可明白其原理:

function showPost($id) {
    $r = redisLink();
    $post = $r->hgetall("post:$id");
    if (empty($post)) return false;

    $userid = $post['user_id'];
    $username = $r->hget("user:$userid","username");
    $elapsed = strElapsed($post['time']);
    $userlink = "<a class=\"username\" href=\"profile.php?u=".urlencode($username)."\">".utf8entities($username)."</a>";

    echo('<div class="post">'.$userlink.' '.utf8entities($post['body'])."<br>");
    echo('<i>posted '.$elapsed.' ago via web</i></div>');
    return true;
}

function showUserPosts($userid,$start,$count) {
    $r = redisLink();
    $key = ($userid == -1) ? "timeline" : "posts:$userid";
    $posts = $r->lrange($key,$start,$start+$count);
    $c = 0;
    foreach($posts as $p) {
        if (showPost($p)) $c++;
        if ($c == $count) break;
    }
    return count($posts) == $count+1;
}
    如果以后工作要用到redis和python,我倒找个时间用python实现一下这个好玩的retwis。


你可能感兴趣的:(Redis学习笔记2:简易的twitter版本PHP+redis)