当前位置: 首页 > news >正文

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原子+1INCR count
INCRBY key n原子+nINCRBY count 10
DECR key原子-1DECR count
DECRBY key n原子-nDECRBY count 5
SETNX key valuekey不存在才设置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 分布式锁场景

分布式锁的核心操作:

  1. 加锁:确保只有一个客户端能获取锁
  2. 释放锁:只能释放自己加的锁

加锁命令:

SET lock uuid NX EX30
SET 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字段原子+nHINCRBY 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结构有上限)

对比:

操作StringHash
读取整个对象GETHGETALL
修改单个字段GET->修改->SETHSET
字段原子加减不支持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分数+nZINCRBY 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 核心区别

特性SetZset
成员无序无序
分数有(用于排序)
查询随机按排名
典型场景好友关系、抽奖排行榜

七、应用场景总结

7.1 String类型总结

应用场景: ├── 缓存:SET user:1 json_data ├── 计数器:INCR view:video:001 ├── 分布式锁:SET lock uuid NX EX 30 └── 位运算:SETBIT sign:202401 30 1

7.2 List类型总结

应用场景: ├── 队列:LPUSH + BRPOP ├── 栈:LPUSH + LPOP ├── 最新列表:LPUSH + LTRIM └── 消息队列:跨系统异步通信

7.3 Hash类型总结

应用场景: ├── 对象存储:HSET user:1 name zhangsan └── 购物车:HSET cart:user:1 item:sku:001 2

7.4 Set类型总结

应用场景: ├── 好友关系:SINTER friends:A friends:B ├── 推荐好友:SDIFF friends:B friends:A └── 抽奖:SRANDMEMBER lottery 3

7.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

http://www.cnnetsun.cn/news/2487057.html

相关文章:

  • 深入STM32WLE5的LoRa核心:对比SX126x裸驱与LoRaWAN协议栈,哪个更适合你的项目?
  • Redis缓存淘汰算法:LRU与LFU的实现原理与调优实战
  • 动手搭建一个‘能源局域网’:基于开源硬件的微型能源路由器原型构想
  • RT-Thread实战:基于STM32F103的线程创建与LED控制
  • 3分钟完成Windows包管理器Winget安装:PowerShell自动化部署方案
  • 微博相册批量下载神器:三步搞定海量图片收藏
  • 别再为RK3588 NPU环境头疼了!手把手教你用Conda搞定rknn-toolkit2安装(附国内源加速)
  • 深入理解STM32的FSMC:如何像访问内存一样轻松驱动TFTLCD屏
  • 开漏输出上拉电阻计算:从原理到I2C/GPIO实战选型
  • Android BroadcastReceiver 深度解析:原理、实践与面试指南
  • SpringBoot+Vue3实战:从零搭建一个咖啡店后台管理系统(附完整源码和数据库设计)
  • WPF TabControl美化实战:从默认丑到高级感,自定义样式与交互动画全攻略
  • 基于HPM6750 RISC-V的PX4飞控硬件设计与移植实战
  • 别再死记硬背了!用‘虚拟时间’这个比喻,5分钟彻底搞懂Linux CFS调度器
  • 你的STM32 RTC时间总跑飞?可能是LSE晶振和电池备份没配对
  • 别再为画图发愁了!手把手教你用开源神器draw.io搞定流程图和数学公式
  • 毕业设计救星:用STC89C52单片机+AD采集,手把手教你做一个400Hz中频电源(附完整电路图)
  • 逆向分析新思路:当Flutter遇上Frida,如何Hook加密函数并自吐算法参数?
  • Linux网络编程实战:从Socket基础到高并发服务器设计
  • 从‘黑窗口’到彩色世界:用GLUT快速实现你的第一个OpenGL图形程序(含完整代码解析)
  • UnityPackage Extractor终极指南:快速免费提取Unity资源包
  • ADS1110与51单片机I2C通信详解:手把手教你驱动并读取三路电压(附常见问题排查)
  • 用Python串口控制机械臂:从RS232协议解析到完整指令序列编程实战
  • 从一次安全扫描告警说起:聊聊SSH Banner那点事与自定义的‘安全艺术’
  • 华科计组实验通关秘籍:用Logisim搞定数据表示九大关卡(附避坑指南与源码)
  • 告别C盘爆满!保姆级教程:在D盘用Qt在线安装器搞定6.2.4开发环境(附组件选择避坑指南)
  • OmniSharp-vim与fzf、vim-clap深度集成:提升C开发效率的7个关键点
  • 拆解ESP32-C3最小系统:除了MCU,你的开发板还需要哪些外围电路?(附BOM清单)
  • 如何快速掌握Rufus:从USB格式化到启动盘制作的终极指南
  • 用GEE和Landsat 8数据,5步搞定城市生态健康“体检报告”(附完整代码)