Redis 的使⽤和原理

第一章:初识 Redis
1.1盛赞 Redis

Redis 是⼀种基于键值对(key-value)的 NoSQL 数据库,与很多键值对数据库不同的是,Redis
中的值可以是由 string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)、 Bitmaps(位图)、HyperLogLog、GEO(地理信息定位)等多种数据结构和算法组成,因此 Redis可以满⾜很多的应⽤场景,⽽且因为 Redis 会将所有数据都存放再内存中,所以它的读写性能⾮常惊⼈。不仅如此,Redis 还可以将内存的数据利⽤快照和⽇志的形式保存到硬盘上,这样在发⽣类似断电或者机器故障的时候,内存中的数据不会"丢失"。除了上述功能以外,Redis 还提供了键过期、发布订阅、事务、流⽔线、Lua 脚本等附加功能。总之,如果在合适的场景使⽤号 Redis,它就会像⼀把瑞⼠军⼑⼀样所向披靡。
2008 年,Redis 的作者 Salvatore Sanfilippo 在开发⼀个叫 LLOOGG 的⽹站时,需要实现⼀个⾼
性能的队列功能,最开始是使⽤ MySQL 来实现的,但后来发现⽆论怎么优化 SQL 语句等都不能使⽹站的性能提⾼上去,再加上⾃⼰囊中羞涩,于是他决定⾃⼰做⼀个专属于 LLOOGG 的数据库,这个就是 Redis 的前⾝。后来,Salvatore Sanfilippo 将 Redis 1.0 的源码发布到 Github 上,可能连他⾃⼰都没想到,Redis 后来如此受欢迎。
假如现在有⼈问 Redis 的作者都有谁在使⽤ Redis,我想他可以开句玩笑的回答:还有谁不使⽤
Redis,当然这只是开玩笑,但是从 Redis 的官⽅公司统计来看,有很多重量级的公司都在使⽤
Redis,如国外的 Twitter、Instagram、Stack Overflow、Github 等,国内就更多了,如果单单从体量来统计,新浪微博可以说是全球最⼤的 Redis 使⽤者,除了新浪微博,还有像阿⾥巴巴、腾讯、搜狐、优酷⼟⾖、美团、⼩⽶、唯品会等公司都是 Redis 的使⽤者。除此之外,许多开源技术像 ELK 等
已经把 Redis 作为它们组件中的重要⼀环,⽽且 Redis 还提供了模块系统让第三⽅⼈员实现功能扩展,让 Redis 发挥出更⼤的威⼒。所以,可以这么说,熟练使⽤和运维 Redis 已经成为开发运维⼈员的⼀个必备技能。

1.2 Redis 特性

Redis 之所以受到如此多公司的⻘睐,必然有之过⼈之处,下⾯是关于 Redis 的 8 个重要特性。

1. 速度快

正常情况下,Redis 执⾏命令的速度⾮常快,官⽅给出的数字是读写性能可以达到 10 万 / 秒,当
然这也取决于机器的性能,但这⾥先不讨论机器性能上的差异,只分析⼀下是什么造就了 Redis 如此之快,可以⼤概归纳为以下四点:
• Redis 的所有数据都是存放在内存中的,下表是⾕歌公司 2009 年给出的各层级硬件执⾏速度,所以把数据放在内存中是 Redis 速度快的最主要原因。

2. 基于键值对的数据结构服务器

⼏乎所有的编程语⾔都提供了类似字典的功能,例如 C++ ⾥的 map、Java ⾥的 map、Python ⾥
的 dict 等,类似于这种组织数据的⽅式叫做基于键值对的⽅式,与很多键值对数据库不同的是,
Redis 中的值不仅可以是字符串,⽽且还可以是具体的数据结构,这样不仅能便于在许多应⽤场景的开发,同时也能提⾼开发效率。Redis 的全程是 REmote Dictionary Server,它主要提供了 5 种数据结构:字符串(string)、哈希(hash)、列表(list)、集合(set)、有序集合(ordered set / zset),同时在字符串的基础之上演变出了位图(Bitmaps)和 HyperLogLog 两种神奇的 "数据结构",并且随着 LBS(Location Based Service,基于位置服务)的不断发展,Redis 3.2. 版本种加⼊有关 GEO(地理信息定位)的功能,总之在这些数据结构的帮助下,开发者可以开发出各种 "有意思" 的应⽤。

3. 丰富的功能

除了 5 种数据结构,Redis 还提供了许多额外的功能:
• 提供了键过期功能,可以⽤来实现缓存。
• 提供了发布订阅功能,可以⽤来实现消息系统。
• ⽀持 Lua 脚本功能,可以利⽤ Lua 创造出新的 Redis 命令。
• 提供了简单的事务功能,能在⼀定程度上保证事务特性。
• 提供了流⽔线(Pipeline)功能,这样客⼾端能将⼀批命令⼀次性传到 Redis,减少了⽹络的开
销。

4. 简单稳定

Redis 的简单主要表现在三个⽅⾯。⾸先,Redis 的源码很少,早期版本的代码只有 2 万⾏左右,
3.0 版本以后由于添加了集群特性,代码增⾄ 5 万⾏左右,相对于很多 NoSQL 数据库来说代码量相对要少很多,也就意味着普通的开发和运维⼈员完全可以 "吃透" 它。其次, Redis 使⽤单线程模型 , 这样不仅使得 Redis 服务端处理模型变得简单,⽽且也使得客⼾端开发变得简单。最后,Redis 不需要依赖于操作系统中的类库(例如 Memcache 需要依赖 libevent 这样的系统类库),Redis ⾃⼰实现了事件处理的相关功能。
但与简单相对的是 Redis 具备相当的稳定性,在⼤量使⽤过程中,很少出现因为 Redis ⾃⾝ BUG
⽽导致宕掉的情况。

5. 客⼾端语⾔多

Redis 提供了简单的 TCP 通信协议,很多编程语⾔可以很⽅便地接⼊到 Redis,并且由于 Redis 受到社区和各⼤公司的⼴泛认可,所以⽀持 Redis 的客⼾端语⾔也⾮常多,⼏乎涵盖了主流的编程语⾔,例如 C、C++、Java、PHP、Python、NodeJS 等,后续我们会对 Redis 的客⼾端使⽤做详细说明。

