本文目录导读:

在PHP项目中实现“关注与粉丝”关系(通常称为社交关系或用户关系),通常采用关系型数据库(如MySQL)来存储用户之间的关注信息,下面我将从数据结构设计、核心功能逻辑、性能优化及代码示例四个方面为你详细讲解。
数据库设计(核心)
最经典的设计是使用一张独立的关系表,而不是在用户表中存储JSON数组。
CREATE TABLE `user_follow` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`follower_id` INT UNSIGNED NOT NULL COMMENT '关注者ID',
`followed_id` INT UNSIGNED NOT NULL COMMENT '被关注者ID',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '关注时间',
`status` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '1:关注中, 0:取消关注(软删除)',
UNIQUE KEY `uk_follower_followed` (`follower_id`, `followed_id`),
INDEX `idx_follower_status` (`follower_id`, `status`),
INDEX `idx_followed_status` (`followed_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户关注关系表';
设计要点:
- 联合唯一索引
(follower_id, followed_id)确保同一对用户仅有一条记录。 - 软删除(status字段)比物理删除更方便恢复数据和做统计。
- 复合索引
(follower_id, status)用于快速查询某人关注了谁;(followed_id, status)用于查询某人的粉丝。
用户表(users)补充字段(可选但推荐):
ALTER TABLE `users` ADD COLUMN `follower_count` INT UNSIGNED DEFAULT 0; ALTER TABLE `users` ADD COLUMN `following_count` INT UNSIGNED DEFAULT 0;
这两个冗余字段可以避免每次查询都要COUNT,大大提升列表页和首页的性能。
核心功能逻辑实现
关注/取消关注
public function toggleFollow(int $currentUserId, int $targetUserId): array
{
// 禁止关注自己
if ($currentUserId === $targetUserId) {
return ['code' => 400, 'msg' => '不能关注自己'];
}
// 检查目标用户是否存在
$user = User::find($targetUserId);
if (!$user) {
return ['code' => 404, 'msg' => '用户不存在'];
}
// 使用事务保证数据一致性
DB::beginTransaction();
try {
$record = UserFollow::where('follower_id', $currentUserId)
->where('followed_id', $targetUserId)
->first();
if ($record) {
// 已存在记录 -> 反转状态
$newStatus = !$record->status;
$record->status = $newStatus;
$record->save();
} else {
// 首次关注
UserFollow::create([
'follower_id' => $currentUserId,
'followed_id' => $targetUserId,
'status' => true
]);
$newStatus = true;
}
// 更新计数器(可根据status增减)
$this->updateCounters($currentUserId, $targetUserId, $newStatus);
DB::commit();
return ['code' => 200, 'msg' => $newStatus ? '关注成功' : '取消关注'];
} catch (\Exception $e) {
DB::rollBack();
return ['code' => 500, 'msg' => '操作失败'];
}
}
private function updateCounters(int $followerId, int $followedId, bool $isFollow)
{
$diff = $isFollow ? 1 : -1;
User::where('id', $followerId)->increment('following_count', $diff);
User::where('id', $followedId)->increment('follower_count', $diff);
}
获取粉丝列表(关注我的人)
public function getFans(int $userId, int $page = 1, int $pageSize = 20): array
{
return UserFollow::where('followed_id', $userId)
->where('status', true)
->orderBy('created_at', 'desc')
->paginate($pageSize, ['*'], 'page', $page)
->through(function ($follow) {
return [
'id' => $follow->follower->id,
'name' => $follow->follower->name,
'avatar' => $follow->follower->avatar,
'followed_at' => $follow->created_at
];
});
}
判断是否已关注(批量判断常用)
// 单个判断
public function isFollowing(int $currentUserId, int $targetUserId): bool
{
return UserFollow::where('follower_id', $currentUserId)
->where('followed_id', $targetUserId)
->where('status', true)
->exists();
}
// 批量判断(例如首页Feed流需要标记已关注状态)
public function batchCheckFollowStatus(int $currentUserId, array $targetUserIds): array
{
$exists = UserFollow::where('follower_id', $currentUserId)
->whereIn('followed_id', $targetUserIds)
->where('status', true)
->pluck('followed_id')
->toArray();
return array_fill_keys($targetUserIds, false) + array_fill_keys($exists, true);
}
高并发与性能优化
索引优化
- 上面的表设计已包含两个核心索引,避免全表扫描。
- 如果查询量大,可以为
(follower_id, created_at)建索引用于查询“我最近关注的人”。
计数器缓存(Redis)
- 将
follower_count和following_count缓存在Redis,设置过期时间(如1小时)。 - 更新时先更新数据库,再删除Redis缓存(Cache Aside模式),下次查询自动回填。
public function getUserFollowCount(int $userId): array
{
$redis = Redis::connection();
$key = "user:{$userId}:follow_count";
$counts = $redis->hgetall($key);
if (!$counts) {
// 从数据库读取并存入缓存
$user = User::find($userId, ['follower_count', 'following_count']);
$counts = [
'follower' => $user->follower_count,
'following' => $user->following_count
];
$redis->hmset($key, $counts);
$redis->expire($key, 3600);
}
return $counts;
}
异步更新计数器
关注/取关操作非常频繁,可以使用消息队列(RabbitMQ/Redis Stream)异步更新MySQL中的计数,减少主流程响应时间。
常见问题与注意事项
| 问题 | 解决方案 |
|---|---|
| 幂等性(重复点击关注) | 利用数据库 unique key,第二次插入报错,或使用INSERT ... ON DUPLICATE KEY UPDATE |
| 用户注销/删除 | 删除用户时同时逻辑删除所有关注关系 (UPDATE ... SET status=0 WHERE follower_id=xxx OR followed_id=xxx) |
| 分页性能 | 粉丝/关注列表避免用OFFSET大数值,改用游标分页(cursor based pagination),基于id或created_at |
| 双向关注判定 | 查询 (a->b) AND (b->a) 可定义为好友,用一条SQL + 自连接实现 |
游标分页示例:
$lastId = request('last_id', 0);
$list = UserFollow::where('followed_id', $userId)
->where('status', true)
->where('id', '<', $lastId) // 游标方式
->orderBy('id', 'desc')
->limit(20)
->get();
完整的关注接口示例(Laravel风格)
// routes/api.php
Route::post('/user/{user}/follow', [FollowController::class, 'toggle']);
// FollowController.php
public function toggle(Request $request, User $user)
{
$result = $this->followService->toggleFollow(
$request->user()->id,
$user->id
);
if ($result['code'] !== 200) {
return response()->json($result, $result['code']);
}
return response()->json([
'followed' => $result['msg'] === '关注成功',
'following_count' => $request->user()->fresh()->following_count,
'follower_count' => $user->fresh()->follower_count
]);
}
| 模块 | 推荐做法 |
|---|---|
| 存储结构 | 独立关系表 + 联合唯一索引 + 软删除status字段 |
| 计数器 | 用户表冗余字段 + Redis缓存加速 |
| 判断逻辑 | 批量查询使用 whereIn 一次返回 |
| 并发安全 | MySQL事务 + 唯一索引保证不重复 |
| 分页 | 游标分页优于传统OFFSET |
这个设计在百万级用户、千万级关注关系的场景下依然能保持良好性能,如果你的项目用户量级更大(如亿级),需要考虑分表(按用户ID取模)或改用图数据库(如Neo4j)。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播