Redis 基础详解(上篇)

一、Redis 是什么

要了解 Redis 肯定是先去官网看看:

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker. Redis provides data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperloglogs, geospatial indexes, and streams.

至少官方介绍第一句应该是可以很容易看懂:"Redis is an open source (BSD licensed),in-memory data structure store, used as a database,cache and message broker."

Redis是一个开源的,基于内存的数据结构存储,可用作于数据库、缓存、消息中间件。

Redis 是一个使用 C 语言写成的,开源的、key-value 结构的、非关系型数据库。它支持存储的 value 类型相对更多,包括 String (字符串)、List (列表)、Set (集合)、Sorted Set (有序集合) 和 Hash (哈希),而且这些操作都是原子性的。在此基础上,Redis 支持各种不同方式的排序。为了保证效率,数据都是缓存在内存中。Redis 可以周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。

使用 Redis 有哪些好处?

  1. 速度快,因为数据存在内存中;

  2. 支持丰富数据类型,支持 string,list,set,sorted set,hash等;

  3. 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行;

  4. 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除;

二、单线程?

你可能听说过 Redis 是单线程的,那会不会很慢呢?

为什么 Redis 是单线程的?

Redis 的数据存储在内存中,如果数据全都在内存里,单线程的去操作就是效率最高的。

为什么呢?因为多线程的本质就是 CPU 模拟出来多个线程的情况,这种模拟出来的情况就有一个代价,就是上下文的切换,对于一个内存的系统来说,它没有上下文的切换就是效率最高的。因为上下文切换所花费的时间远大于直接从内存中读取数据所花费的时间。

Redis 用单个 CPU 绑定一块内存的数据,然后针对这块内存的数据进行多次读写的时候,都是在一个CPU上完成的,所以它是单线程处理这个事。在内存的情况下,这个方案就是最佳方案。

这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来处理,一个正式的 Redis Server 运行的时候肯定是不止一个线程的,例如 Redis 进行持久化的时候会以子进程或者子线程的方式执行。

三、Redis 数据类型

1、String
String 数据结构是简单的 key value 类型,value 其实不仅可以是 String,也可以是数字。需要注意是一个键值最大存储 512MB。

  • 适合的业务场景
    1. 缓存:最经典的用法。缓存数据库查询结果、网页内容、会话信息等。例如:SET user:1001 "{name: 'Alice', email: 'alice@example.com'}"
    2. 计数器:利用 INCRDECR 命令实现原子性操作,无需担心多线程竞争。例如:文章阅读量 INCR article:100:views、网站总用户数、限流器(每秒请求数)。
    3. 分布式锁:利用 SET key value NX EX seconds 命令实现简单的分布式锁。NX 表示只有当 Key 不存在时才设置,EX 设置过期时间。
    4. 存储小规模二进制数据:虽然不推荐存大对象,但可以存储序列化后的小图片、文件等。

简单使用

> set myName "redis"
OK

> get myName
"redis"

> set myName "memcache"
OK

> get myName
"memcache"

> set mycount 1
OK

> get mycount
"1"

> incr mycount
(integer) 2

场景:快速获取文章内容,并统计文章被阅读了多少次。

# 1. 缓存一篇完整的文章(序列化为JSON字符串)
> SET article:1001 '{"id": 1001, "title": "Redis Tutorial", "content": "...", "author": "Alice"}'
"OK"

# 2. 获取文章缓存
> GET article:1001
"{\"id\": 1001, \"title\": \"Redis Tutorial\", \"content\": \"...\", \"author\": \"Alice\"}"

# 3. 阅读量+1 (原子操作,无需担心并发问题)
> INCR article:1001:views
(integer) 1
> INCR article:1001:views
(integer) 2

# 4. 获取阅读量
> GET article:1001:views
"2"

# 5. 批量获取多篇文章的标题(使用Pipeline效率更高)
> MSET article:1001:title "Redis Tutorial" article:1002:title "Python Basics"
"OK"
> MGET article:1001:title article:1002:title
1) "Redis Tutorial"
2) "Python Basics"

