如何用PHP结合Redis计数和数据库实现文章点赞功能?
- 内容介绍
- 文章标签
- 相关推荐
本文共计833个文字,预计阅读时间需要4分钟。
直接说结论:
Redis 用什么结构存点赞数和用户状态
两个 key 分开管,别塞进一个 Hash 里硬扛所有逻辑:
-
article:like_count:{article_id}:用INCR/DECR做原子计数,类型是 String。这是你前端显示的“XX 人点赞”来源,快且线程安全 -
article:liked_by:{article_id}:用 Redis Set 存已点赞的user_id,比如SADD article:liked_by:123 456。查“我点没点”就SISMEMBER article:liked_by:123 456,O(1) 响应
不用 Hash 存状态,是因为 Hash 的 field 不能做成员存在性判断(HGET 返回 null 不代表没点过,可能是点过又取消),而 Set 天然支持 SISMEMBER + SREM + SCARD,语义清晰、操作原子。
PHP 怎么连 Redis 并做点赞/取消的原子操作
用 Predis 客户端(比原生 Redis 扩展更易处理 pipeline 和事务):
立即学习“PHP免费学习笔记(深入)”;
$redis = new Predis\Client([ 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379, 'password' => 'your_pass', 'database' => 0, ]); <p>// 点赞(带防重) $pipe = $redis->pipeline(); $pipe->sAdd('article:liked_by:'.$article_id, $user_id); $pipe->incr('article:like_count:'.$article_id); $result = $pipe->execute();</p><p>// 取消点赞(同样原子) $pipe = $redis->pipeline(); $pipe->sRem('article:liked_by:'.$article_id, $user_id); $pipe->decr('article:like_count:'.$article_id); $result = $pipe->execute();
注意:pipeline 不等于事务,但对这两个操作足够了;真要强一致性(比如防止超赞),得用 WATCH + MULTI,但实际中 SADD/SREM 本身已幂等,够用。
定时把 Redis 数据写回 MySQL 的坑在哪
不是简单地 SELECT * FROM article WHERE id IN (...) 再 UPDATE 就完事:
- 别用
SCARD当最终点赞数 —— Redis 可能有脏数据(如用户删号后没清理 Set),要以数据库为准做校验 - 写回时用
INSERT ... ON DUPLICATE KEY UPDATE,主键设为(article_id, user_id),避免重复插入或丢失取消动作 - 定时任务别卡死:用
SCAN游标分批读article:liked_by:*,每次最多取 1000 个 key,防止 Redis 阻塞 - 记录最后同步时间戳到 MySQL 表里,下次只扫新增的点赞 key,别全量扫
示例 SQL:
INSERT INTO article_like (article_id, user_id, created_at) VALUES (?, ?, NOW()) ON DUPLICATE KEY UPDATE updated_at = NOW();
为什么不能只靠 Redis 持久化
Redis 是缓存,不是数据库。哪怕开了 AOF + RDB,以下情况仍会丢数据:
- 服务器断电且 AOF 未刷盘(默认每秒刷一次,可能丢 1 秒数据)
- 内存淘汰策略触发
maxmemory-policy,误删了点赞 key(尤其用allkeys-lru时) - 运维误操作
FLUSHALL或配置错误导致 DB 编号错乱
所以点赞关系表(article_like)必须存在,且字段要有 created_at 和 updated_at,用于对账和补漏 —— 这不是备胎,是底线。
本文共计833个文字,预计阅读时间需要4分钟。
直接说结论:
Redis 用什么结构存点赞数和用户状态
两个 key 分开管,别塞进一个 Hash 里硬扛所有逻辑:
-
article:like_count:{article_id}:用INCR/DECR做原子计数,类型是 String。这是你前端显示的“XX 人点赞”来源,快且线程安全 -
article:liked_by:{article_id}:用 Redis Set 存已点赞的user_id,比如SADD article:liked_by:123 456。查“我点没点”就SISMEMBER article:liked_by:123 456,O(1) 响应
不用 Hash 存状态,是因为 Hash 的 field 不能做成员存在性判断(HGET 返回 null 不代表没点过,可能是点过又取消),而 Set 天然支持 SISMEMBER + SREM + SCARD,语义清晰、操作原子。
PHP 怎么连 Redis 并做点赞/取消的原子操作
用 Predis 客户端(比原生 Redis 扩展更易处理 pipeline 和事务):
立即学习“PHP免费学习笔记(深入)”;
$redis = new Predis\Client([ 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379, 'password' => 'your_pass', 'database' => 0, ]); <p>// 点赞(带防重) $pipe = $redis->pipeline(); $pipe->sAdd('article:liked_by:'.$article_id, $user_id); $pipe->incr('article:like_count:'.$article_id); $result = $pipe->execute();</p><p>// 取消点赞(同样原子) $pipe = $redis->pipeline(); $pipe->sRem('article:liked_by:'.$article_id, $user_id); $pipe->decr('article:like_count:'.$article_id); $result = $pipe->execute();
注意:pipeline 不等于事务,但对这两个操作足够了;真要强一致性(比如防止超赞),得用 WATCH + MULTI,但实际中 SADD/SREM 本身已幂等,够用。
定时把 Redis 数据写回 MySQL 的坑在哪
不是简单地 SELECT * FROM article WHERE id IN (...) 再 UPDATE 就完事:
- 别用
SCARD当最终点赞数 —— Redis 可能有脏数据(如用户删号后没清理 Set),要以数据库为准做校验 - 写回时用
INSERT ... ON DUPLICATE KEY UPDATE,主键设为(article_id, user_id),避免重复插入或丢失取消动作 - 定时任务别卡死:用
SCAN游标分批读article:liked_by:*,每次最多取 1000 个 key,防止 Redis 阻塞 - 记录最后同步时间戳到 MySQL 表里,下次只扫新增的点赞 key,别全量扫
示例 SQL:
INSERT INTO article_like (article_id, user_id, created_at) VALUES (?, ?, NOW()) ON DUPLICATE KEY UPDATE updated_at = NOW();
为什么不能只靠 Redis 持久化
Redis 是缓存,不是数据库。哪怕开了 AOF + RDB,以下情况仍会丢数据:
- 服务器断电且 AOF 未刷盘(默认每秒刷一次,可能丢 1 秒数据)
- 内存淘汰策略触发
maxmemory-policy,误删了点赞 key(尤其用allkeys-lru时) - 运维误操作
FLUSHALL或配置错误导致 DB 编号错乱
所以点赞关系表(article_like)必须存在,且字段要有 created_at 和 updated_at,用于对账和补漏 —— 这不是备胎,是底线。

