Redis详解以应用场景
一、Redis简介
1.1 什么是Redis
Redis = Remote Dictionary Service(远程字典服务)
传统数据库: 数据存储在磁盘,访问需要磁盘IO └── 速度慢,但数据持久化 Redis: 所有数据存储在内存 └── 速度极快,断电丢失Redis的核心特征:
- 内存数据库:所有数据在内存中,访问速度极快
- KV数据库:通过Key查找Value,键值对存储
- 数据结构数据库:Value可以是多种数据结构,不只是字符串
1.2 支持的数据结构
| 数据结构 | 说明 | 典型场景 |
|---|---|---|
| String | 字符串 | 缓存、计数器、分布式锁 |
| List | 双向链表 | 队列、消息队列、最新列表 |
| Hash | 哈希表 | 存储对象、购物车 |
| Set | 无序集合 | 好友关系、抽奖 |
| Zset | 有序集合 | 排行榜 |
二、String类型
2.1 常用命令
| 命令 | 说明 | 示例 |
|---|---|---|
| SET key value | 设置值 | SET name zhangsan |
| GET key | 获取值 | GET name |
| INCR key | 原子+1 | INCR count |
| INCRBY key n | 原子+n | INCRBY count 10 |
| DECR key | 原子-1 | DECR count |
| DECRBY key n | 原子-n | DECRBY count 5 |
| SETNX key value | key不存在才设置 | SETNX lock 1 |
| DEL key | 删除 | DEL name |
| SETBIT key offset 0/1 | 设置位 | SETBIT sign 5 1 |
| GETBIT key offset | 获取位 | GETBIT sign 5 |
| BITCOUNT key | 统计1的个数 | BITCOUNT sign |
2.2 存储对象(JSON)
// 用户对象{"name":"zhangsan","age":18,"city":"beijing"}SET user:1 '{"name":"zhangsan","age":18,"city":"beijing"}' GET user:1适用场景:
- 对象结构稳定,不需要频繁修改字段
- 整个对象一起读取
不适用场景:
- 对象某个字段需要频繁修改
- 需要对字段单独操作
2.3 计数器场景
场景:统计访问量、点赞数、播放量
INCR video:play:1001# 播放量+1INCR user:login:20240101# 今日登录次数+1DECR stock:1001# 库存-1DECRBY account:1001100# 账户扣减100为什么用INCR不用GET+SET?
错误方式: count = GET cnt count = count + 1 SET cnt count # 问题:两个请求同时读到count=10,都变成11,丢失一次 正确方式: INCR cnt # Redis保证原子性,不会丢失2.4 分布式锁场景
分布式锁的核心操作:
- 加锁:确保只有一个客户端能获取锁
- 释放锁:只能释放自己加的锁
加锁命令:
SET lock uuid NX EX30SET lock uuid NX EX 30 │ │ │ │ └─ 30秒自动过期,防止死锁 │ │ │ └───── 设置过期时间 │ │ └──────── 只有key不存在时才设置 │ └───────────── uuid作为唯一标识 └───────────────── 锁的名称释放锁命令(Lua脚本保证原子性):
ifGET(lock)==uuidthenDEL(lock)end完整流程:
客户端A: 客户端B: SET lock A NX EX 30 -> OK SET lock B NX EX 30 -> 失败(已存在) 业务操作... 等待... DEL lock -> OK ... DEL lock -> 失败(已超时,A已自动删除)SETNX的排他功能:
SETNX lock 1 -> 成功(返回1),获取锁 SETNX lock 1 -> 失败(返回0),锁已被占用2.5 位运算场景
场景:月签到统计
思路: - 一个月31天,用一个字符串表示 - 第N天签到,SETBIT key 30-N 1 - 统计 BITCOUNT key 得到签到天数日期:2024年1月 第1天签到:SETBIT sign:202401 30 1 # 索引30 第5天签到:SETBIT sign:202401 26 1 # 索引26 第10天签到:SETBIT sign:202401 21 1 # 索引21 BITCOUNT sign:202401 # 返回3,本月签到3天位图结构:
索引:31 30 29 28 27 26 25 ... 22 21 ... 2 1 0 第1天: 0 1 0 0 0 0 0 ... 0 0 ... 0 0 0 第5天: 0 1 0 0 0 1 0 ... 0 0 ... 0 0 0 第10天:0 1 0 0 0 1 0 ... 0 1 ... 0 0 0 BITCOUNT = 3(签到3天)优势:
- 31天只用4字节,内存极省
- BITCOUNT O(n) 统计很快
- 适合海量用户签到统计
三、List类型
3.1 常用命令
| 命令 | 说明 | 示例 |
|---|---|---|
| LPUSH key value | 左插入 | LPUSH queue task1 |
| RPUSH key value | 右插入 | RPUSH queue task2 |
| LPOP key | 左弹出 | LPOP queue |
| RPOP key | 右弹出 | RPOP queue |
| LRANGE key start end | 范围查询 | LRANGE queue 0 -1 |
| LREM key count value | 删除元素 | LREM queue 1 task1 |
| BRPOP key timeout | 阻塞右弹出 | BRPOP queue 0 |
| BLPOP key timeout | 阻塞左弹出 | BLPOP queue 0 |
| LTRIM key start end | 裁剪保留范围 | LTRIM queue 0 99 |
3.2 队列与栈
队列(FIFO - 先进先出):
LPUSH RPOP 起点 ──────────────────────────────── 终点 [A] [B] [C] [D] [E] [A] [B] [C] [D] [E] │ │ └───────────────────────────┘ RPUSH + LPOP 也能实现栈(FILO - 先进后出):
LPUSH + LPOP 或 RPUSH + RPOP [A] [B] [C] LPOP -> [C] -> [A] [B] LPOP -> [B] -> [A] LPOP -> [A]3.3 阻塞队列
普通队列的问题:
生产者:LPUSH task # 放入任务 消费者:RPOP queue # 取出任务 │ └── 如果队列空,RPOP返回nil,消费者需要轮询阻塞队列解决方案:
消费者:BRPOP queue 0 │ └── 队列空时阻塞,0表示永远等待 └── 有任务时立即返回 └── 超时则返回nil超时时间的作用:
BRPOP queue 10 │ └── 阻塞最多10秒 └── 10秒内有任务就返回 └── 10秒内没任务返回nil(防止永远阻塞)3.4 异步消息队列
生产者系统:
RPUSH mq:orders order_id_1 RPUSH mq:orders order_id_2 RPUSH mq:orders order_id_3消费者系统:
BRPOP mq:orders 0 # 阻塞等待,收到 order_id_1 处理任务... BRPOP mq:orders 0 # 收到 order_id_2 处理任务...特点:
- 不同系统间通过Redis解耦
- 生产者只管发,消费者只管收
- 支持多个消费者,实现负载均衡
3.5 最新列表窗口
场景:展示最新商品、最新评论
用户发表新评论: LPUSH comments:user:1 comment_123 LTRIM comments:user:1 0 9 # 只保留最新10条 展示最新10条: LRANGE comments:user:1 0 9流程:
LTRIM key 0 9 的效果: 插入前:[c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11] │ ▼ 裁剪后:[c1 c2 c3 c4 c5 c6 c7 c8 c9 c10] # c11被删除 每次新数据进来,旧数据自动淘汰四、Hash类型
4.1 常用命令
| 命令 | 说明 | 示例 |
|---|---|---|
| HSET key field value | 设置字段 | HSET user:1 name zhangsan |
| HGET key field | 获取字段 | HGET user:1 name |
| HMSET key f1 v1 f2 v2 | 批量设置 | HMSET user:1 age 18 city bj |
| HMGET key field1 field2 | 批量获取 | HMGET user:1 name age |
| HINCRBY key field n | 字段原子+n | HINCRBY user:1 age 1 |
| HLEN key | 获取字段数量 | HLEN user:1 |
| HDEL key field | 删除字段 | HDEL user:1 name |
4.2 存储对象 vs String存储对象
String存储(整个对象):
SET user:1 '{"name":"zhangsan","age":18,"city":"beijing"}' │ ├── 优点:简单,整个对象一起操作 │ └── 缺点: ├── 修改单个字段需要:GET -> 解JSON -> 修改 -> SET ├── 无法单独对某个字段做原子操作 └── 字段多的对象解析开销大Hash存储(字段级操作):
HSET user:1 name zhangsan HSET user:1 age 18 HSET user:1 city beijing │ ├── 优点: │ ├── 修改单个字段:HSET user:1 age 19 │ ├── 原子操作:HINCRBY user:1 age 1 │ └── 不用解析JSON,直接操作字段 │ └── 缺点: └── 字段不能太多(hash结构有上限)对比:
| 操作 | String | Hash |
|---|---|---|
| 读取整个对象 | GET | HGETALL |
| 修改单个字段 | GET->修改->SET | HSET |
| 字段原子加减 | 不支持 | HINCRBY |
| 内存占用 | 较低 | 较高 |
4.3 购物车场景
购物车数据结构:
key: cart:user:1001 field value ──────────── ───────── item:sku:001 2 # 商品001买2件 item:sku:002 1 # 商品002买1件 item:sku:003 5 # 商品003买5件购物车操作:
添加商品: HSET cart:user:1001 item:sku:001 2 增加数量: HINCRBY cart:user:1001 item:sku:001 1 减少数量: HINCRBY cart:user:1001 item:sku:001 -1 查看商品数量: HGET cart:user:1001 item:sku:001 删除商品: HDEL cart:user:1001 item:sku:001 查看购物车所有商品: HGETALL cart:user:1001 购物车商品数量: HLEN cart:user:1001为什么用Hash而不是String?
String方式(不推荐): SET cart:user:1001:sku:001 2 SET cart:user:1001:sku:002 1 # 问题:商品数量无法原子操作,遍历麻烦 Hash方式(推荐): HSET cart:user:1001 item:sku:001 2 HSET cart:user:1001 item:sku:002 1 # 同一用户的购物车在一个key下,操作方便五、Set类型
5.1 常用命令
| 命令 | 说明 | 示例 |
|---|---|---|
| SADD key member | 添加成员 | SADD tags python |
| SCARD key | 获取成员数 | SCARD tags |
| SMEMBERS key | 获取所有成员 | SMEMBERS tags |
| SISMEMBER key member | 判断是否成员 | SISMEMBER tags python |
| SRANDMEMBER key count | 随机取成员 | SRANDMEMBER tags 3 |
| SPOP key count | 随机弹出成员 | SPOP tags 1 |
| SDIFF key1 key2 | 差集 | SDIFF tags1 tags2 |
| SINTER key1 key2 | 交集 | SINTER tags1 tags2 |
| SUNION key1 key2 | 并集 | SUNION tags1 tags2 |
5.2 好友关系场景
共同好友:
用户A的好友:SADD friends:A alice bob carol david 用户B的好友:SADD friends:B bob carol eve frank 查询共同好友: SINTER friends:A friends:B └── 结果:[bob, carol]好友推荐(可能认识):
用户A的好友:SADD friends:A alice bob carol 用户B的好友:SADD friends:B alice eve A认识B认识的人(推荐): SDIFF friends:B friends:A └── 结果:[eve] # 推荐A认识eve流程图:
推荐算法: 1. 找到目标用户的所有好友 2. 找到这些好友的所有好友 3. 排除目标用户已经认识的人 4. 剩下的就是推荐列表5.3 抽奖场景
简单抽奖:
参与抽奖: SADD lottery:20240101 user001 SADD lottery:20240101 user002 SADD lottery:20240101 user003 查看参与人数: SCARD lottery:20240101 随机抽取1个中奖者: SRANDMEMBER lottery:20240101 1 随机抽取3个中奖者: SRANDMEMBER lottery:20240101 3一次性抽奖(抽完不能重复中):
抽取1个: SPOP lottery:20240101 1 └── 返回中奖者,同时从名单删除 抽取3个: SPOP lottery:20240101 3两种方式区别:
SRANDMEMBER:查看抽奖名单,不减少参与者 SPOP:弹出中奖者,参与者数量减少六、Zset(有序集合)类型
6.1 常用命令
| 命令 | 说明 | 示例 |
|---|---|---|
| ZADD key score member | 添加成员 | ZADD rank 100 alice |
| ZSCORE key member | 获取分数 | ZSCORE rank alice |
| ZRANK key member | 获取排名(升序) | ZRANK rank alice |
| ZREVRANK key member | 获取排名(降序) | ZREVRANK rank alice |
| ZRANGE key start end | 按排名查(升序) | ZRANGE rank 0 9 |
| ZREVRANGE key start end | 按排名查(降序) | ZREVRANGE rank 0 9 |
| ZINCRBY key n member | 分数+n | ZINCRBY rank 10 alice |
6.2 排行榜场景
积分排行榜:
用户Alice:ZADD rank 850 alice 用户Bob: ZADD rank 1200 bob 用户Carol:ZADD rank 950 carol查询排名(从高到低):
第1名到第10名: ZREVRANGE rank 0 9 WITHSCORES 结果: 1) bob 1200 2) carol 950 3) alice 850 查看某用户排名: ZREVRANK rank alice └── 返回 2(第3名) 查看某用户分数: ZSCORE rank alice └── 返回 850用户积分变化:
Alice点赞+10: ZINCRBY rank 10 alice Bob被踩-50: ZINCRBY rank -50 bob完整流程图:
用户行为 -> 更新积分 -> ZINCRBY rank score user_id │ v 查询排行榜 -> ZREVRANGE rank 0 9 WITHSCORES │ v 展示:第1名 xxx 1200分 第2名 yyy 950分 ...6.3 Zset vs Set 核心区别
| 特性 | Set | Zset |
|---|---|---|
| 成员 | 无序 | 无序 |
| 分数 | 无 | 有(用于排序) |
| 查询 | 随机 | 按排名 |
| 典型场景 | 好友关系、抽奖 | 排行榜 |
七、应用场景总结
7.1 String类型总结
应用场景: ├── 缓存:SET user:1 json_data ├── 计数器:INCR view:video:001 ├── 分布式锁:SET lock uuid NX EX 30 └── 位运算:SETBIT sign:202401 30 17.2 List类型总结
应用场景: ├── 队列:LPUSH + BRPOP ├── 栈:LPUSH + LPOP ├── 最新列表:LPUSH + LTRIM └── 消息队列:跨系统异步通信7.3 Hash类型总结
应用场景: ├── 对象存储:HSET user:1 name zhangsan └── 购物车:HSET cart:user:1 item:sku:001 27.4 Set类型总结
应用场景: ├── 好友关系:SINTER friends:A friends:B ├── 推荐好友:SDIFF friends:B friends:A └── 抽奖:SRANDMEMBER lottery 37.5 Zset类型总结
应用场景: └── 排行榜:ZADD rank score user; ZREVRANGE rank 0 9八、面试追问FAQ
| 问题 | 答案 |
|---|---|
| Redis为什么这么快? | 纯内存操作,单线程(避免竞争),IO多路复用 |
| Redis和MySQL如何选? | 热数据放Redis,冷数据放MySQL,两者配合使用 |
| String能存多大数据? | 单个value最大512MB,但一般不超过10MB |
| Hash适合存什么? | 字段频繁变化的对象,如购物车、用户信息 |
| Zset用什么算法实现? | 跳表(SkipList)+ 哈希表,查找和排序都快 |
| 分布式锁为什么不直接DEL? | 可能删了别人的锁,需要先判断uuid再删除 |
| Redis过期键的删除策略? | 惰性删除(访问时检查)+ 定期删除(定时扫描) |
根据零声教育教学写作https://github.com/0voice
