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/s50ms
Redis INCR约 8000 ops/s0.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 命令为什么是原子操作?底层原理是什么?

参考答案(3 个标签)
RedisINCR原子性

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 次?

参考答案(3 个标签)
Redis限流INCR

限流器实现方案

方案 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 数据结构有哪些常用命令?分别适用于点赞场景的哪些功能?

参考答案(3 个标签)
RedisSet数据结构

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

如何实现”批量点赞”功能,用户可以一次性对多篇文章点赞?

参考答案(3 个标签)
Redis批量操作Pipeline

批量点赞实现方案

方案 1:循环调用

方案 2:Redis Pipeline

方案 3:Lua 脚本(最佳)

性能对比

批量点赞 20 篇文章:

方案 1(循环调用):
- 网络往返:20 次
- 总耗时:约 100ms

方案 2(Pipeline):
- 网络往返:2 次
- 总耗时:约 10ms
- 提升:10 倍

方案 3(Lua 脚本):
- 网络往返:1 次
- 总耗时:约 5ms
- 提升:20 倍

批量取消点赞

应用场景

批量点赞功能:
- 用户浏览文章列表,一键点赞所有感兴趣的文章
- 管理员批量操作
- 数据迁移/导入

批量取消点赞:
- 用户取消所有点赞
- 清理测试数据

练习 5

如何设计 Redis 的 key 命名规范,便于管理和维护?

参考答案(3 个标签)
RedisKey命名最佳实践

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 监控和维护

最佳实践总结

  1. 使用冒号分隔层级
  2. Key 名称包含业务、对象、ID、属性
  3. 定义 Key 前缀常量
  4. 使用工厂方法创建 Key
  5. 为短期数据设置过期时间
  6. 定期监控和清理 Key
  7. 避免 Key 名称过长(影响内存和性能)
  8. 团队内统一命名规范

思考题

  1. 如果 Redis 内存不足,如何优化点赞数据的存储?

  2. 如何实现”点赞数据迁移”,从数据库迁移到 Redis?

  3. 如果需要支持”点赞数据分析”(如每小时点赞趋势),如何设计 Redis 数据结构?

💡 提示:这些问题没有标准答案,建议结合实际情况深入思考。