2、Hash(哈希)
Hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。 比如我们可以 Hash 数据结构来存储用户信息,商品信息等等。

  • 适合的业务场景
    1. 存储对象信息:这是 Hash 的最佳场景。例如存储用户信息 HMSET user:1001 name "Alice" age "30" email "alice@example.com"。相比于将整个用户对象序列化成 String 存储,Hash 允许你单独获取或更新某一个字段,更加高效,节省网络带宽。
    2. 购物车:以用户ID为 Key,商品ID为 Field,商品数量为 Value。例如:HSET cart:1001 商品A 2HINCRBY cart:1001 商品A 1。可以非常方便地添加商品、增加数量、获取所有商品。
> hmset lilei name "LiLei" age 26 title "Senior"
OK

> hget lilei age
26

> hget lilei title
Senior

> hset lilei title "Primary"
(integer) 0

> hget lilei title
Primary

场景:文章对象有很多字段,我们想单独修改某个字段(如更新标题),而不需要重写整个对象。

# 1. 用Hash存储文章对象
> HMSET article:1001 title "Redis Tutorial" content "..." author "Alice" views 0
"OK"

# 2. 获取整个文章对象
> HGETALL article:1001
1) "title"
2) "Redis Tutorial"
3) "content"
4) "..."
5) "author"
6) "Alice"
7) "views"
8) "0"

# 3. 只获取文章的标题和作者
> HMGET article:1001 title author
1) "Redis Tutorial"
2) "Alice"

# 4. 只更新文章的标题(非常高效)
> HSET article:1001 title "Advanced Redis Tutorial"
(integer) 0 # 表示更新了已存在的字段

# 5. 增加文章的阅读量(Hash也有原子自增命令)
> HINCRBY article:1001 views 1
(integer) 1

对比 String 存整个 JSON,Hash在部分更新和获取时优势明显。

3、List(列表)
是 Redis 的简单的字符串列表。list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

  • 适合的业务场景
    1. 消息队列(简单版):使用 LPUSH(生产消息)和 BRPOP(阻塞消费消息)可以实现一个简单的 FIFO 队列。但由于缺乏 ACK 机制和消息重试,对于要求严格的业务,更推荐使用 Stream。
    2. 最新消息/文章列表:例如朋友圈的时间线、新闻推送。LPUSH 添加新内容,LRANGE 0 9 获取最新的10条。
    3. 记录操作日志:将用户的操作记录 LPUSH 到一个列表中,需要时可以从头遍历。
> lpush mylist aaa
(integer) 1
> lpush mylist bbb
(integer) 2

> rpush mylist ccc
(integer) 3

> llen mylist
(integer) 3

> lrange mylist 0 2
1) "bbb"
1) "aaa"
1) "ccc"

4、Set
Set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 不允许重复元素,可以自动排重的,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。

  • 适合的业务场景
    1. 标签(Tag)系统:给文章、用户添加标签。SADD article:1001:tags tech redis python。可以轻松找出拥有共同标签的文章(求交集)。
    2. 共同关注/好友SINTER user:1001:follows user:1002:follows 可以快速计算出两个用户的共同关注。
    3. 抽奖/随机推荐SRANDMEMBERSPOP 命令可以随机返回一个或多个元素,非常适合实现抽奖活动。
    4. 数据去重:确保添加的元素不重复,例如对一批爬取的 URL 进行去重。
> sadd myset 111
(integer) 1
> sadd myset 222
(integer) 1
> sadd myset 333
(integer) 1
> sadd myset 222
(integer) 0

> smembers myset
1) "111"
2) "222"
3) "333"

> sismember myset 111
(integer) 1
> sismember myset 444
(integer) 0

还可使用 sinter 求多个 Set 的交集,sunion 求 Set 并集。

例如以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同喜好、二度好友等功能。

> smembers myset1
1) "zhangsan"
2) "wangwu"

> smembers myset2
1) "lisi"
2) "zhangsan"

> sinter myset1 myset2
1) "zhangsan"

> sunion myset1 myset2
1) "lisi"
2) "wangwu"
3) "zhangsan"

