INCR 命令
性能瓶颈
数据库方案上线两周后,我遇到了第一个真正的挑战。
那天是周五晚上,平台推送了一篇爆款文章:《创业者的一天》。
数据飙升:
文章发布时间:18:00
发布后 1 小时:500 阅读,50 点赞
发布后 2 小时:2000 阅读,300 点赞
发布后 3 小时:5000 阅读,800 点赞
发布后 4 小时:10000 阅读,1500 点赞
峰值:每秒约 50 个点赞请求!
系统报警:
【监控报警】数据库 CPU 使用率超过 80%
【监控报警】数据库连接数接近上限
【监控报警】慢查询数量增加
我赶紧查看数据库监控:
MySQL 状态:
- CPU:85%
- 连接数:180/200
- QPS:1500
- 平均响应时间:200ms
慢查询日志:
- INSERT INTO article_likes:平均 50ms
- UPDATE articles SET like_count...:平均 150ms
数据库扛不住了!
为什么数据库慢?
我分析了原因:
点赞操作:
1. INSERT INTO article_likes (article_id, user_id)
2. UPDATE articles SET like_count = like_count + 1
瓶颈分析:
- INSERT 操作:需要检查唯一索引,写入磁盘
- UPDATE 操作:需要锁定记录,写入磁盘
- 磁盘 IO:每次操作都需要写磁盘
- 锁竞争:大量并发 UPDATE 导致锁等待
核心问题:磁盘 IO 和锁竞争。
我在笔记本上画了个图:
用户请求 → 应用服务器 → MySQL(磁盘)
瓶颈:
1. 每次点赞都要写磁盘
2. 磁盘写入速度:约 100 IOPS
3. 50 个并发请求 → 排队等待
如果改用内存:
- 内存读写速度:约 100,000 OPS
- 性能提升:1000 倍!
解决方案:使用 Redis(内存数据库)。
Redis INCR 命令
初识 INCR
我打开 Redis 官方文档,找到了 INCR 命令:
INCR key
将 key 中存储的数字值增一。
如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
时间复杂度:O(1)
测试一下:
验证要点
- 命令只用于验证系统状态,读者不需要记具体参数。
完美!一个命令就能完成计数!
INCR 的特性
我研究了 INCR 的关键特性:
特性 1:原子性
INCR 是原子操作:
- 单线程执行,不需要锁
- 不会被其他命令打断
- 高并发下也能正确计数
对比 MySQL:
- MySQL UPDATE 需要锁行
- 高并发时锁等待严重
- 性能下降明显
特性 2:O(1) 时间复杂度
INCR 时间复杂度:O(1)
无论计数器有多大,执行时间都是常数:
- 计数器值为 100:执行时间约 0.1ms
- 计数器值为 1000000:执行时间还是约 0.1ms
对比 MySQL COUNT:
- COUNT(*) 时间复杂度:O(n)
- 点赞数越多,查询越慢
特性 3:内存操作
Redis 数据存储在内存中:
- 内存读写速度:纳秒级
- 磁盘读写速度:毫秒级
- 性能差异:1000 倍以上
性能测试
我用 Redis 做了压力测试:
对比数据库:
| 方案 | 吞吐量 | 平均响应时间 |
|---|---|---|
| MySQL UPDATE | 约 200 ops/s | 50ms |
| Redis INCR | 约 8000 ops/s | 0.1ms |
| 性能提升 | 40 倍 | 500 倍 |
我决定立刻引入 Redis!
点赞功能实现
数据结构设计
我在 Redis 中设计了两种数据结构:
为什么需要两种结构?
计数器(like_count):
- 快速获取点赞总数
- INCR/DECR 操作,性能极高
点赞集合(liked_users):
- 判断用户是否点赞
- SADD/SREM/SISMEMBER 操作
- 防止重复点赞
点赞接口实现
取消点赞接口
获取点赞数
判断是否点赞
MySQL 方案:
- 点赞接口:平均 50ms
- 获取点赞数:平均 100ms(需要 COUNT)
- 判断是否点赞:平均 80ms
Redis 方案:
- 点赞接口:平均 0.5ms
- 获取点赞数:平均 0.1ms
- 判断是否点赞:平均 0.1ms
提升倍数:100 倍!
### 吞吐量
压力测试:1000 个并发用户点赞同一篇文章
MySQL:
- 最大吞吐量:约 200 ops/s
- 响应时间:随着并发增加,从 50ms 上升到 500ms+
Redis:
- 最大吞吐量:约 8000 ops/s
- 响应时间:始终保持在 1ms 以下
提升倍数:40 倍!
### 系统负载
MySQL CPU:
- 优化前:85%
- 优化后:15%(只处理其他业务)
Redis CPU:
- 约 5%
服务器资源:
- MySQL 压力大幅降低
- Redis 资源占用很小
- 整体系统更稳定
## 实际效果
### 爆款文章测试
我用 Redis 方案重新测试了爆款文章:
文章:《创业者的一天》 发布时间:周五 18:00
数据:
- 4 小时内:10000 阅读,1500 点赞
- 峰值:每秒 50 个点赞请求
Redis 监控:
- CPU:5%
- 内存:约 10MB
- 响应时间:平均 0.3ms
系统状态:
- 无报警
- 无慢查询
- 用户体验流畅
**完美!系统轻松应对爆款文章。**
### 用户反馈
用户 A:“点赞秒响应,体验太好了!” 用户 B:“之前点赞要等好几秒,现在瞬间完成” 作者 C:“看到点赞数快速增加,很有成就感” “
新的问题
虽然 Redis 性能很好,但我开始担心一些问题:
问题 1:数据持久化
Redis 数据存储在内存中:
- 如果 Redis 宕机,数据会丢失吗?
- 如果服务器断电,点赞数还能恢复吗?
我需要数据持久化方案。
问题 2:数据同步
Redis 和数据库的数据如何同步?
- 用户点赞后,Redis 计数 +1
- 数据库的 article_likes 表怎么办?
- 如何保证数据一致性?
问题 3:内存限制
Redis 内存有限:
- 如果点赞数越来越多,内存够吗?
- 10000 篇文章 × 每篇平均 1000 点赞 × 每个记录 100 字节
- 大约需要 1GB 内存
- 如果增长到 10 倍?100 倍?
问题 4:冷启动
系统重启后,Redis 数据如何初始化?
- 从数据库加载所有点赞数?
- 10000 篇文章,需要多久?
- 启动时间会很长吗?
课后练习
练习 1
Redis INCR 命令为什么是原子操作?底层原理是什么?
Redis INCR 的原子性原理:
原因:Redis 是单线程执行命令
Redis 执行模型:
1. 所有命令在单个线程中顺序执行
2. 一个命令执行期间,不会被其他命令打断
3. 不需要锁机制,天然保证原子性
对比多线程数据库:
- MySQL:多线程,需要锁来保证原子性
- Redis:单线程,天然原子性底层实现:
为什么单线程也能高性能?
Redis 性能分析:
纯内存操作:
- 内存读写速度:纳秒级
- 无磁盘 IO 等待
单线程优势:
- 无锁竞争开销
- 无上下文切换开销
- 无多线程同步复杂度
高效 IO 多路复用:
- epoll/kqueue 等机制
- 一个线程处理多个连接
- 不会阻塞等待网络 IO
结论:单线程 + 内存 + IO 多路复用 = 极高性能INCRBY 命令:
验证要点
- 命令只用于验证系统状态,读者不需要记具体参数。
原子性保证范围:
单命令原子性:
- INCR、INCRBY、DECR、DECRBY:都是原子操作
- SET、GET:也是原子操作
- SADD、SREM:原子操作
多命令原子性:
- 需要使用 MULTI/EXEC(事务)
- 或使用 Lua 脚本
示例:非原子性的多命令
127.0.0.1:6379> GET counter
"100"
127.0.0.1:6379> INCR counter
"101"
问题:GET 和 INCR 之间,可能有其他客户端修改 counter练习 2
如何用 Redis 实现”限流器”功能,限制用户每分钟最多点赞 10 次?
限流器实现方案:
方案 1:INCR + EXPIRE
问题:INCR 和 EXPIRE 不是原子操作
并发问题:
T1: GET key → None
T2: GET key → None(同时)
T1: SET key 1 EX 60
T2: SET key 1 EX 60(覆盖了 T1 的设置)
解决方案:使用 Lua 脚本方案 2:Lua 脚本(原子性)
方案 3:使用 INCR 的原子特性
问题:INCR 和 EXPIRE 不是原子操作,可能出错
场景:
T1: INCR key → 1
T1: (准备设置 EXPIRE)
Redis 宕机!
结果:key 存在,但没有过期时间,永久存储最佳方案:Lua 脚本或使用 SET + NX + EX
滑动窗口限流(更精确)
对比总结:
| 方案 | 精确度 | 原子性 | 实现复杂度 |
|---|---|---|---|
| INCR + EXPIRE | 固定窗口 | 非原子 | 低 |
| Lua 脚本 | 固定窗口 | 原子 | 中 |
| 滑动窗口 | 滑动窗口 | 原子 | 高 |
推荐:一般场景使用 Lua 脚本(固定窗口),严格场景使用滑动窗口。
练习 3
Redis 的 Set 数据结构有哪些常用命令?分别适用于点赞场景的哪些功能?
Redis Set 常用命令:
| 命令 | 功能 | 时间复杂度 | 点赞场景应用 |
|---|---|---|---|
| SADD | 添加元素 | O(1) | 用户点赞 |
| SREM | 移除元素 | O(1) | 取消点赞 |
| SISMEMBER | 判断元素是否存在 | O(1) | 检查是否点赞 |
| SMEMBERS | 获取所有元素 | O(n) | 获取点赞用户列表 |
| SCARD | 获取元素数量 | O(1) | 获取点赞用户数 |
| SPOP | 随机移除元素 | O(1) | 随机抽奖 |
| SRANDMEMBER | 鎷取随机元素 | O(1) | 随机抽奖 |
点赞场景应用:
Set 的优势:
1. 自动去重:
- 一个用户对一篇文章只能点赞一次
- Set 自动保证唯一性
2. 高性能:
- O(1) 时间复杂度的添加、删除、查找
- 适合高频操作
3. 集合运算:
- 可以计算交集、并集、差集
- 适合复杂业务场景集合运算示例:
注意事项:
SMEMBERS 时间复杂度:O(n)
问题:如果 Set 包含大量元素,获取所有元素会很慢
解决方案:
1. 使用 SSCAN 分批获取
2. 只在需要时获取,不频繁调用
3. 限制点赞用户数量(如最多显示前 1000 个)练习 4
如何实现”批量点赞”功能,用户可以一次性对多篇文章点赞?
批量点赞实现方案:
方案 1:循环调用
方案 2:Redis Pipeline
方案 3:Lua 脚本(最佳)
性能对比:
批量点赞 20 篇文章:
方案 1(循环调用):
- 网络往返:20 次
- 总耗时:约 100ms
方案 2(Pipeline):
- 网络往返:2 次
- 总耗时:约 10ms
- 提升:10 倍
方案 3(Lua 脚本):
- 网络往返:1 次
- 总耗时:约 5ms
- 提升:20 倍批量取消点赞:
应用场景:
批量点赞功能:
- 用户浏览文章列表,一键点赞所有感兴趣的文章
- 管理员批量操作
- 数据迁移/导入
批量取消点赞:
- 用户取消所有点赞
- 清理测试数据练习 5
如何设计 Redis 的 key 命名规范,便于管理和维护?
Redis Key 命名规范:
基本原则:
1. 可读性:key 名称应该清晰易懂
2. 结构化:使用分隔符组织层级
3. 唯一性:避免 key 冲突
4. 简洁性:key 名称不要太长
5. 一致性:团队内统一规范推荐格式:
格式:{业务}:{对象}:{ID}:{属性}
示例:
- 点赞计数:article:123:like_count
- 点赞集合:article:123:liked_users
- 用户信息:user:1001:profile
- 会话信息:session:abc123:data点赞场景 Key 设计:
Key 前缀管理:
批量 Key 管理:
Key 过期时间管理:
Key 监控和维护:
最佳实践总结:
- 使用冒号分隔层级
- Key 名称包含业务、对象、ID、属性
- 定义 Key 前缀常量
- 使用工厂方法创建 Key
- 为短期数据设置过期时间
- 定期监控和清理 Key
- 避免 Key 名称过长(影响内存和性能)
- 团队内统一命名规范
思考题
-
如果 Redis 内存不足,如何优化点赞数据的存储?
-
如何实现”点赞数据迁移”,从数据库迁移到 Redis?
-
如果需要支持”点赞数据分析”(如每小时点赞趋势),如何设计 Redis 数据结构?
💡 提示:这些问题没有标准答案,建议结合实际情况深入思考。