6. 持久化(Persistence)

通常看,将数据放在内存中是不安全的,⼀旦发⽣断电或者机器故障,重要的数据可能就会丢
失,因此 Redis 提供了两种持久化⽅式:RDB 和 AOF,即可以⽤两种策略将内存的数据保存到硬盘中
(如图 1-1 所⽰),这样就保证了数据的可持久性,后续我们将对 Redis 的持久化进⾏详细说明。

7. 主从复制(Replication)

Redis 提供了复制功能,实现了多个相同数据的 Redis 副本(Replica)(如图 1- 2 所⽰),复制
功能是分布式 Redis 的基础。后续我们会对 Redis 的复制功能进⾏详细演⽰。

8. ⾼可⽤(High Availability)和分布式(Distributed)

Redis 提供了⾼可⽤实现的 Redis 哨兵(Redis Sentinel),能够保证 Redis 结点的故障发现和故
障⾃动转移。也提供了 Redis 集群(Redis Cluster),是真正的分布式实现,提供了⾼可⽤、读写和容量的扩展性

1.3 Redis 使⽤场景
1.3.1 Redis 可以做什么
1. 缓存(Cache)

缓存机制⼏乎在所有⼤型⽹站都有使⽤,合理地使⽤缓存不仅可以加速数据的访问速度,⽽且能
够有效地降低后端数据源的压⼒。Redis 提供了键值过期时间设置,并且也提供了灵活控制最⼤内存和内存溢出后的淘汰策略。可以这么说,⼀个合理的缓存设计能够为⼀个⽹站的稳定保驾护航。

2. 排⾏榜系统

排⾏榜系统⼏乎存在于所有的⽹站,例如按照热度排名的排⾏榜,按照发布时间的排⾏榜,按照
各种复杂维度计算出的排⾏榜,Redis 提供了列表和有序集合的结构,合理地使⽤这些数据结构可以很⽅便地构建各种排⾏榜系统。

3. 计数器应⽤

计数器在⽹站中的作⽤⾄关重要,例如视频⽹站有播放数、电商⽹站有浏览数,为了保证数据的
实时性,每⼀次播放和浏览都要做加 1 的操作,如果并发量很⼤对于传统关系型数据的性能是⼀种挑战。Redis 天然⽀持计数功能⽽且计数的性能也⾮常好,可以说是计数器系统的重要选择。

4. 社交⽹络

赞 / 踩、粉丝、共同好友 / 喜好、推送、下拉刷新等是社交⽹站的必备功能,由于社交⽹站访问量
通常⽐较⼤,⽽且传统的关系型数据不太合适保存这种类型的数据,Redis 提供的数据结构可以相对⽐较容易地实现这些功能。

5. 消息队列系统

消息队列系统可以说是⼀个⼤型⽹站的必备基础组件,因为其具有业务解耦、⾮实时业务削峰等
特性。Redis 提供了发布订阅功能和阻塞队列的功能,虽然和专业的消息队列⽐还不够⾜够强⼤,但是对于⼀般的消息队列功能基本可以满⾜

1.3.2 Redis 不可以做什么

实际上和任何⼀⻔技术⼀样,每个技术都有⾃⼰的应⽤场景和边界,也就是说 Redis 并不是万⾦
油,有很多合适它解决的问题,但是也有很多不合适它解决的问题。 我们可以站在数据规模和数据冷热的⻆度来进⾏分析。
站在数据规模的⻆度看,数据可以分为⼤规模数据和⼩规模数据,我们知道 Redis 的数据是存放
在内存中的,虽然现在内存已经⾜够便宜,但是如果数据量⾮常⼤,例如每天有⼏亿的⽤⼾⾏为数
据,使⽤ Redis 来存储的话,基本上是个⽆底洞,经济成本相当⾼。
站在数据冷热的⻆度,数据分为热数据和冷数据,热数据通常是指需要频繁操作的数据,反之为
冷数据 ,例如对于视频⽹站来说,视频基本信息基本上在各个业务线都是经常要操作的数据,⽽⽤⼾的观看记录不⼀定是经常需要访问的数据,这⾥暂且不讨论两者数据规模的差异,单纯站在数据冷热的⻆度上看,视频信息属于热数据,⽤⼾观看记录属于冷数据。如果将这些冷数据放在 Redis 上,基本上是对于内存的⼀种浪费,但是对于⼀些热数据可以放在 Redis 中加速读写,也可以减轻后端存储的负载,可以说是事半功倍。
所以,Redis 并不是万⾦油,相信随着我们对 Redis 的逐步学习,能够清楚 Redis 真正的使⽤场
景。

第⼆章 Redis 常⻅数据类型
2.1.1 基本全局命令

Redis 有 5 种数据结构,但它们都是键值对种的值,对于键来说有⼀些通⽤的命令。

KEYS


返回所有满⾜样式(pattern)的 key。⽀持如下统配样式。
• h?llo 匹配 hello , hallo 和 hxllo
• h*llo 匹配 hllo 和 heeeello
• h[ae]llo 匹配 hello 和 hallo 但不匹配 hillo
• h[^e]llo 匹配 hallo , hbllo , ... 但不匹配 hello
• h[a-b]llo 匹配 hallo 和 hbllo
返回值:匹配 pattern 的所有 key。

EXISTS

判断某个 key 是否存在。

DEL

删除指定的 key。

返回值:删除掉的 key 的个数

EXPIRE

为指定的 key 添加秒级的过期时间(Time To Live TTL)

返回值:1 表⽰设置成功。0 表⽰设置失败

TTL

获取指定 key 的过期时间,秒级

剩余过期时间。-1 表⽰没有关联过期时间,-2 表⽰ key 不存在。
关于键过期机制,可以参考图 2-1 所⽰。

TYPE

返回 key 对应的数据类型。

返回值: none , string , list , set , zset , hash and stream

2.1.2 数据结构和内部编码

type 命令实际返回的就是当前键的数据结构类型,它们分别是:string(字符串)、list(列
表)、hash(哈希)、set(集合)、zset(有序集合),但这些只是 Redis 对外的数据结构。
Redis 的 5 种数据类型