5、Sorted Set
与 Set 相比 Sorted Set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。

  • 适合的业务场景
    1. 排行榜:这是最完美的应用场景。例如游戏积分排行榜、热搜榜。ZADD leaderboard 100 "playerA"ZINCRBY leaderboard 5 "playerA" 增加积分,ZREVRANGE leaderboard 0 9 WITHSCORES 获取前十名。
    2. 带权重的队列score 可以代表任务的优先级,消费者按 score 顺序来处理任务。
    3. 范围查找:例如处理按时间排序的数据。将时间戳作为 scoreZRANGEBYSCORE 可以轻松查询某一时间段内的数据。

例如,根据点击量实现排行榜,适合使用 Sorted Set 结构进行存储。

> zadd myzset 3 a
(integer) 1
> zadd myzset 1 b
(integer) 1
> zadd myzset 2 c
(integer) 1
> zadd myzset 2 c
(integer) 0
> zadd myzset 1 d
(integer) 1

> zcard myzset
(integer) 4

> zrangebyscore myzset 0 3
1) "b"
2) "d"
3) "c"
4) "a"

根据文章获得的点赞数排序,生成一个周榜。

# 1. 用户100为文章1001点赞,点赞数+1
> ZINCRBY article:weekly_ranking 1 article:1001
"1"

# 2. 用户101也为文章1001点赞
> ZINCRBY article:weekly_ranking 1 article:1001
"2"

# 3. 用户100为文章1002点赞
> ZINCRBY article:weekly_ranking 1 article:1002
"1"

# 4. 获取点赞排行榜前3名(WITHSCORES选项会同时返回分数)
> ZREVRANGE article:weekly_ranking 0 2 WITHSCORES
1) "article:1001" # 冠军文章
2) "2"            # 点赞数
3) "article:1002" # 亚军文章
4) "1"            # 点赞数

四、Redis 应用

例1: Redis 中有 1 亿个 key,其中有 10 万个 key 是已知的某个固定前缀,如何将找出这些 key。

当问题是:如何找出某一前缀的 key。这里特别需要注意数据规模是多大,再结合实际场景去解决。

如果数据量小,可以使用 keys 命令来查询。

> keys key*
 1) "key9"
 2) "key3"
 3) "key1"
 4) "key2"
 5) "key5"
 6) "key8"
 7) "key7"
 8) "key6"
 9) "key"
10) "key4"

但是,如果数据量大,而 Redis 正在线上运行,使用 keys 命令很可能会阻塞 Redis,使其不能提供服务。
这时可以使用 SCAN cursor [MATCH pattern] [COUNT count] 命令。需要注意的是条命令返回的数量是不可控的,只能大概符合 count。

> scan 0 match key* count 5
1) "12"
2) 1) "key9"
   2) "key7"
   3) "key4"
> scan 12 match key* count 5
1) "14"
2) 1) "key8"
   2) "key6"
   3) "key5"
> scan 14 match key* count 5
1) "0"
2) 1) "key3"
   2) "key1"
   3) "key2"
   4) "key"

cursor 从 0 开始,这里第一个返回值为 cursor 用于下一次迭代,当 cursor 再次为 0 时,说明迭代完成。

例2:如何通过 Redis 实现分布式锁

首先想到的方法是使用 SETNX key value,如果 key 不存在,则创建并赋值,返回 1;若 key 已存在,则创建失败,返回 0。

当某一个线程通过 SETNX key value 设置成功后占用该资源,其他线程执行该命令就会设置失败,说明已经有线程占用该资源,通过这种方式来实现分布式锁。

> setnx mylock 11
(integer) 1
> setnx mylock 22
(integer) 0
> get mylock
"11"

同时,为了防止死锁,还需要设置 key 的过期时间 EXPIRE key second,设置 key 的过期时间,经过 second 秒后 key 会被删除。

> expire mylock 11
(integer) 1

虽然 Redis 中每个命令都满足原子性,但两个命令组合起来不满足了,例如若设置锁后,程序挂了,来不及设置过期时间,那么这个锁就无法释放了。

