在开发社交类应用时,我们常常会纠结于数据库的选择 —— 关系型数据库虽成熟,但在高频读写和复杂关系处理上总有些力不从心。而 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:用户ID的auth字段是否匹配
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 的数据结构如何映射到实际业务场景 —— 列表存动态、有序集合存关系、哈希存对象,配合原子操作确保并发安全。这种设计不仅简洁高效,还能轻松扩展。 如果本文对你有帮助,别忘了点赞收藏,关注我,一起探索更高效的开发方式~