实际上 Redis 针对每种数据结构都有⾃⼰的底层内部编码实现,⽽且是多种实现,这样 Redis 会
在合适的场景选择合适的内部编码,如表 2-1 所⽰。

可以看到每种数据结构都有⾄少两种以上的内部编码实现,例如 list 数据结构包含了 linkedlist 和
ziplist 两种内部编码。同时有些内部编码,例如 ziplist,可以作为多种数据结构的内部实现,可以通过 object encoding 命令查询内部编码
可以看到 hello 对应值的内部编码是 embstr,键 mylist 对应值的内部编码是 ziplist。
Redis 这样设计有两个好处:
1)可以改进内部编码,⽽对外的数据结构和命令没有任何影响,这样⼀旦开发出更优秀的内部编码,⽆需改动外部数据结构和命令,例如 Redis 3.2 提供了 quicklist,结合了 ziplist 和 linkedlist 两者的优势,为列表类型提供了⼀种更为优秀的内部编码实现,⽽对⽤⼾来说基本⽆感知。
2)多种内部编码实现可以在不同场景下发挥各⾃的优势,例如 ziplist ⽐较节省内存,但是在列表元素⽐较多的情况下,性能会下降,这时候 Redis 会根据配置选项将列表类型的内部实现转换为
linkedlist,整个过程⽤⼾同样⽆感知。

2.1.3 单线程架构

Redis 使⽤了单线程架构来实现⾼性能的内存数据库服务,本节⾸先通过多个客⼾端命令调⽤的例
⼦说明 Redis 单线程命令处理机制,接着分析 Redis 单线程模型为什么性能如此之⾼,最终给出为什么理解单线程模型是使⽤和运维 Redis 的关键。

1. 引出单线程模型

redis单线程,只有一个线程去接收处理所有的请求,并不是服务器进程内部只有一个线程,其实也有很多线程,这些线程都在处理网络IO

上面这个情况,可能会有线程安全问题

其实redis服务器并不会有这种情况,因为redis服务器是单线程服务器,把接收到的请求串行执行,多个请求到达redis队列中,也是需要进行排队等候的.

redis能够使用单线程模型,很好的工作,是因为redis处理的业务逻辑是短平快,不消耗CPU资源,如果一个操作执行的时间太长,就会影响其他的请求的执行

2.为什么redis是单线程,速度还能这么快,效率这么高?

他的速度快和效率高是与关系性数据库(MySQL,Orcale)进行对比

1.redis直接访问的是内存,而关系性数据库访问的硬盘

2.redis的核心功能,比关系性数据库的核心功能要简单,数据库对于数据的插入查询,都有更复杂的功能支持,

3.单线程模型,减少了不必要的线程竞争开销,redis每个操作都是短平快,不涉及特别消耗cpu的操作

4.处理网络IO的时候,使用了epoll这样的IO多路复用机制,IO多路复用机制,一个线程就可以处理多个socket,,epoll事件通知/回调机制,一个线程可以完成好多任务,前提是这些任务的交互不频繁,大部分时间都在等,可以采取epoll多路复用机制

1.String 字符串

在redis中,所以的key都是string类型,value的数据类型才有差异
字符串类型是 Redis 最基础的数据类型,直接按照二进制数据的方式进行存储,不会做任何的编码转化,存的是啥,取出来就是啥,关于字符串需要特别注意:1)⾸先 Redis 中所有的键的 类型都是字符串类型,⽽且其他⼏种数据结构也都是在字符串类似基础上构建的,例如列表和集合的元素类型是字符串类型,所以字符串类型能为其他 4 种数据结构的学习奠定基础。2)其次,如图 2-7所⽰,字符串类型的值实际可以是字符串,包含⼀般格式的字符串或者类似 JSON、XML 格式的字符串;数字,可以是整型或者浮点型;甚⾄是⼆进制流数据,例如图⽚、⾳频、视频等。不过⼀个字符串的最⼤值不能超过 512 MB。

常见命令
SET

将 string 类型的 value 设置到 key 中。如果 key 之前存在,则覆盖,⽆论原来的数据类型是什么。之前关于此 key 的 TTL 也全部失效。
语法:
1 SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
命令有效版本:1.0.0 之后
时间复杂度:O(1)


SET 命令⽀持多种选项来影响它的⾏为:
• EX seconds⸺使⽤秒作为单位设置 key 的过期时间。
• PX milliseconds ⸺使⽤毫秒作为单位设置 key 的过期时间。
• NX ⸺只在 key 不存在时才进⾏设置,即如果 key 之前已经存在,设置不执⾏。
• XX ⸺只在 key 存在时才进⾏设置,即如果 key 之前不存在,设置不执⾏。
返回值:
• 如果设置成功,返回 OK。
• 如果由于 SET 指定了 NX 或者 XX 但条件不满⾜,SET 不会执⾏,并返回 (nil)。

GET

获取 key 对应的 value。如果 key 不存在,返回 nil。如 果 value 的数据类型不是 string,会报错
语法:
1 GET key
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:key 对应的 value,或者 nil 当 key 不存在。

MGET

⼀次性获取多个 key 的值。如果对应的 key 不存在或者对应的数据类型不是 string,返回 nil。
语法:
1 MGET key [key ...]
命令有效版本:1.0.0 之后
时间复杂度:O(N) N 是 key 数量
返回值:对应 value 的列表

MSET

⼀次性设置多个 key 的值。
语法:
1 MSET key value [key value ...]
命令有效版本:1.0.1 之后
时间复杂度:O(N) N 是 key 数量
返回值:永远是 OK



学会使⽤批量操作,可以有效提⾼业务处理效率,但是要注意,每次批量操作所发送的键的数量也不是⽆节制的,否则可能造成单⼀命令执⾏时间过⻓,导致 Redis 阻塞。

SETNX

设置 key-value 但只允许在 key 之前不存在的情况下。
语法:
1 SETNX key value
命令有效版本:1.0.0 之后
时间复杂度:O(1)
返回值:1 表⽰设置成功。0 表⽰没有设置。