所以我们需要把上面两个命令结合起来的命令 SET key value [EX seconds] [PX milliseconds] [NX|XX],其中 NX 是 key 不存在时设置成功,XX 是 key 存在时设置成功。例如:

> set mylock 123 ex 10 nx
OK

这里还需要注意,不要让大量的 key 在同一时间过期,因为删除大量的 key 很耗时,会出现卡顿现象。所以我们可以在设置 key 的过期时间时,加上一个随机值来避免。

例3:使用 Redis 实现消息队列

使用 List 作为队列,RPUSH 生产消息,LPOP 消费消息。

rpush testlist aaa
(integer) 1
> rpush testlist bbb
(integer) 2
> rpush testlist ccc
(integer) 3
> lpop testlist
"aaa"
> lpop testlist
"bbb"
> lpop testlist
"ccc"

但是这样有个缺点,就是只能一对一的消息通信,所以还可以使用 pub/sub 主题订阅者模式,发送者(pub)使用 PUBLISH channel message 发送消息,订阅者(sub)使用 SUBSCRIBE channel 接受消息,并且订阅者可以接收任意数量的 channel。

但是消息的发布是无状态的,无法保证可达性。

五、Redis 数据持久化存储

上面说了 Redis 是基于内存的数据库,一旦进程退出,数据就会丢失,所以我们需要它也可以把数据写到磁盘上,当 Redis 重启后,可以从磁盘中恢复数据。

Redis 提供了两种解决方案将内存中的数据保存到磁盘上。一种是 RDB 持久化,原理是将 Reids 在内存中的全部数据库记录定时 dump 到磁盘上的 RDB 持久化;另外一种是 AOF 持久化,原理是将 Reids 的操作日志以追加的方式写入文件。

1、RDB(Redis DataBase)

RDB 是 Redis 用来进行持久化的一种方式,是把当前内存中的数据集快照写入磁盘,也就是 Snapshot 快照所有数据。恢复时是将快照文件直接读到内存里。

RDB 存储的劣势:

  1. RDB 方式数据没办法做到实时持久化,该方式是每间隔一段时间做一次备份。因为 bgsave 每次运行都要执行 fork 操作创建子进程,属于重量级操作,频繁执行成本过高。所以如果 Redis 意外挂掉,就会丢失最后一次快照后的所有修改;

  2. RDB 是内存数据的全量同步,数据量大会由于 I/O 而严重影响性能;

  3. RDB 文件使用特定二进制格式保存,Redis 版本演进过程中有多个格式的 RDB 版本,旧版本无法兼容新版的格式。

2、AOF(Append Only File)

AOF 持久化:记录除了查询以外,所有变更数据库状态的指令,以 append 的形式追加保存到 AOF 文件中。AOF 文件通常会比 RDF 文件体积更大。

3、RDB-AOF 混合模式
混合模式是先使用 bgsave 以 RDB 形式将内存中的全部数据写入磁盘,之后当有新的数据时,再使用 AOF 的形式追加到文件中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 本文精心整理了书籍、博客以及本人面试中遇到的基础知识点,方便大家快速回顾知识。 参考: https://githu...
    蛮三刀酱阅读 998评论 0 4
  • 本文为笔者对在学习Redis过程中所收集资料的一个总结,目的是为了以后方便回顾相关的知识,大部分为非原创内容。特此...
    EakonZhao阅读 14,627评论 0 9
  • redis简介: 官方链接:跳转地址;中文官方链接:跳转地址 Redis 是一个开源(BSD许可)的,内存中的数据...
    _偏執狂阅读 1,441评论 1 32
  • 4月8日凌晨两点我们到的勐腊县城,到了后找到宾馆住下,实在太累了,我简单冲了澡就马上睡了。 第二天一大早,路上的人...
    陈飞鹏阅读 1,065评论 0 0
  • 我感到不公平,中间还夹杂着一丝愤怒。我想不明白事情是如何发展到如今的地步的。她在提出分开的时候振振有词,似乎有一种...
    滑冰的夏虫阅读 111评论 0 0

友情链接更多精彩内容