用 Redis 构建简易 Twitter 克隆:从数据结构到实战部署

72 阅读6分钟

在开发社交类应用时,我们常常会纠结于数据库的选择 —— 关系型数据库虽成熟,但在高频读写和复杂关系处理上总有些力不从心。而 Redis 作为一款高性能的键值存储,凭借其丰富的数据结构和原子操作,其实能轻松扛起这类应用的核心存储职责。今天,我们就通过一个简易 Twitter 克隆项目 Retwis,聊聊 Redis 在实际开发中的具体用法,从基础数据结构到完整功能实现,一步到位搞懂 Redis 的实战技巧。

一、Redis 核心:不止于键值的存储

提到键值存储,我们可能会觉得它就是 “存键取值” 这么简单。但 Redis 的强大之处在于,它的 “值” 能是多种复杂结构,而且操作都是原子性的 —— 这也是我们能靠它独立开发完整应用的关键。

1. 基础键值操作

键值存储的核心是通过 “键” 存取 “值”,Redis 的基础命令很直观:

  • SET key value:存储键值对,比如SET username "redis_user"
  • GET key:获取值,GET username会返回"redis_user"
  • DEL key:删除键值对,DEL username后再获取就返回空
  • SETNX key value:仅当键不存在时设置值,适合做分布式锁
  • INCR key:原子递增数值,比如SET count 10后,INCR count会返回 11

2. 为什么需要原子操作?

INCR这样的原子操作,在并发场景下至关重要。如果我们手动实现递增:

php

$x = $redis->get('count');
$x = $x + 1;
$redis->set('count', $x);

在多客户端同时操作时,可能两个客户端都读到10,最终都写成11,导致递增丢失。而INCR是 Redis 服务器层面的原子操作,能保证同一时间只有一个客户端执行,完美避免并发问题。

二、Redis 数据结构:Retwis 中的实战应用

Redis 支持列表、集合、有序集合、哈希等数据结构,每个结构都有其独特场景。我们结合 Retwis 的功能,看看它们是如何被用到的。

1. 列表(Lists):存储用户动态

列表是有序的字符串集合,支持从两端添加 / 删除元素,适合存储时序数据(比如用户发布的动态)。

  • LPUSH key value:从列表左侧(头部)添加元素

  • RPUSH key value:从列表右侧(尾部)添加元素

  • LRANGE key start end:获取指定范围的元素(0 表示第一个,-1 表示最后一个)

  • LTRIM key start end:截取列表,只保留指定范围的元素

在 Retwis 中,用户发布的动态会用LPUSH添加到posts:用户ID列表中,比如:

php

// 发布动态时,将动态ID添加到作者的动态列表
$redis->lpush("posts:$userid", $postid);

获取动态时用LRANGE分页:

php

// 获取用户动态,从第$start个开始,取$count$posts = $redis->lrange("posts:$userid", $start, $start + $count - 1);

还可以用LTRIM限制列表长度(比如只保留最新的 1000 条动态):

php

// 全局时间线只保留最新1000条动态
$redis->ltrim("timeline", 0, 999);

2. 集合(Sets):无重复元素的集合

集合是无序的、无重复元素的集合,适合存储 “唯一关系”(比如用户的标签)。

  • SADD key member:添加元素

  • SREM key member:删除元素

  • SINTER key1 key2...:求多个集合的交集

  • SMEMBERS key:获取所有元素

  • SCARD key:获取元素数量

比如,若需要存储用户的兴趣标签(不重复),可以用集合:

php

// 给用户添加兴趣标签
$redis->sadd("user:1000:interests", "tech");
$redis->sadd("user:1000:interests", "sports");
// 获取用户所有兴趣
$interests = $redis->smembers("user:1000:interests"); // 返回["tech", "sports"]

3. 有序集合(Sorted Sets):带分数的有序集合

有序集合在集合的基础上,给每个元素关联一个 “分数”,元素按分数排序,适合存储 “带权重的关系”(比如关注 / 粉丝关系,分数可以是关注时间)。

  • ZADD key score member:添加元素(指定分数)

  • ZRANGE key start end:按分数升序获取元素

  • ZSCORE key member:获取元素的分数

  • ZINTERSTORE dest numkeys key1 key2...:求多个有序集合的交集

在 Retwis 中,关注 / 粉丝关系用有序集合存储,分数为关注时间:

php

// 用户1000关注用户5000,分数为当前时间戳
$redis->zadd("following:1000", time(), 5000);
// 用户5000的粉丝增加用户1000,分数为当前时间戳
$redis->zadd("followers:5000", time(), 1000);

获取用户的所有关注列表:

php

// 获取用户1000关注的所有用户ID
$following = $redis->zrange("following:1000", 0, -1);

4. 哈希(Hashes):存储对象属性

哈希是键值对的集合,适合存储对象(比如用户信息、动态内容)。

  • HMSET key field1 value1 field2 value2...:设置多个字段

  • HGET key field:获取单个字段的值

  • HGETALL key:获取所有字段和值

  • HINCRBY key field increment:递增字段的值

在 Retwis 中,用户信息用哈希存储:

php

// 创建用户,用哈希存储用户名、密码等信息
$redis->hmset("user:1000", "username", "antirez", "password", "p1pp0", "auth", "fea5e81ac...");

动态内容也用哈希存储:

php

// 存储动态信息:用户ID、时间、内容
$redis->hmset("post:10343", "user_id", 1000, "time", time(), "body", "I'm using Redis!");

三、Retwis 实战:核心功能实现

1. 用户系统设计

(1)生成唯一用户 ID

INCR原子操作生成唯一 ID,确保并发安全:

php

// 每次创建新用户,先获取唯一ID
$userid = $redis->incr("next_user_id"); // 比如返回1000

(2)用户信息存储

用哈希存储用户详情,同时用users哈希映射用户名到用户 ID(方便登录查询):

php

// 存储用户信息
$redis->hmset("user:$userid", "username", $username, "password", $password, "auth", $authsecret);
// 映射用户名到用户ID
$redis->hset("users", $username, $userid);

2. 认证机制

通过 cookie 和 Redis 哈希实现无状态认证:

  • 用户登录时,从users哈希获取用户 ID,验证密码后,用user:用户ID中的auth字段作为 cookie 值

  • 客户端请求时,通过 cookie 中的auth值,在auths哈希中查询对应的用户 ID,再验证user:用户IDauth字段是否匹配

php

// 登录逻辑
$username = $_POST['username'];
$password = $_POST['password'];
$userid = $redis->hget("users", $username);
if ($userid && $redis->hget("user:$userid", "password") == $password) {
    $authsecret = $redis->hget("user:$userid", "auth");
    setcookie("auth", $authsecret, time() + 3600*24*365); // 设置cookie
}

// 验证登录状态
function isLoggedIn() {
    global $User;
    if (isset($_COOKIE['auth'])) {
        $authsecret = $_COOKIE['auth'];
        $userid = $redis->hget("auths", $authsecret);
        if ($userid && $redis->hget("user:$userid", "auth") == $authsecret) {
            // 验证通过,加载用户信息
            $User = loadUserInfo($userid);
            return true;
        }
    }
    return false;
}

3. 动态发布与时间线

(1)发布动态

  • 生成唯一动态 ID,用哈希存储动态内容

  • 将动态 ID 添加到作者的动态列表和所有粉丝的动态列表

  • 同时添加到全局时间线,并LTRIM限制长度

php

// 发布动态
$postid = $redis->incr("next_post_id"); // 生成动态ID
// 存储动态内容
$redis->hmset("post:$postid", "user_id", $User['id'], "time", time(), "body", $status);
// 获取所有粉丝ID
$followers = $redis->zrange("followers:".$User['id'], 0, -1);
$followers[] = $User['id']; // 包含自己
// 给每个粉丝的动态列表添加该动态
foreach ($followers as $fid) {
    $redis->lpush("posts:$fid", $postid);
}
// 更新全局时间线
$redis->lpush("timeline", $postid);
$redis->ltrim("timeline", 0, 999); // 只保留最新1000条

(2)动态分页展示

LRANGE获取指定范围的动态 ID,再查询详情展示:

php

// 展示用户动态
function showUserPosts($userid, $start, $count) {
    $posts = $redis->lrange("posts:$userid", $start, $start + $count - 1);
    foreach ($posts as $postid) {
        showPost($postid); // 展示单条动态
    }
}

// 展示单条动态
function showPost($postid) {
    $post = $redis->hgetall("post:$postid"); // 获取动态详情
    $username = $redis->hget("user:".$post['user_id'], "username"); // 获取作者用户名
    echo "<div>{$username}: {$post['body']}</div>";
}

4. 关注 / 粉丝关系

用有序集合存储关注和粉丝列表(分数为关注时间):

php

// 关注用户:用户A关注用户B
function follow($userA, $userB) {
    $now = time();
    // A的关注列表添加B
    $redis->zadd("following:$userA", $now, $userB);
    // B的粉丝列表添加A
    $redis->zadd("followers:$userB", $now, $userA);
}

// 取消关注
function unfollow($userA, $userB) {
    $redis->zrem("following:$userA", $userB);
    $redis->zrem("followers:$userB", $userA);
}

四、水平扩展:从单节点到分布式

Redis 单节点性能足够支撑大量用户(文档中测试单节点可轻松处理百万级日活),但如需进一步扩展,可通过以下方式:

  • 客户端分片:按用户 ID 范围将数据分配到不同 Redis 节点

  • 代理分片:用 Twemproxy 等工具统一管理分片

  • Redis Cluster:官方分布式方案,自动分片和故障转移

Retwis 的设计避免了多键操作,因此分片扩展非常简单,只需确保同一用户的数据落在同一节点即可。

总结

通过 Retwis 的案例,我们能清晰看到 Redis 的数据结构如何映射到实际业务场景 —— 列表存动态、有序集合存关系、哈希存对象,配合原子操作确保并发安全。这种设计不仅简洁高效,还能轻松扩展。 如果本文对你有帮助,别忘了点赞收藏,关注我,一起探索更高效的开发方式~