SET、SET NX、SET XX 执⾏流程

计数命令
INCR

将 key 对应的 string 表⽰的数字加⼀。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是⼀个整型或者范围超过了 64 位有符号整型,则报错。

INCRBY

将 key 对应的 string 表⽰的数字加上对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是⼀个整型或者范围超过了 64 位有符号整型,则报错。
返回值:integer 类型的加完后的数值。

DECR

将 key 对应的 string 表⽰的数字减⼀。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是⼀个整型或者范围超过了 64 位有符号整型,则报错

DECYBY

将 key 对应的 string 表⽰的数字减去对应的值。如果 key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的 string 不是⼀个整型或者范围超过了 64 位有符号整型,则报错。

INCRBYFLOAT

将 key 对应的 string 表⽰的浮点数加上对应的值。如果对应的值是负数,则视为减去对应的值。如果key 不存在,则视为 key 对应的 value 是 0。如果 key 对应的不是 string,或者不是⼀个浮点数,则报错。允许采⽤科学计数法表⽰浮点数。
很多存储系统和编程语⾔内部使⽤ CAS 机制实现计数功能,会有⼀定的 CPU 开销,但在 Redis 中完全不存在这个问题,因为 Redis 是单线程架构,任何命令到了 Redis 服务端都要顺序执⾏。

APPEND

如果 key 已经存在并且是⼀个 string,命令会将 value 追加到原有 string 的后边。如果 key 不存在,则效果等同于 SET 命令。
语法:
1 APPEND KEY VALUE
时间复杂度:O(1). 追加的字符串⼀般⻓度较短, 可以视为 O(1).
返回值:追加完成之后 string 的⻓度。

GETRANGE

返回 key 对应的 string 的⼦串,由 start 和 end 确定(左闭右闭)。可以使⽤负数表⽰倒数。-1 代表倒数第⼀个字符,-2 代表倒数第⼆个,其他的与此类似。超过范围的偏移量会根据 string 的⻓度调整成正确的值。
语法:
1 GETRANGE key start end
时间复杂度:O(N). N 为 [start, end] 区间的⻓度. 由于 string 通常⽐较短, 可以视为是 O(1)
返回值:string 类型的⼦串

SETRANGE

覆盖字符串的⼀部分,从指定的偏移开始。
语法:
1 SETRANGE key offset value
时间复杂度:O(N), N 为 value 的⻓度. 由于⼀般给的 value ⽐较短, 通常视为 O(1).
返回值:替换后的 string 的⻓度。

STRLEN

获取 key 对应的 string 的⻓度。当 key 存放的类似不是 string 时,报错。
语法:
1 STRLEN key
时间复杂度:O(1)
返回值:string 的⻓度。或者当 key 不存在时,返回 0

命令⼩结

字符串类型命令的效果、时间复杂度,可以参考此表,结合业务需求和数据⼤⼩选择合适的命令。

内部编码

字符串类型的内部编码有 3 种:使用object encoding来查询当前编码
int:8 个字节的⻓整型。

embstr:⼩于等于 39 个字节的字符串。

raw:⼤于 39 个字节的字符串

典型使⽤场景
缓存(Cache)功能

图 2-10 是⽐较典型的缓存使⽤场景,其中 Redis 作为缓冲层,MySQL 作为存储层,绝⼤部分请
求的数据都是从 Redis 中获取。由于 Redis 具有⽀撑⾼并发的特性,所以缓存通常能起到加速读写和降低后端压⼒的作⽤。

举一个例子来理解一下缓存

这是一段伪代码
1)假设业务是根据⽤⼾ uid 获取⽤⼾信息
UserInfo getUserInfo(long uid) {
...
}
2)⾸先从 Redis 获取⽤⼾信息,我们假设⽤⼾信息保存在 "user:info:<uid>" 对应的键中:
// 根据 uid 得到 Redis 的键
String key = "user:info:" + uid;
// 尝试从 Redis 中获取对应的值
String value = Redis 执⾏命令: get key;
// 如果缓存命中( hit )
if (value != null) {
// 假设我们的⽤⼾信息按照 JSON 格式存储
UserInfo userInfo = JSON 反序列化 (value);
return userInfo;
}
如果没有从 Redis 中得到⽤⼾信息,及缓存 miss,则进⼀步从 MySQL 中获取对应的信息,随后写⼊缓存并返回:
// 如果缓存未命中( miss )
if (value == null) {
// 从数据库中,根据 uid 获取⽤⼾信息
UserInfo userInfo = MySQL 执⾏ SQL : select * from user_info where uid = <uid>
// 如果表中没有 uid 对应的⽤⼾信息
if (userInfo == null) {
响应 404
return null;
}
// 将⽤⼾信息序列化成 JSON 格式
String value = JSON 序列化 (userInfo);
// 写⼊缓存,为了防⽌数据腐烂( rot ),设置过期时间为 1 ⼩时( 3600 秒)
Redis 执⾏命令: set key value ex 3600
// 返回⽤⼾信息
return userInfo;
}

redis这样的缓存,经常存储"热点"数据(被高频使用的数据),最近一段时间都会反复用到的数据,

计数(Counter)功能

许多应⽤都会使⽤ Redis 作为计数的基础⼯具,它可以实现快速计数、查询缓存的功能,同时数
据可以异步处理或者落地到其他数据源。如图 2-11 所⽰,例如视频⽹站的视频播放次数可以使⽤
Redis 来完成:⽤⼾每播放⼀次视频,相应的视频播放数就会⾃增 1。

// 在 Redis 中统计某视频的播放次数
long incrVideoCounter(long vid) {
key = "video:" + vid;
long count = Redis 执⾏命令: incr key
return count;
}

共享会话(Session)

如图 2-12 所⽰,⼀个分布式 Web 服务将⽤⼾的 Session 信息(例如⽤⼾登录信息)保存在各⾃
的服务器中,但这样会造成⼀个问题:出于负载均衡的考虑,分布式服务会将⽤⼾的访问请求均衡到不同的服务器上,并且通常⽆法保证⽤⼾每次请求都会被均衡到同⼀台服务器上,这样当⽤⼾刷新⼀次访问是可能会发现需要重新登录,这个问题是⽤⼾⽆法容忍的。

