一个小小的签到功能,到底用 MySQL 还是 Redis?

📅 发布时间:2026/7/6 10:20:17 👁️ 浏览次数:
一个小小的签到功能,到底用 MySQL 还是 Redis?
1. 引言签到功能几乎是现代互联网应用的标配从电商APP的每日签到领积分到学习社区的连续打卡记录再到健身应用的锻炼打卡。这个看似简单的功能背后却涉及数据存储、高并发处理、连续签到计算等一系列技术决策。其中最基础也是最核心的问题就是数据该存哪里开发者常常在关系型数据库 MySQL 和内存数据库 Redis 之间犹豫。MySQL 稳定可靠、支持复杂查询Redis 性能极高、数据结构灵活。本文将围绕签到功能从需求分析出发深入探讨两种存储方案的实现细节、优缺点对比并给出不同场景下的最佳实践全文约 2 万字希望能帮助你彻底理清这个“小功能”背后的“大学问”。2. 签到功能的核心需求分析在设计签到系统之前我们需要明确它通常要满足哪些业务需求。一个典型的签到功能包括2.1 基础功能用户签到用户每天只能签到一次重复签到需进行防重处理。签到记录记录用户的签到时间通常精确到天。签到历史查询用户可以查看自己某个月或某段时间的签到情况如日历形式。连续签到统计计算用户当前连续签到的天数用于发放奖励。积分/奖励机制根据连续签到天数给予不同积分或虚拟物品。2.2 非功能性需求高并发签到通常发生在特定时间点如凌晨0点容易产生瞬时高并发。数据一致性签到不可丢失积分发放需准确。扩展性随着用户量增长系统应能水平扩展。成本存储成本和开发运维成本需在可控范围。这些需求将直接影响我们对数据库的选择。3. 使用 MySQL 实现签到功能MySQL 作为传统关系数据库凭借其事务支持和丰富的查询能力是许多项目的首选。下面我们逐步分析如何用 MySQL 实现签到功能。3.1 表结构设计最简单的签到表设计如下sqlCREATE TABLE user_sign ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT 用户ID, sign_date date NOT NULL COMMENT 签到日期, points int(11) DEFAULT 0 COMMENT 本次签到获得的积分, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_date (user_id,sign_date), KEY idx_user_id (user_id), KEY idx_sign_date (sign_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;说明使用联合唯一索引uk_user_date保证每个用户每天只能签到一次。索引idx_user_id和idx_sign_date用于加速查询。points字段记录本次签到获得的积分可根据连续规则动态计算。3.2 签到操作用户签到时需要插入一条记录。但由于唯一索引的存在重复签到会抛出异常因此通常使用INSERT ... ON DUPLICATE KEY UPDATE或先查询再插入。3.2.1 插入或忽略sqlINSERT IGNORE INTO user_sign (user_id, sign_date, points) VALUES (123, CURDATE(), 5);INSERT IGNORE会在违反唯一约束时静默忽略返回影响行数为0。程序可通过影响行数判断是否签到成功。3.2.2 先检查后插入事务为了保证积分计算的正确性有时需要先查询当天是否已签到再决定是否插入同时计算连续天数和积分。sqlSTART TRANSACTION; -- 检查今日是否已签到 SELECT id FROM user_sign WHERE user_id 123 AND sign_date CURDATE() FOR UPDATE; -- 如果不存在则插入 INSERT INTO user_sign (user_id, sign_date, points) VALUES (123, CURDATE(), ?); COMMIT;使用FOR UPDATE加行锁可以防止并发插入但会降低并发性能。对于签到场景大多数情况下INSERT IGNORE足够。3.3 查询签到历史要展示某个月的签到日历可以查询该月所有签到记录sqlSELECT sign_date FROM user_sign WHERE user_id 123 AND sign_date BETWEEN 2025-02-01 AND 2025-02-28 ORDER BY sign_date;3.4 计算连续签到天数连续签到是签到系统的核心难点。MySQL 中计算连续签到有多种方法下面介绍两种常用方案。3.4.1 使用窗口函数MySQL 8.0假设我们要计算截至今天2025-02-16的连续签到天数。思路是找到最近一次未签到的日期然后用今天减去那个日期。sqlWITH user_signs AS ( SELECT sign_date, LAG(sign_date, 1, DATE_SUB(CURDATE(), INTERVAL 1 DAY)) OVER (ORDER BY sign_date) AS prev_date FROM user_sign WHERE user_id 123 AND sign_date CURDATE() ), gaps AS ( SELECT sign_date, CASE WHEN DATEDIFF(sign_date, prev_date) 1 THEN 1 ELSE 0 END AS is_gap FROM user_signs ) SELECT DATEDIFF(CURDATE(), MAX(sign_date)) 1 AS continuous_days FROM gaps WHERE is_gap 0 AND sign_date ( SELECT MAX(sign_date) FROM gaps WHERE is_gap 1 );逻辑比较复杂实际应用中更常见的是在业务层通过循环或利用签到表数据计算。3.4.2 业务层计算查询用户最近一段时间的签到记录比如最近30天。从今天往前遍历直到发现断签。计算连续天数。示例代码PHPphp$today date(Y-m-d); $rows $db-query(SELECT sign_date FROM user_sign WHERE user_id 123 AND sign_date DATE_SUB($today, INTERVAL 30 DAY) ORDER BY sign_date DESC)-fetchAll(); $continuous 0; $currentDate $today; foreach ($rows as $row) { if ($row[sign_date] $currentDate) { $continuous; $currentDate date(Y-m-d, strtotime($currentDate -1 day)); } else { break; } }这种方法简单直接但在用户连续签到很长时如几百天查询数据量可能较大但一般签到记录不会太多一年也就365条性能可接受。3.5 优化建议分区表按用户ID哈希或按日期范围分区提高查询和归档效率。读写分离签到写多读少可考虑主从复制将查询分散到从库。缓存连续签到结果可缓存在 Redis 中减少数据库压力。3.6 MySQL 实现签到功能的优缺点优点事务支持保证积分、奖励的一致性。复杂查询可方便地统计历史签到、排行等。成熟稳定备份恢复、监控工具丰富。数据持久化无需担心数据丢失。缺点写入性能每次签到都是一次插入随着记录数增长索引维护成本增加高并发下可能成为瓶颈。存储成本每条记录至少包含用户ID、日期等字段占用空间较大。连续计算复杂需要编写复杂 SQL 或业务逻辑且随着数据量增大效率下降。4. 使用 Redis 实现签到功能Redis 基于内存支持多种数据结构尤其适合计数器、位图等场景。利用 Redis 的 BITMAP 可以极高效地存储签到状态。4.1 为什么选择 BitmapBitmap位图是一种通过位操作来标记状态的数据结构。在签到场景中每个用户一年的签到状态仅需 365 个比特约 46 字节存储成本极低且支持快速位运算。4.2 Redis Bitmap 核心命令SETBIT key offset value设置 key 的第 offset 位为 0 或 1。GETBIT key offset获取第 offset 位的值。BITCOUNT key [start end]统计 key 中值为 1 的位数。BITFIELD key [GET type offset ...]对位图进行多字节操作适合获取连续位。BITOP operation destkey key [key ...]对多个位图进行与、或、非、异或操作。4.3 签到功能实现4.3.1 存储设计方案一每个用户一个 key以日期为偏移量key 格式sign:{user_id}:{yyyyMM}offset当月第几天从 0 或 1 开始value1 表示签到0 表示未签例如用户 123 在 2025 年 2 月的签到记录存储在 keysign:123:202502中2 月 1 日对应 offset 02 月 2 日对应 offset 1依此类推。优点内存占用极小每月约 31 位不到 4 字节查询某月签到只需操作单个 key。方案二每个日期一个 key以用户 ID 为偏移量key 格式sign:{yyyyMMdd}offset用户 ID假设用户 ID 为连续数字或可映射value1 表示该用户当天签到这种方案适合需要统计当天签到人数、用户签到排行等场景但用户 ID 不连续时浪费空间且每个 key 的大小与最大用户 ID 相关可能极大。通常采用方案一。4.3.2 签到操作用户签到假设以月为单位存储bash# 计算当天是该月第几天假设 offset 从 0 开始 offset day_of_month - 1 # 执行 setbit SETBIT sign:123:202502 $offset 1 # 判断是否签到成功返回旧值0 表示之前未签1 表示已签若返回 0说明签到成功返回 1说明重复签到。4.3.3 查询某月签到记录获取整个月的签到状态bash# 获取 2 月所有签到位返回二进制字符串 BITFIELD sign:123:202502 GET u31 0得到的结果是一个 31 位的整数通过位运算或程序解析即可知道每天的签到情况。如果需要列出具体签到日期可以遍历每个 offset 使用GETBIT或者用 Lua 脚本一次性获取。4.3.4 计算连续签到天数计算截至今天的连续签到天数需要从当前 offset 向前遍历直到遇到 0 为止。方法一循环调用 GETBITbash# 伪代码 offset today_offset continuous 0 while offset 0: if GETBIT sign:123:202502 $offset 1: continuous offset-- else: break这种方式需要多次网络请求效率低可以使用 Lua 脚本优化。方法二Lua 脚本lua-- KEYS[1]: key -- ARGV[1]: today_offset (0-based) local offset tonumber(ARGV[1]) local count 0 while offset 0 do if redis.call(getbit, KEYS[1], offset) 1 then count count 1 offset offset - 1 else break end end return count调用EVAL script 1 sign:123:202502 15假设今天是 2 月 16 日offset 15方法三使用 BITFIELD 获取多个位BITFIELD可以一次获取多个位例如从 0 到 today_offset然后交给程序计算连续 1 的个数。但由于 Redis 返回的是整数需要解析位模式可能较复杂。4.3.5 跨月连续签到当连续签到跨月时需要处理上个月的数据。例如今天是 3 月 1 日需要检查 2 月最后几天是否签到。一种思路在 Lua 脚本中如果当前月的 offset 已经减到 -1则切换到上个月的 key继续计算直到找到断点或达到某个上限如最大连续天数限制。4.4 Redis 持久化与数据可靠性Redis 是内存数据库虽然提供 RDB 和 AOF 持久化但仍可能丢失少量数据取决于配置。对于签到数据丢失可能导致用户权益受损因此需权衡。RDB定期快照两次快照之间的数据可能丢失。AOF每秒同步everysec最多丢失 1 秒数据可接受吗混合持久化Redis 4.0 引入结合 RDB 和 AOF。若对可靠性要求极高可考虑 Redis 配合 MySQL 双写先写 Redis 成功返回给用户再异步写入 MySQL 作为持久化备份。4.5 Redis 实现签到功能的优缺点优点极高读写性能基于内存单机 QPS 可达 10 万。存储空间极小每个用户每年仅需约 46 字节。原子操作SETBIT是原子操作天然防并发。丰富位运算可快速统计月签到次数、多个用户交集等。缺点持久化风险依赖持久化配置极端情况可能丢数据。跨月连续计算稍复杂需要 Lua 脚本处理多 key。内存占用随用户数线性增长但相对传统存储依然小得多。缺乏事务支持但 Lua 脚本可保证原子性。5. MySQL 与 Redis 详细对比维度MySQLRedis (Bitmap)读写性能单机几千 QPS需索引写操作涉及磁盘 I/O单机 10万 QPS纯内存操作存储成本每条记录约几十字节一年 365 条 ≈ 十几 KB每个用户每年约 46 字节每月约 4 字节数据持久化通过事务、binlog 保证强一致通过 RDB/AOF 保证最终一致可能丢数据复杂查询支持 SQL可任意聚合、统计有限只能进行位运算需在客户端处理连续签到计算可通过 SQL 或业务层实现效率一般通过 Lua 脚本极快并发控制通过锁或事务可能影响性能原子操作天然支持高并发扩展性可通过分库分表扩展运维复杂可通过 Redis Cluster 扩展但跨节点计算复杂开发维护成本表结构设计、SQL 优化、主从复制等数据结构设计、Lua 脚本、内存监控典型适用场景中小项目需要复杂统计数据一致性要求高高并发签到需要快速连续统计可容忍数据短暂丢失6. 实际应用场景建议6.1 小型项目或早期阶段用户量少10万并发低可使用 MySQL 简单实现。优点开发快无需引入额外组件数据绝对可靠。示例个人博客的每日签到、内部工具。6.2 中大型高并发项目用户量百万级以上特别是零点峰值明显推荐使用 Redis 作为主要存储。优点扛住高并发节省存储成本。需配合持久化策略并考虑双写到 MySQL 进行备份。6.3 混合方案最佳实践结合两者优点实现高性能与可靠性并存写入路径用户签到 → 先写 RedisSETBIT成功 → 返回签到成功。同时将签到消息发送到消息队列如 Kafka/RabbitMQ。消费者异步将签到记录写入 MySQL。读取路径查询当天签到状态直接读 Redis。查询历史签到或连续天数优先读 Redis若数据完整若 Redis 无数据则回查 MySQL并回填 Redis。数据一致性Redis 中的数据可设置过期时间如保留最近 3 个月确保热数据在内存。MySQL 作为最终数据源用于对账和离线统计。这种架构既保证了签到接口的低延迟又保证了数据最终一致。6.4 纯 Redis 方案如何保证数据不丢若决定完全依赖 Redis可采取以下措施开启 AOF 持久化并设置appendfsync everysec最多丢失 1 秒数据。使用 Redis 集群避免单点故障。定期将 Redis 数据导出备份例如用SCAN遍历所有签到 key存入 MySQL 或对象存储。但必须接受在极端故障如机器断电下最后 1 秒的签到可能丢失。对于签到功能通常用户可接受偶尔补签但需提前告知。7. 深入探讨连续签到算法的更多细节7.1 跨月/跨年连续计算Redis 方案中跨月连续需要合并两个月的数据。假设今天是 3 月 2 日需要判断是否连续到 2 月。我们可以写一个 Lua 脚本参数为当前年月和 offset脚本内先计算当月若当月第一天为 1 且 offset 已到 0则加载上个月的 key 继续。考虑性能可以限制最大连续天数如 365 天避免无限循环。7.2 时区处理签到通常以自然天为单位但服务器时区可能与用户不一致。解决方案统一使用 UTC 存储但按用户时区转换。Redis key 中包含日期时需用用户所在时区的日期。例如中国用户使用北京时间YYYYMMDD。如果使用 UTC 日期凌晨 0 点 UTC 可能对应当地时间的上午 8 点导致用户预期不符。因此强烈建议按用户当地时间记录。7.3 签到补签与漏签有些业务允许补签消耗道具。实现时Redis 可以直接SETBIT修改过去的位但需注意连续天数重新计算。若补签导致连续应在业务层更新连续天数缓存。7.4 积分发放规则积分通常与连续天数挂钩如连续第 1 天 1 分第 2 天 2 分第 3 天 3 分超过 7 天每天 7 分。计算积分可在签到后立即计算并记录到积分流水表MySQL 或 Redis hash。由于积分涉及账户余额建议最终落地到 MySQL保证数据一致性。8. 总结回到最初的问题“一个小小的签到功能到底用 MySQL 还是 Redis”答案不是绝对的取决于你的业务规模、并发要求、团队技术栈和数据一致性需求。如果你在做一个个人项目或初期产品用户量小MySQL 完全足够简单可靠。如果你在做一个百万级日活的应用并且预计签到会成为高频接口Redis 是更优选择能轻松应对高并发且存储成本极低。如果你追求极致性能和可靠性兼备可以采用 Redis MySQL 的混合架构各取所长。