【redis】Redis五种常用数据类型和内部编码,以及对String字符串类型的总结

˃͈꒵˂͈꒱ write in front ꒰˃͈꒵˂͈꒱

ʕ̯•͡˔•̯᷅ʔ大家好,我是xiaoxie.希望你看完之后,有不足之处请多多谅解,让我们一起共同进步૮₍❀ᴗ͈ . ᴗ͈ აxiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客

本文由xiaoxieʕ̯•͡˔•̯᷅ʔ 原创 CSDN 如需转载还请通知˶⍤⃝˶​
个人主页:xiaoxieʕ̯•͡˔•̯᷅ʔ—CSDN博客

系列专栏:​ xiaoxie的redis学习系列专栏——CSDN博客●'ᴗ'σσணღ ​
我的目标:"团团等我💪( ◡̀_◡́ ҂)" 

( ⸝⸝⸝›ᴥ‹⸝⸝⸝ )欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​+关注(互三必回)!

目录

​编辑 一.五种常用数据类型和内部编码

1.五种常用数据类型和内部编码

2.内部编码方式

1.string 类型 

2.hash类型

3.list类型

4.set类型

5.zset(有序集合)类型

6.Redis这样设计有两个好处:

3.单线程架构

 redis单线程为什么可以做到高性能?(面试重点)

二.字符串类型的总结

1.字符串类型的常用命令

1.set 及其选项

1.普通的set语句

2. nx选项

3. xx选项

4. ex seconds 或 px milliseconds 选项

2.get命令

3.mset,mget

2.计数命令

1.incr / decr 

 2.incrby / decrby

3.incrbyfloat

 3.其他命令

1.APPEND

2.getrange

3.setrange

4. strlen

​编辑 5.查看内部编码

 4.典型使用场景

1.缓存功能

2.计数(Counter)功能

 3.手机验证码 

4.共享会话(Session)


 

 一.五种常用数据类型和内部编码

redis我们在之前提到过,它是 key value 以键值对的存储数据的,并且,key 的数据类型是固定,只为字符串类型,而value是可以以多种的数据类型来吧表示的,这里只介绍五种常用的数据类型和内部的编码.它们分别是. String类型,哈希类型,列表类型,集合类型,有序集合类型.它们的对外的表示类型为

1.五种常用数据类型和内部编码

1.字符串类型可以类比于 java 的 String以及 C++的std :: string ,不过其编码方式不一定会是String类型,redis 会根据数据的不同自适应编码方式,这个下文就会介绍到.

2.哈希类型 可以类比于 java 的 HashMap以及 C++的std :: unordered_map,还是一样其编码方式不一定是和Java 或 C++的一样,会根据数据的不同自适应编码方式.

3.列表类型可以类比于java 的 List 以及 C++的std :: deque,有序的字符串集合,可以作为栈或队列使用其编码方式也是根据数据的不同自适应编码方式

4.集合类型可以类比于Java 的 set 和 c++ 的std :: set 也就是说,它是有去重功能并且是无序的,其编码方式也是根据数据的不同自适应编码方式.

5.有序集合类型,在Java 或者是 C++中找不到可以类比的数据结构(不包括在第三方库),不过它大致,是这样构成的:

它除了存储member 之外(看成图上的张三,李四之类的数据),还需要存储一个 Sorce(图上的99,97)这个数据就代表着权重,集合就可以理解为是按照权重来排序的它结合了集合的去重特性和列表的有序特性。

2.内部编码方式

首先博主这里强调一点,就是Redis 对使用者承诺它就相当于hash表 对数据的删除,增加,修改,查找的时间复杂度都为O(1).但是它背后的实现原理,也就是内部的编码方式也不一定是,标准的hash表,但是可能是使用别的数据结构来实现这些操作,以优化内存使用、提高处理速度或适应不同的使用场景,但是时间复杂度依然是为O(1).

Redis 在底层实现上述的这些数据类型的时候,会在源码层面上,对不同的情况做出优化也就是使用特定的数据结构(改变内部的编码方式)以提高性能.