为了解决这个问题,可以使⽤ Redis 将⽤⼾的 Session 信息进⾏集中管理,如图 2-13 所⽰,在这种模式下,只要保证 Redis 是⾼可⽤和可扩展性的,⽆论⽤⼾被均衡到哪台 Web 服务器上,都集中从Redis 中查询、更新 Session 信息。

⼿机验证码

很多应⽤出于安全考虑,会在每次进⾏登录时,让⽤⼾输⼊⼿机号并且配合给⼿机发送验证码,
然后让⽤⼾再次输⼊收到的验证码并进⾏验证,从⽽确定是否是⽤⼾本⼈。为了短信接⼝不会频繁访问,会限制⽤⼾每分钟获取验证码的频率,例如⼀分钟不能超过 5 次

此功能可以⽤以下伪代码说明基本实现思路:

2.Hash 哈希

⼏乎所有的主流编程语⾔都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数
组、映射。在 Redis 中,哈希类型是指值本⾝⼜是⼀个键值对结构,形如 key = "key",value = { {
field1, value1 }, ..., {fieldN, valueN } },Redis 键值对和哈希类型⼆者的关系可以⽤图 2-15 来表⽰。

常见命令
1.HSET

设置 hash 中指定的字段(field)的值(value)。
语法:
HSET key field value [field value ...]
时间复杂度:插⼊⼀组 field 为 O(1), 插⼊ N 组 field 为 O(N)
返回值:添加的字段的个数。

2.HGET

获取 hash 中指定字段的值。
语法:
1 HGET key field
时间复杂度:O(1)
返回值:字段对应的值或者 nil。

3.HEXISTS

判断 hash 中是否有指定的字段。
语法:
HEXISTS key field
时间复杂度:O(1)
返回值:1 表⽰存在,0 表⽰不存在。

4.HDEL

删除 hash 中指定的字段。
语法:
1 HDEL key field [field ...]
时间复杂度:删除⼀个元素为 O(1). 删除 N 个元素为 O(N).
返回值:本次操作删除的字段个数。

5.HKEYS

获取 hash 中的所有字段。
语法:
HKEYS key
时间复杂度:O(N), N 为 field 的个数.
返回值:字段列表。

6.HVALS
获取 hash 中的所有的值。
语法:
HVALS key
时间复杂度:O(N), N 为 field 的个数.
返回值:所有的值。

6.HGETALL

获取 hash 中的所有字段以及对应的值。
语法:
HGETALL key
时间复杂度:O(N), N 为 field 的个数.
返回值:字段和对应的值。

7.HMGET

⼀次获取 hash 中多个字段的值。
语法:
HMGET key field [field ...]
时间复杂度:只查询⼀个元素为 O(1), 查询多个元素为 O(N), N 为查询元素个数.
返回值:字段对应的值或者 nil。

在使⽤ HGETALL 时,如果哈希元素个数⽐较多,会存在阻塞 Redis 的可能。如果开发⼈员只
需要获取部分 field,可以使⽤ HMGET,如果⼀定要获取全部 field,可以尝试使⽤ HSCAN
命令,该命令采⽤渐进式遍历哈希类型,

8.HLEN

获取 hash 中的所有字段的个数。
语法:
HLEN key
时间复杂度:O(1)
返回值:字段个数。

9.HSETNX

在字段不存在的情况下,设置 hash 中的字段和值。
语法:
HSETNX key field value
时间复杂度:O(1)
返回值:1 表⽰设置成功,0 表⽰失败。

10.HINCRBY

将 hash 中字段对应的数值添加指定的值。
语法:
HINCRBY key field increment
时间复杂度:O(1)
返回值:该字段变化之后的值

HINCRBYFLOAT
HINCRBY 的浮点数版本。
语法:
HINCRBYFLOAT key field increment
时间复杂度:O(1)
返回值:该字段变化之后的值。

命令⼩结

表 2-4 是哈希类型命令的效果、时间复杂度,开发⼈员可以参考此表,结合⾃⾝业务需求和数据
⼤⼩选择合适的命令。


内部编码

哈希的内部编码有两种:

ziplist(压缩列表):

当哈希类型元素个数⼩于 hash-max-ziplist-entries 配置(默认 512 个)、
同时所有值都⼩于 hash-max-ziplist-value 配置(默认 64 字节)时,Redis 会使⽤ ziplist 作为哈
希的内部实现,ziplist 使⽤更加紧凑的结构实现多个元素的连续存储,所以在节省内存⽅⾯⽐
hashtable 更加优秀。

hashtable(哈希表):

当哈希类型⽆法满⾜ ziplist 的条件时,Redis 会使⽤ hashtable 作为哈希
的内部实现,因为此时 ziplist 的读写效率会下降,⽽ hashtable 的读写时间复杂度为 O(1)。
下⾯的⽰例演⽰了哈希类型的内部编码,以及响应的变化


1)当 field 个数⽐较少且没有⼤的 value 时,内部编码为 ziplist:

2)当有 value ⼤于 64 字节时,内部编码会转换为 hashtable:

3)当 field 个数超过 512 时,内部编码也会转换为 hashtable:

使⽤场景

图 2-16 为关系型数据表记录的两条⽤⼾信息,⽤⼾的属性表现为表的列,每条⽤⼾信息表现为
⾏。

。如果映射关系表⽰这两个⽤⼾信息,则如图 2-17 所⽰。

相⽐于使⽤ JSON 格式的字符串缓存⽤⼾信息,哈希类型变得更加直观,并且在更新操作上变得
更灵活。可以将每个⽤⼾的 id 定义为键后缀,多对 field-value 对应⽤⼾的各个属性

注意

哈希类型和关系型数据库有两点不同之处:

哈希类型是稀疏的,⽽关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,⽽
关系型数据库⼀旦添加新的列,所有⾏都要为其设置值,即使为 null,如图 2-18 所⽰。


关系数据库可以做复杂的关系查询,⽽ Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等
基本不可能,维护成本⾼。

缓存⽅式对⽐

截⾄⽬前为⽌,我们已经能够⽤三种⽅法缓存⽤⼾信息,下⾯给出三种⽅案的实现⽅法和优缺点
分析。

1. 原⽣字符串类型

使⽤字符串类型,每个属性⼀个键。
set user:1:name James
set user:1:age 23
set user:1:city Beijing
优点:实现简单,针对个别属性变更也很灵活。
缺点:占⽤过多的键,内存占⽤量较⼤,同时⽤⼾信息在 Redis 中⽐较分散,缺少内聚性,所以这种⽅案基本没有实⽤性。

2. 序列化字符串类型,例如 JSON 格式

set user:1 经过序列化后的⽤⼾对象字符串
优点:针对总是以整体作为操作的信息⽐较合适,编程也简单。同时,如果序列化⽅案选择合适,内存的使⽤效率很⾼。
缺点:本⾝序列化和反序列需要⼀定开销,同时如果总是操作个别属性则⾮常不灵活。

3. 哈希类型

hmset user:1 name James age 23 city Beijing
优点:简单、直观、灵活。尤其是针对信息的局部变更或者获取操作。
缺点:需要控制哈希在 ziplist 和 hashtable 两种内部编码的转换,可能会造成内存的较⼤消耗。

关于内聚和耦合

高内聚:有关联的代码紧密联系在一起

低耦合:代码的各个模块之间影响不大

3.List列表

列表类型是⽤来存储多个有序的字符串,如图 2-19 所⽰,a、b、c、d、e 五个元素从左到右组成
了⼀个有序的列表,列表中的每个字符串称为元素(element),⼀个列表最多可以存储 个元
素。在 Redis 中,可以对列表两端插⼊(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等(如图 2-19 和图 2-20 所⽰)。列表是⼀种⽐较灵活的数据结构,它可以充当栈和队列的⻆⾊,在实际开发上有很多应⽤场景

列表类型的特点:
第⼀:

列表中的元素是有序的,这意味着可以通过索引下标获取某个元素或者某个范围的元素列表,
例如要获取图 2-20 的第 5 个元素,可以执⾏ lindex user:1:messages 4 或者倒数第 1 个元素,lindex user:1:messages -1 就可以得到元素 e。

第⼆

区分获取和删除的区别,例如图 2-20 中的 lrem 1 b 是从列表中把从左数遇到的前 1 个 b 元素删
除,这个操作会导致列表的⻓度从 5 变成 4;但是执⾏ lindex 4 只会获取元素,但列表⻓度是不会变化的。

第三

列表中的元素是允许重复的,例如图 2-21 中的列表中是包含了两个 a 元素的。

常见命令
LPUSH(头插)

将⼀个或者多个元素从左侧放⼊(头插)到 list 中。
语法:
LPUSH key element [element ...]

时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度

LPUSHX

在 key 存在时,将⼀个或者多个元素从左侧放⼊(头插)到 list 中。不存在,直接返回0
语法:
1 LPUSHX key element [element ...]

时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度。

RPUSH(尾插)

将⼀个或者多个元素从右侧放⼊(尾插)到 list 中。
语法:
RPUSH key element [element ...]
时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度。

RPUSHX

在 key 存在时,将⼀个或者多个元素从右侧放⼊(尾插)到 list 中。
语法:
RPUSHX key element [element ...]
时间复杂度:只插⼊⼀个元素为 O(1), 插⼊多个元素为 O(N), N 为插⼊元素个数.
返回值:插⼊后 list 的⻓度。

LRANGE

获取从 start 到 end 区间的所有元素,左闭右闭。
语法:
LRANGE key start stop

时间复杂度:O(N)
返回值:指定区间的元素。

LPOP(头删)

从 list 左侧取出元素(即头删)。
语法:
LPOP key

时间复杂度:O(1)
返回值:取出的元素或者 nil。

RPOP(尾删)

从 list 右侧取出元素(即尾删)。
语法:
RPOP key

时间复杂度:O(1)
返回值:取出的元素或者 nil。

LINDEX

获取从左数第 index 位置的元素。
语法:
LINDEX key index

时间复杂度:O(N)
返回值:取出的元素或者 nil

LINSERT

在特定位置插⼊元素。
语法:
LINSERT key <BEFORE | AFTER> pivot element

时间复杂度:O(N)
返回值:插⼊后的 list ⻓度。

LLEN

获取 list ⻓度。
语法:
LLEN key

时间复杂度:O(1)
返回值:list 的⻓度。

LREM

指定元素精准删除

1.当count>0时,从头开始删除指定元素的次数

2.当count<0时,从尾开始删除指定元素的次数

3.当count=0时,删除全部指定的元素

LTRIM

只保留范围内的元素

LSET

根据下标修改元素

阻塞版本命令

blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,和对应⾮阻塞版本的作⽤基本⼀致,除了:

在列表中有元素的情况下,阻塞和⾮阻塞表现是⼀致的。但如果列表中没有元素,⾮阻塞版本会理
解返回 nil,但阻塞版本会根据 timeout,阻塞⼀段时间,期间 Redis 可以执⾏其他命令,但要求执
⾏该命令的客⼾端会表现为阻塞状态(如图 2-22 所⽰)。

命令中如果设置了多个键,那么会从左向右进⾏遍历键,⼀旦有⼀个键对应的列表中可以弹出元
素,命令⽴即返回。

如果多个客⼾端同时多⼀个键执⾏ pop,则最先执⾏命令的客⼾端会得到弹出的元素。

BLPOP

LPOP 的阻塞版本。
语法:
BLPOP key [key ...] timeout
时间复杂度:O(1)
返回值:取出的元素或者 nil。

BRPOP

RPOP 的阻塞版本。
语法:
BRPOP key [key ...] timeout
时间复杂度:O(1)
返回值:取出的元素或者 nil。

内部编码

列表类型的内部编码有两种:

ziplist(压缩列表):

当列表的元素个数⼩于 list-max-ziplist-entries 配置(默认 512 个),同时
列表中每个元素的⻓度都⼩于 list-max-ziplist-value 配置(默认 64 字节)时,Redis 会选⽤
ziplist 来作为列表的内部编码实现来减少内存消耗。

linkedlist(链表):

当列表类型⽆法满⾜ ziplist 的条件时,Redis 会使⽤ linkedlist 作为列表的内部实现。

quicklist

每个节点都是一个压缩列表,以链表的形式连接起来

使⽤场景
作为数组
消息队列

如图 2-22 所⽰,Redis 可以使⽤ lpush + brpop 命令组合实现经典的阻塞式⽣产者-消费者模型队列,⽣产者客⼾端使⽤ lpush 从列表左侧插⼊元素,多个消费者客⼾端使⽤ brpop 命令阻塞式地从队列中"争抢" 队⾸元素。通过多个客⼾端来保证消费的负载均衡和⾼可⽤性。

分频道的消息队列
如图 2-23 所⽰,Redis 同样使⽤ lpush + brpop 命令,但通过不同的键模拟频道的概念,不同的消费者可以通过 brpop 不同的键值,实现订阅不同频道的理念。

微博 Timeline

每个⽤⼾都有属于⾃⼰的 Timeline(微博列表),现需要分⻚展⽰⽂章列表。此时可以考虑使⽤
列表,因为列表不但是有序的,同时⽀持按照索引范围获取元素。
1)每篇微博使⽤哈希结构存储,例如微博中 3 个属性:title、timestamp、content:
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx
2)向⽤⼾ Timeline 添加微博,user:<uid>:mblogs 作为微博的键:
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9
3)分⻚获取⽤⼾的 Timeline,例如获取⽤⼾ 1 的前 10 篇微博:
keylist = lrange user:1:mblogs 0 9
for key in keylist {
hgetall key
}
此⽅案在实际中可能存在两个问题:

  1. 1 + n 问题。即如果每次分⻚获取的微博个数较多,需要执⾏多次 hgetall 操作,此时可以考虑使⽤pipeline(流⽔线)模式批量提交命令,或者微博不采⽤哈希类型,⽽是使⽤序列化的字符串类型,使⽤ mget 获取。
  2. 分裂获取⽂章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分。