总结来说,同一个数据类型,其背后的编码方式可能是不同的,会根据特点的场景做出优化,并且它是个自适应的过程,我们是感知不到的.

1.string 类型 

1.raw: 就是最基础的字符串类型,其底层就是字节数组,这里强调一下,redis存储字符串就是存储二进制字节,而不是字符,这就使得 Redis 的字符串类型可以存储任何二进制数据,这使得它非常灵活,可以用于存储图像、音频、视频、序列化的对象等。需要注意:当存储汉字或其他 Unicode 字符时,需要注意它们可能占用多个字节。例如,UTF-8 编码的汉字通常占用 3 个字节。Redis 不会对这些字符进行任何特殊处理,它们会被直接存储为二进制数据,在使用这些数据时我们要确保数据正确的话要进行编码和解码操作

2.int: redis通常可以实现"计数"这样的功能,而当redis存储的数据类型为整数,并且存储的值是一个范围在 long 类型之内的整数时(即,通常为 int64,在 Java 中为 long其内部编码方式就会被自适应为 int 类型.这里要强调的是当存储浮点数时,Redis 实际上会将其转换为字符串形式.而不是自适应为 int内部编码方式.

3.embstr: 针对短的字符串进行特殊的优化,当字符串的长度少于一定的字节时,这里就不说明是多少字节了,因为像这样数据每个版本的redis都可能不一样,并且,这个数值也是可以更改的,例如,可能因为一些业务上的需求,就可能需要对这个数值进行修改,所以博主这里就不说明是多少字节了只需要知道:

  1. 内部结构:embstr 编码的字符串包含数据长度、数据本身和一个尾部的空字节(用于简单的字符串终止符)。

  2. 当 embstr 编码的字符串增长超过一定长度时,Redis 会将其转换为其他编码方式,如简单的动态字符串(SDS)。
  3. embstr 编码的字符串直接存储在内存分配块中,避免了额外的内存分配和指针管理,从而节省了内存。

这几点 并且知道embstr 编码是 Redis 为了提高性能和内存效率而进行的一项优化即可.

2.hash类型

1. hashtable :最基本的哈希表同时注意这里的实现方式和Java 或者是 C++的可能不太一样,不过思想是差不多的,

2.ziplist 当哈希中的字段数量较少时,可能会自适应使用 ziplist(压缩列表)的内部编码方式来存储,这是一种连续分配的、紧凑的存储格式,这样就使得占用的内存空间减少了.

3.list类型

1.linkedList: 也就是以链表的方式来存储.

2.ziplis:当列表存储的数据较少时,就会自适应的使用 ziplist(压缩列表)的内部编码方式来存储数据.

3.quicklist: 自从redis 3.2之后redis 在使用list类型时引入了全新的内部编码方式即 quicklist,这个数据结构就牛逼了,它同时结合了链表和压缩列表的优点, quicklist 是一个链表(双端),同时它存储的每个元素是一个个的压缩链表,所以它同时支持大量数据存储和快速访问的场景.

4.set类型

1.hashtable :这是 set 最基本的实现方式,使用哈希表来存储集合中的元素,支持快速的添加、删除和成员存在性检查操作。每个元素都是哈希表中的一个键,值通常是一个固定值,用于指示元素的存在

2.intset::当集合中只包含整数值时,Redis 会自适应的使用 intset 编码。intset 是一种紧凑的数组实现,它根据集合内元素的数量和数值大小,动态在编码方式(例如,从单个字节到更宽的整型)间转换,以节省空间。这种方式特别适合整数集合,因为不需要额外的哈希计算,并且存储更为紧凑。

5.zset(有序集合)类型

1.skiplist : 有序集合之所以具有列表的有序以及集合的去重性就是基于这个数据机构跳跃表是一种随机化的数据结构,它通过在列表中添加多级索引来加速查询操作。每个节点包含指向其他节点的指针,这些指针可以让算法“跳跃式”地前进,从而快速定位到目标元素。这种结构在保持有序集合特性的同时,还能提供对数据的高效访问。

2.ziplist:当有序集合的元素数量较少,且每个元素的成员(member)和分数(score)都较短时,Redis 会自适应 ziplist 作为 zset 的内部编码方式.

6.Redis这样设计有两个好处:

  1. 无感升级的内部编码改进:Redis设计允许其内部编码的优化和更新,而不影响外部接口和用户使用的数据结构及命令。这意味着,当Redis开发者引入了更高效的内部编码方式,如在Redis 3.2中引入的quicklist来优化list类型,用户的代码和数据结构无需任何改变就能享受到性能提升的好处。这种机制极大降低了软件升级的成本,确保了向后兼容性,使得技术迭代更加平滑。

  2. 场景适配的编码选择:Redis为不同的数据结构提供了多种内部编码实现,使得它能够根据实际数据特性和操作场景自动选择最合适的编码方式。例如,ziplist因其紧凑的内存占用,在处理少量元素时非常高效,但随着元素数量增长,其性能会下降,这时Redis会自动切换到linkedlist编码以保证操作性能,这一转换过程对用户完全透明。这样的设计确保了Redis能够在不同规模和不同类型的数据集上都能提供最优的性能表现,同时也保持了使用的简便性。

通过这样的设计哲学,Redis不仅实现了高性能的数据处理能力,还保证了高度的灵活性和易用性,使得开发者能够专注于业务逻辑,而不必过多担心底层数据结构的优化问题。

3.单线程架构

redis使用单线程架构来实现高性能的内存数据库服务,也就是说,reids服务端是单线程的处理客服端的请求并响应.

例如有三个客户端同时向服务端发起请求

虽然宏观上看是三个客户端同时向服务端发起请求,但其实在内部里,redis服务端是这样处理客户端的请求的

尽管听起来可能与直觉相悖,即多线程或多进程通常与高性能相关联,但Redis通过这一策略实现了高效的内存数据库服务。以下是几个关键点解释.

 redis单线程为什么可以做到高性能?(面试重点)

  1. 基于内存操作:Redis 数据存储在内存中,这意味着读写操作都非常迅速,远超硬盘I/O的速度。大部分请求直接在内存中完成,没有磁盘访问延迟。

  2. 避免多线程开销:单线程避免了多线程环境下的上下文切换成本和线程同步的开销,比如锁的竞争和释放。这减少了CPU在不同线程间切换的时间,也消除了多线程编程中可能出现的竞态条件和死锁问题。

  3. I/O多路复用:Redis 使用I/O多路复用技术(如epoll on Linux),能够在一个线程里同时监控多个客户端连接,有效地管理这些连接上的事件(如读、写就绪)。当有事件发生时,Redis线程可以立即响应,这大大提升了处理并发连接的效率,而不需要为每个连接创建单独的线程。

  4. 简单且高效的命令执行:由于是单线程,Redis 可以简化其内部实现,不需要复杂的锁机制来保护数据结构。所有命令串行执行,确保了操作的原子性,同时也简化了编程模型和调试过程。

  5. 数据结构优化:Redis 提供了多种经过优化的数据结构(如哈希表、跳跃表等),这些数据结构本身就是为高性能设计的,进一步增强了单线程处理的效率。

总之,尽管是单线程,但通过将数据存储在内存中、利用I/O多路复用技术以及优化的数据结构,Redis成功地实现了高吞吐量和低延迟的服务,适用于大量读写操作的场景,特别是那些对响应时间和数据一致性有严格要求的应用。 

同时需要注意的是,虽然redis使用单线程架构一样可以做到高性能,但是由于是单线程,要是某个操作的执行时间过长,就长时间阻塞其他的操作,例如在生产环境下使用 

keys *

这样需要消耗大量时间的操作,阻塞其他的操作,使得MySql需要一下子处理大量的请求.所以我们在使用redis 时,要特别注意这点.

同时在这里用一个小栗子来说明一下I/O多路复用

理解I/O多路复用,可以通过一个生活中的类比来形象地说明。想象一下,你是一位繁忙的餐厅服务员,负责同时服务多桌客人。每桌客人都可能会有各种需求,比如点餐、加水、结账等。如果按照传统的做法,你每次只能专心服务一桌客人,直到他们当前的需求被完全满足,然后再转向下一桌。这样一来,其他等待服务的桌次就会造成等待时间较长,整体效率低下。

而采用I/O多路复用的做法就像是这位服务员有了“分身术”或者“超级注意力”。你不再是一次只服务于一桌客人,而是快速地在各个桌之间巡视,询问每桌是否需要服务,但并不停留等待具体任务完成。当某桌客人提出需求时,你会记录下来,然后继续巡视。一旦有任务(比如厨房通知某道菜好了,或者某桌客人示意结账)准备好,你立刻知道并快速响应,而不会让其他桌的客人等待过长时间。

这个过程就好比操作系统中的I/O多路复用技术,服务器程序(服务员)通过一个系统调用(如select、poll或epoll)同时监控多个文件描述符(各桌客人),这些文件描述符代表了不同的I/O操作(如网络连接的读写)。当任何一描述符上有事件发生(如数据可读或写缓冲区有空闲),系统就会通知程序,程序随即针对性地处理该事件,而不是为每个描述符分配一个单独的线程或进程等待。这样,即使在单线程环境下,也能高效地处理大量并发的I/O操作,提高了系统的响应速度和资源利用率。

二.字符串类型的总结

Redis中的字符串类型是其核心且最为基础的数据结构,需着重留意几个关键点:

  1. 字符串作为键的基石:在Redis数据库内部,不论数据结构多么复杂,所有键(Keys)均统一采用字符串形式。这一设计不仅维护了数据访问接口的一致性,也意味着掌握字符串类型是理解其余四种高级数据结构(列表、集合、散列、有序集合)的前提。

  2. 多样的值类型支持:字符串值展现了极高的灵活性,能够承载各种类型的数据:不仅是常规文本字符串,还包括如JSON、XML这类结构化数据格式;支持整数、浮点数等数值类型;甚至可以是图片、音频、视频文件等二进制大数据块。值得注意的是,尽管用途广泛,但单个字符串值的大小限制为不超过512MB,确保了数据操作的高效性与内存使用的合理性。

1.字符串类型的常用命令

1.set 及其选项

语法:

一些常用的SET命令选项及其功能:

  1. ex seconds 或 px milliseconds:设置键的过期时间。ex后面跟秒数,px后面跟毫秒数。过了指定的时间后,键会自动被删除。

  2. nx:仅当键不存在时才设置键的值。这可以用来实现"设置-if-not-exists"操作,类似于某些数据库的"INSERT IF NOT EXISTS"。

  3. XX:仅当键已经存在时才设置键的值。这可以用来实现"更新-if-exists"操作。

注意:由于带选项的SET命令可以被setnx 、 setex 、psetex 等命令代替,所以之后的版本中,Redis可能进⾏合并 

1.普通的set语句

将一个键(key)关联到一个值(value),如果键已经存在,则更新其对应的值。

语法:

set key value

示例 存储 hello 1

set hello 1

如果hello 这个键之前存在,就会覆盖之前的value

2. nx选项

仅当键不存在时才设置键的值,如果成功设置就返回 OK,如果键已经存在就返回 nil

语法:

set key value nx
#或者
setnx key value#返回值可能会有所差别

当键不存在时 

 当键存在时

3. xx选项

仅当键存在时才设置键的值,即如果key之前不存在设置不执行,如果成功设置就返回 OK,如果键不存在就返回 nil

语法:

set key value xx

当键存在时

当键不存在时 

set set nx set xx 总结图示

4. ex seconds 或 px milliseconds 选项

设置键的过期时间。ex后面跟秒数,px后面跟毫秒数。过了指定的时间后,键会自动被删除

语法:

set key value ex 秒
set key value px 毫秒
#或者时
setex key 秒  value 
psetex key 毫秒 value

 ex后面跟秒数

 px后面跟毫秒数

2.get命令

获取key对应的value。如果key不存在,返回nil。如果value的数据类型不是string,会报错。

语法:

get key

 

3.mset,mget

 一次性获取多个key,和一次性设置多个key value

语法:

mset key value key value ...
mget key ,key,key...

同时这里需要提醒一点,由于每次get / set 都是rides客户端通过网络访问redis服务端,就要消耗系统资源,以及网络开销时间 所以我们使⽤mget/mset可以有效地减少了网络时间,这里也建议,尽量使用 mget / mset,一次获取多个,或设置多个,但注意,由于redis是单线程架构的,所以不建议,设置或获取太多个数据,以防一个操作太久,阻塞其他操作.  

2.计数命令

上文提到了,如果value 为整数的话,redis就有计数的功能,这里就介绍一些用于计数的命令

1.incr / decr 

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

语法:

incr key 
decr key

 2.incrby / decrby

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

语法:

incrby key n
decrby key n 

 这里注意:如果n为正数,那么incrby 就是加,为负数,incrby 就是减,decrby 同理

n为正数:

n为负数:

3.incrbyfloat

将key对应的string表示的浮点数加上对应的值。如果对应的值是负数,则视为减去对应的值。如果key不存在,则视为key对应的value是0。如果key对应的不是string,则报错。允许采⽤科学计数法表示浮点数 

语法:

incrbyfloat key n

这里n和incrby一样正数为加,负数为减,并且,浮点数只有 incrbyfloat.

 3.其他命令

1.APPEND

如果key已经存在并且是⼀个string,命令会将value追加到原有string的后边。如果key不存在,则效果等同于SET命令。

语法:

append key value

时间复杂度:O(1)

返回值:追加后的value的长度 

2.getrange

返回key对应的string的子串,由start和end确定(左闭右闭)。可以使用负数表示倒数。-1代表倒数第⼀个字符,-2代表倒数第⼆个,其他的与此类似。超过范围的偏移量会根据string的长度调整成正确的值

 语法:

getrange key star end 

时间复杂度:O(n)这里的n是[star,end]区间的长度,也可以看做O(1)

返回值:value的子串的长度

 

注意:

在Redis中,GETRANGE命令处理字符串数据时,并不会直接区分字符编码或字符类型(如汉字或其他Unicode字符)。它是基于字节序列来操作的,这意味着如果你的键中存储的是多字节字符(例如UTF-8编码的汉字),你需要按照字节来计算start和end参数。

UTF-8编码中,一个汉字通常占用3个字节。因此,如果你要获取字符串中特定位置的汉字,你需要确保start和end参数正确地对齐到汉字的起始字节。例如,如果你想获取第一个汉字,start应设置为0;若想获取第二个汉字,由于第一个汉字占用了前3个字节,start应设置为3,以此类推。

3.setrange

覆盖字符串的⼀部分,从指定的偏移开始。 

语法:

setrange key offset value

时间复杂度:O(N),N为value的长度.由于⼀般给的value⽐较短,通常视为O(1).

返回值:替换后的string的长度。

 注意:

如汉字或其他Unicode字符要注意转换,和 getrange 差不多这里就不过多的解释了.

4. strlen

获取key对应的string的长度。当key存放的类不是string时,报错。

语法:

strlen key

时间复杂度:O(1)

返回值:string的长度。或者当key不存在时,返回0。

 5.查看内部编码

Redis会根据当前值的类型和⻓度动态决定使用哪种内部编码实现,可以通过命令查看哪种内部编码实现

语法:

object encoding key

 4.典型使用场景

1.缓存功能

Redis+MySQL组成的缓存存储架构

  1. 用户:

    用户是系统的终端用户,他们通过 Web 服务与应用程序交互。
  2. Web 服务:

    Web 服务是用户与应用程序交互的接口,它处理用户的请求并返回响应。
  3. 业务层:

    业务层包含应用程序的业务逻辑,负责处理用户的请求并返回相应的结果。
  4. 缓存层(Redis):

    • 缓存层使用 Redis 作为缓存数据库,用于存储频繁访问的数据以提高性能。
    • 当用户请求数据时,系统首先检查 Redis 中是否存在该数据(称为缓存命中,hit)。
    • 如果 Redis 中存在数据(缓存命中),则直接返回该数据给用户。
    • 如果 Redis 中不存在数据(缓存未命中,miss),则需要从存储层获取数据。
  5. 存储层(MySQL):

    • 存储层使用 MySQL 作为持久化数据库,存储应用程序的所有数据。
    • 当缓存未命中时,业务层会从 MySQL 获取数据,然后将数据写入 Redis 缓存,并返回数据给用户。

2.计数(Counter)功能

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

  1. 用户播放视频:

    用户在视频网站上播放视频,视频通常有唯一的编号或标识符,例如 "5253"。
  2. Redis 作为缓存和计数器:

    • 视频网站的后端使用 Redis 来存储视频的播放次数。
    • 每个视频的播放次数存储在一个独立的键中,键名通常采用格式 "video:" + 视频编号,例如 "video:1"、"video:3582"、"video:5253"。
  3. 获取和更新播放次数:

    • 当用户播放视频时,系统执行一个 Redis 命令来增加对应视频的播放计数器。
    • 使用 INCR 命令来原子地增加键 "video:5253" 的值。INCR 命令将键的值增加一。
  4. 异步数据同步:

    • 为了提高性能,播放次数的增加通常是实时的,但将播放次数同步到其他数据源(如视频存储或统计数据仓库)可能会采用异步方式。
    • 这意味着在 Redis 中更新播放次数后,系统会安排一个后台任务或使用消息队列来异步处理数据同步。
  5. 数据持久化:

    尽管 Redis 是一个内存数据库,但它提供了数据持久化的选项,以防止在系统故障时丢失数据。

 3.手机验证码 

Redis 可以有效地用于存储和管理手机验证码,主要是因为它提供的原子操作、快速响应和易于使用的数据结构。以下是使用 Redis 来处理手机验证码的一些关键步骤和考虑因素:

  1. 生成验证码:

    通常,验证码是一个随机生成的数字或字母字符串,长度和复杂度可以根据安全需求来设定。
  2. 设置验证码到 Redis:

    • 使用 Redis 的 SET 命令将验证码与用户的手机号码关联起来。例如:
      SET phone:123456 "验证码1234" EX 300
    • 其中 phone:123456 是一个唯一标识用户手机的键,"验证码1234" 是验证码,EX 300 表示验证码的有效期为 300 秒。
  3. 验证过程:

    • 当用户输入验证码后,应用程序会向 Redis 查询该验证码是否正确,并是否在有效期内。
    • 使用 GET 命令来获取存储的验证码,并与用户输入的验证码进行比对。
  4. 安全性:

    • 为了提高安全性,可以为验证码设置一个较短的有效期,例如几分钟。
    • 也可以考虑使用 Redis 的散列(Hash)数据结构来存储验证码,并将用户的手机作为键。
  5. 验证码使用后的处理:

    • 一旦验证码被使用,应该立即从 Redis 中删除,以防止重复使用。
    • 使用 DEL 命令来删除已使用的验证码。
  6. 容错性和高可用性:

    在高流量的应用中,可能需要使用 Redis 集群来提高容错性和可用性。
  7. 监控和日志:

    记录验证码的生成、验证尝试和使用情况,以便于监控和审计。
  8. 防止暴力破解:

    实现限制机制,如在一定时间内只允许用户尝试一定次数的验证码输入。

4.共享会话(Session)

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

为了解决这个问题,可以使用Redis将用户的Session信息进行集中管理,在这种模式下,只要保证Redis是高可用和可扩展性的,⽆论用户被均衡到哪台Web服务器上,都集中从Redis中查询、更新Session信息。

以上介绍了使⽤Redis的字符串数据类型可以使⽤的⼏个场景,但其适⽤场景远不⽌于此,开发⼈员可以结合字符串类型的特点以及提供的命令,充分发挥⾃⼰的想象⼒,在⾃⼰的业务中去找到合适的场景去使⽤Redis的字符串类型

感谢你的收看