4.set集合

集合类型也是保存多个字符串类型的元素的,但和列表类型不同的是,集合中 1)元素之间是⽆序
的 2)元素不允许重复,如图 2-24 所⽰。⼀个集合中最多可以存储 个元素。Redis 除了⽀持
集合内的增删查改操作,同时还⽀持多个集合取交集、并集、差集,合理地使⽤好集合类型,能在实际开发中解决很多问题。
set这个术语有不同的含义,如果是集合的话,就是把有关联的数据联系在一起,集合中的元素是无序的,集合中的元素是唯一的,集合中的每个元素都是string类型的

基本命令
SADD

将⼀个或者多个元素添加到 set 中。注意,重复的元素⽆法添加到 set 中。
语法:
SADD key member [member ...]

时间复杂度:O(1)
返回值:本次添加成功的元素个数

SMEMBERS

获取⼀个 set 中的所有元素,注意,元素间的顺序是⽆序的。
语法:
SMEMBERS key

时间复杂度:O(N)
返回值:所有元素的列表。

SISMEMBER

判断⼀个元素在不在 set 中。
语法:
SISMEMBER key member

时间复杂度:O(1)
返回值:1 表⽰元素在 set 中。0 表⽰元素不在 set 中或者 key 不存在。

SCARD

获取⼀个 set 的基数(cardinality),即 set 中的元素个数。
语法:
SCARD key

时间复杂度:O(1)
返回值:set 内的元素个数。

SPOP

从 set 中删除并返回⼀个或者多个元素。注意,由于 set 内的元素是⽆序的,所以取出哪个元素实际是未定义⾏为,即可以看作随机的。
语法:
SPOP key [count]

时间复杂度:O(N), n 是 count
返回值:取出的元素。
srandmember
从set中随机获取一个元素,但是不会移除该元素

SMOVE

将⼀个元素从源 set 取出并放⼊⽬标 set 中。
语法:
SMOVE source destination member

时间复杂度:O(1)
返回值:1 表⽰移动成功,0 表⽰失败

SREM

将指定的元素从 set 中删除。
语法:
SREM key member [member ...]

时间复杂度:O(N), N 是要删除的元素个数.
返回值:本次操作删除的元素个数。

集合间操作

交集(inter)、并集(union)、差集(diff)的概念如图 2-25 所⽰。

SINTER

获取给定 set 的交集中的元素。
语法:
SINTER key [key ...]

时间复杂度:O(N * M), N 是最⼩的集合元素个数. M 是最⼤的集合元素个数.
返回值:交集的元素。

SINTERSTORE

获取给定 set 的交集中的元素并保存到⽬标 set 中。
语法:
SINTERSTORE destination key [key ...]

时间复杂度:O(N * M), N 是最⼩的集合元素个数. M 是最⼤的集合元素个数.
返回值:交集的元素个数。

SUNION

获取给定 set 的并集中的元素。
语法:
SUNION key [key ...]

时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:并集的元素。

SUNIONSTORE

获取给定 set 的并集中的元素并保存到⽬标 set 中。
语法:
1 SUNIONSTORE destination key [key ...]

时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:并集的元素个数。

SDIFF

获取给定 set 的差集中的元素。
语法:
SDIFF key [key ...]


时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:差集的元素。

SDIFFSTORE

获取给定 set 的差集中的元素并保存到⽬标 set 中。
语法:
SDIFFSTORE destination key [key ...]

时间复杂度:O(N), N 给定的所有集合的总的元素个数.
返回值:差集的元素个数

命令小结
内部编码

集合类型的内部编码有两种:

intset(整数集合):

当集合中的元素都是整数并且元素的个数⼩于 set-max-intset-entries 配置
(默认 512 个)时,Redis 会选⽤ intset 来作为集合的内部实现,从⽽减少内存的使⽤。

hashtable(哈希表):

当集合类型⽆法满⾜ intset 的条件时,Redis 会使⽤ hashtable 作为集合
的内部实现。

使⽤场景

集合类型⽐较典型的使⽤场景是标签(tag)。例如 A ⽤⼾对娱乐、体育板块⽐较感兴趣,B ⽤⼾
对历史、新闻⽐较感兴趣,这些兴趣点可以被抽象为标签。有了这些数据就可以得到喜欢同⼀个标签的⼈,以及⽤⼾的共同喜好的标签,这些数据对于增强⽤⼾体验和⽤⼾黏度都⾮常有帮助。 例如⼀个电⼦商务⽹站会对不同标签的⽤⼾做不同的产品推荐。

1.使用set来保存用户的标签
2.使用set来计算共同好友,基于集合求交集


3.使用set来计算uv

Zset 有序集合

有序集合相对于字符串、列表、哈希、集合来说会有⼀些陌⽣。它保留了集合不能有重复成员的
特点,但与集合不同的是,有序集合中的每个元素都有⼀个唯⼀的浮点类型的分数(score)与之关联,着使得有序集合中的元素是可以维护有序性的,但这个有序不是⽤下标作为排序依据⽽是⽤这个分数。如图 2-26 所⽰,该有序集合显⽰了三国中的武将的武⼒

有序集合提供了获取指定分数和元素范围查找、计算成员排名等功能,合理地利⽤有序集合,可
以帮助我们在实际开发中解决很多问题。

常见命令
zadd

添加或者更新指定的元素以及关联的分数到 zset 中,分数应该符合 double 类型,+inf/-inf 作为正负
极限也是合法的。
ZADD 的相关选项:
XX:仅仅⽤于更新已经存在的元素,不会添加新元素。
NX:仅⽤于添加新元素,不会更新已经存在的元素。
CH:默认情况下,ZADD 返回的是本次添加的元素个数,但指定这个选项之后,就会还包含本次更新的元素的个数。
INCR:此时命令类似 ZINCRBY 的效果,将元素的分数加上指定的分数。此时只能指定⼀个元素和分数。

ZCARD

获取⼀个 zset 的基数(cardinality),即 zset 中的元素个数。

ZCOUNT

返回分数在 min 和 max 之间的元素个数,默认情况下,min 和 max 都是包含的,可以通过 (,表示开区间

ZRANGE

返回指定区间⾥的元素,分数按照升序。带上 WITHSCORES 可以把分数也返回。

ZREVRANGE

返回指定区间⾥的元素,分数按照降序。带上 WITHSCORES 可以把分数也返回。
备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。

ZRANGEBYSCORE

返回分数在 min 和 max 之间的元素,默认情况下,min 和 max 都是包含的,可以通过 ( 排除。
备注:这个命令可能在 6.2.0 之后废弃,并且功能合并到 ZRANGE 中。

ZPOPMAX

删除并返回分数最⾼的 count 个元素。

BZPOPMAX

ZPOPMAX 的阻塞版本。

ZPOPMIN

删除并返回分数最低的 count 个元素。

BZPOPMIN

ZPOPMIN 的阻塞版本。

ZRANK

返回指定元素的排名,升序。

ZREVRANK

返回指定元素的排名,降序。

ZSCORE

返回指定元素的分数。

ZREM

删除指定的元素。

ZREMRANGEBYRANK

按照排序,升序删除指定范围的元素,左闭右闭。

ZREMRANGEBYSCORE

按照分数删除指定范围的元素,左闭右闭。

ZINCRBY

为指定的元素的关联分数添加指定的分数值

集合间操作
ZINTERSTORE(交集)

求出给定有序集合中元素的交集并保存进⽬标有序集合中,在合并过程中以元素为单位进⾏合并,元素对应的分数按照不同的聚合⽅式和权重得到新的分数。

时间复杂度:O(N*K)+O(M*log(M)) N 是输⼊的有序集合中, 最⼩的有序集合的元素个数; K 是输⼊了⼏个有序集合; M 是最终结果的有序集合的元素个数.

ZUNIONSTORE并集

求出给定有序集合中元素的并集并保存进⽬标有序集合中,在合并过程中以元素为单位进⾏合并,元素对应的分数按照不同的聚合⽅式和权重得到新的分数。
语法:
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight
[weight ...]] [AGGREGATE <SUM | MIN | MAX>]
时间复杂度:O(N)+O(M*log(M)) N 是输⼊的有序集合总的元素个数; M 是最终结果的有序集合的元素个数.
返回值:⽬标集合中的元素个数

命令⼩结
内部编码

有序集合类型的内部编码有两种:

ziplist(压缩列表):
当有序集合的元素个数⼩于 zset-max-ziplist-entries 配置(默认 128 个), 同时每个元素的值都⼩于 zset-max-ziplist-value 配置(默认 64 字节)时,Redis 会⽤ ziplist 来作

为有序集合的内部实现,ziplist 可以有效减少内存的使⽤。

skiplist(跳表):

当 ziplist 条件不满⾜时,有序集合会使⽤ skiplist 作为内部实现,因为此时ziplist 的操作效率会下降
1)当元素个数较少且每个元素较⼩时,内部编码为 ziplist:
2)当元素个数超过 128 个,内部编码 skiplist:
3)当某个元素⼤于 64 字节时,内部编码 skiplist:

使⽤场景

有序集合⽐较典型的使⽤场景就是排⾏榜系统。例如常⻅的⽹站上的热榜信息,榜单的维度可能
是多⽅⾯的:按照时间、按照阅读量、按照点赞量。本例中我们使⽤点赞数这个维度,维护每天的热榜:

相关推荐
fpcc5 小时前
redis6.0之后的多线程版本的问题
c++·redis
刘九灵5 小时前
Redis ⽀持哪⼏种数据类型?适⽤场景,底层结构
redis·缓存
登云时刻8 小时前
Kubernetes集群外连接redis集群和使用redis-shake工具迁移数据(一)
redis·kubernetes·bootstrap
煎饼小狗14 小时前
Redis五大基本类型——Zset有序集合命令详解(命令用法详解+思维导图详解)
数据库·redis·缓存
秋意钟16 小时前
缓存雪崩、缓存穿透【Redis】
redis
简 洁 冬冬16 小时前
046 购物车
redis·购物车
雯0609~16 小时前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
soulteary16 小时前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
wkj00117 小时前
php操作redis
开发语言·redis·php
菠萝咕噜肉i18 小时前
超详细:Redis分布式锁
数据库·redis·分布式·缓存·分布式锁