欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

学习之Redis(一)

程序员文章站 2022-10-29 19:56:07
一、redis简介 一般学习,最好先去官网,之所以建议看官网,是因为这是一手的学习资料,其他资料都最多只能算二手,一手资料意味着最权威,准确性最高。https://redis.io/topics/introduction。如果像我一样,英语不好的童鞋,不要紧,咋们用Chrome浏览器,翻译成中文。E ......

一、redis简介

  一般学习,最好先去官网,之所以建议看官网,是因为这是一手的学习资料,其他资料都最多只能算二手,一手资料意味着最权威,准确性最高。。如果像我一样,英语不好的童鞋,不要紧,咋们用chrome浏览器,翻译成中文。eumm。。。来看看官网给的解释:“redis is an open source (bsd licensed), in-memory data structure store, used as a database, cache and message broker.” ,第一句就告诉我们,redis是什么:redis是一个开源的,基于内存的数据结构存储,可用作于数据库缓存息中间件

1.1、为什么使用redis?

  由官网可知:redis是基于内存,常用于缓存的一种技术,并redis存储方式是以 key-value 的形式。等等?key-value??这个不就是java中map容器的特性吗?那为什么还用redis呢?

  • java实现的map是本地缓存,只能存在创建他的程序中。最主要的特点是轻量以及快速。而且实例多的情况下,每个实例都需要各自保存一份缓存缓存不具有一致性
  • redis实现的是分布式缓存,如果有多个实例机器,每个实例共享一份缓存缓存具有一致性
  • java中map不是专业做缓存的,jvm内存太大容易挂掉,所以一般用来做容器存储临时数据,缓存随着jvm销毁而结束。
  • redis是专门做缓存的,缓存可以持久化,可以将缓存的数据保存在硬盘中,redis重启之后就可以恢复。但是map是内存对象,程序重启数据就没有了。
  • redis可以处理每秒百万级的并发,map只是一个普通的对象。

1.2、为什么要用缓存

  如果我们的网站出现了性能问题(访问时间慢),一般是由于数据库扛不住了。因为一般的数据库的读写都是经过磁盘的,磁盘的读写相当于内存来说非常慢了。参考资料:让cpu告诉你硬盘和网络到底有多慢:。用过mybatis和hibernate的同学都知道,他们有一级缓存、二级缓存这样的功能(实质就是本地缓存),目的就是为了不用每次读取数据的时候,都去数据库查询

 


 

二、redis的对象和数据结构

  注:本篇博文不讲述redis命令的使用方式,具体的使用请查看api.(redis命令参考:)

【对象】

  redis使用对象来表示数据库中的键和值,每次在redis中新建一个键值对的时候,至少会创建出两个对象。一个对象用作键值对的键(键对象),一个对象用作键值对的(值对象)。redis中的每种对象都由对象结构(redisobject) 与对应编码的 数据结构 组合而成,redis支持5种对象类型,分别是字符串(string)列表(list)哈希(hash)集合(set)有序集合(zset),而每种对象类型至少对应两种编码方式,不同的编码方式所对应的底层数据结构是不同的。

  每个对象会用到的编码以及对应的数据结构详见下表:

      学习之Redis(一)

   每种对象对应两至三种编码,除skiplist编码需要用到两种数据结构(字典+跳跃表)外,其余编码均用到一种底层的数据结构同一个对象类型在不同的场景下用到的编码(数据结构)不同,redis支持8种编码以及8种底层的数据结构。这种方式更加灵活,可以帮助redis获得更高的性能以及尽量占用更少的内存。比如如果字符串对象中要存储的字符串内容所占字节较小,会用embstr编码的格式,如果要存储的内容所占字节较大,会用raw编码的格式,具体细节后文会详细说明。

 

 

   上面说过,redis中的键和值都是由对象组成的,而对象是由对象结构和数据结构共同组成的。redis中的键,都是用字符串来存储的,即对于redis数据库中的键值对来说,键总是一个字符串对象,而值可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象中的其中一种
  键、值的整体大致结构可以如下图所示

学习之Redis(一)

【对象结构】

  对象结构(redisobject)共有5个属性,分别是type属性、encoding属性、ptr属性、refcount属性、lru属性。
    其中type属性、encoding属性、ptr属性和保存数据有关
      type属性:表示该对象的类型是什么
      encoding属性:表示这个对象使用的底层数据结构是什么
      ptr属性:是一个指向底层数据结构的指针
      refcount属性是一个引用计数属性,可以用于内存回收和对象共享
      lru属性,记录了对象最后一次被命令程序访问的时间,可以计算出某个键的空转时长


  对象结构的逻辑图如下所示:

  学习之Redis(一)

【内存回收--refcount属性】

  在对象结构中,有refcount这个属性,该属性用于记录对象的引用计数信息,redis利用引用计数(reference counting)技术实现内存回收机制,通过这一机制,程序可以通过跟踪对象的引用计数信息,在适当的时候自动释放对象并进行内存回收

  具体策略:
    在创建一个新对象时,引用计数的值会被初始化为1
    当对象被一个新程序使用时,它的引用计数值会被增一
    当对象不再被一个程序使用时,它的引用计数值会被减一
    当对象的引用计数值变为0时,对象所占用的内存会被释放

【对象共享--refcount属性】

  redis会在初始化服务器时,服务器会创建一万个字符串对象,这些对象包含了从0到9999的所有整数值,当服务器、新创建的键需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
  对象结构中,refcount是引用指针属性,如果有n个键共享一个值,refcount对应的值就为n。创建共享字符串对象的数量可以通过redis.h/redis_shared_intengers常量来修改。object refcount命令可以查看某个键对应的值被引用了多少次。

  让多个键共享一个值,需要执行以下两个步骤:

    将键的值指针,指向被共享的值对象

    被共享的值对象的引用计数器加一,即refcount属性的值加一,引用数为2的共享对象结构图如下图所示:

学习之Redis(一)

 【进一步说明】
  当服务器考虑将一个键的值引用共享对象时,键的值作为目标对象,程序需要先检查共享对象和目标对象的类型是否完全相同,只有在完全相同的情况下共享对象才会被引用。而一个共享对象保存的值越复杂,验证共享对象与目标对象所需的复杂度就会越高,消耗的cpu时间也会越多。
  所以共享对象的优点是被其它键引用时可以节省内存空间,缺点是被引用时需要进行判断,这个过程需要消耗cpu,如果共享对象简单,消耗很小的cpu并节省内存空间是值得的。但如果对象很复杂,进行判断就需要消耗大量cpu,消耗大量cpu去节省内存空间是不值得的,因为redis本身的内存空间还是很大的。

  redis支持5种对象,包括字符串对象、列表对象、哈希对象、集合对象以及有序集合对象。而字符串对象是redis中的一个基础对象其它对象均可以在底层的数据结构内部嵌套字符串对象

对象共享

  1、只有字符串对象才能被创建为共享对象,被其它字符串键使用

  2、用字符串对象创建的共享对象,不单单只有字符串键可以使用,那些在数据结构中嵌套了字符串对象的对象(linkedlist编码的列表对象、hashtable编码的哈希对象、hashtable编码的集合对象,以及skiplist编码的有序集合对象)都可以使用这些字符串共享对象

【对象的空转时长--lru属性】

  对象结构的lru属性,记录了对象最后一次被命令程序访问的时间
  空转时长:当前时间减去键的值对象的lru时间,就是该键的空转时长。object idletime命令可以打印出给定键的空转时长
  如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时空转时长较高的那部分键会优先被服务器释放,从而回收内存。

2.1、字符串对象 

2.11、字符串对象介绍

  字符串对象可以存储整数、浮点数、字符串,具体策略是:
    当存储整数时,用到的编码是int,底层的数据结构可以用来存储long类型的整数
    当存储字符串时,如果字符串的长度小于等于32字节,那么将用编码为embstr的格式来存储;如果字符串的长度大于32字节,将用编码为raw的sds格式来存储
    当存储浮点数时会先将浮点数转换为字符串,如果转换后的字符串长度小于32字节就用编码为embstr的格式来存储否则用编码为raw的sds格式来存储

  下图是以raw编码的字符串对象结构图,最左侧是对象结构,中间跟右侧合起来是raw编码的sds数据结构(sdshdr),示例图:

学习之Redis(一)

 

 2.12、raw编码,简单动态字符串(simple dynamic string-sds)

  虽然redis由c语言编写,但是redis用的并不是c语言传统的字符串,而是自己构建了简单动态字符串(simple dynamic string,sds)。当redis打印日志信息或输出报错信息,这些输出的字符串是不会被修改的字符串字面量(sting literal),此时用的是c语言传统的字符串来存储这些信息的。当redis需要存储的是可以被修改的字符串时就会使用sds结构。除了用来保存数据库中的字符串值之外,sds还被用作缓冲区(buffer):aof模块中的aof缓冲区,以及客户端状态中的输入缓冲区,都是由sds实现的。

  redis使用sdshdr结构来表示一个sds的值,sds结构示意图如下:

    学习之Redis(一)

 sdshdr是该数据结构的名称即sds,其中:
  buf属性,是一个字节数组,用来保存字符串,后面箭头对应的就是实际保存的字符串内容,最后以’\0’空字符串结尾
  len属性,记录的是buf数组中实际已使用的字节数量,等于sds所保存字符串的长度
  free属性,记录的是buf数组中未存储内容的空余大小,单位字节

2.1.3、使用sds的好处

一、可以用o(1)的复杂度获取到字符串长度
    sds的len属性记录了字符串的长度,而传统c字符串要想知道长度需要遍历整个字符串。相比于传统c字符串,redis获取字符串长度所需的复杂度从o(n)降低到了o(1)
  即使对非常长的字符串反复执行strlen命令(获取字符串长度),也不会造成过多的性能消耗。

二、杜绝缓冲区溢出
  在传统的c字符串中,如果要修改字符串的内容,但修改后字符串的长度超过原先的长度就会发生溢出现象。详见下图:

       学习之Redis(一)

  在sds中,当需要对buf字节数组中存储的内容进行修改(增添或删除)时,api会先通过free和len属性检查sds的空间是否足够,如果不够的话,sds会自动扩展空间再对内容进行修改。关于自动扩展空间的策略见下方“空间预分配”的内容。

三、减少修改字符串长度时所需的内存重分配次数
  对于传统c字符串:
    如果执行的是增长字符串的操作,如拼接操作(append),那么在执行命令之前,程序需要先通过内存重分配来扩展底层数据的空间大小——否则会产生缓冲区溢出。
    如果执行的是缩短字符串的操作,如截断操作(trim),那么在执行这个操作之后,程序需要通过内存重分配来释放字符串不再使用的空间——否则会产生内存泄漏。

  对于redis中的sds结构:
    内存重分配设计复杂的算法,是一个比较耗时的操作,redis作为速度要求严苛、数据会被频繁执行的数据库,如果每次修改字符串都需要进行一次内存重分配,会严重影响性能。
    使用sds,buf数组里可以包含未使用的字节,这些字节的数量由free属性记录,可以减少修改字符串长度时所需的内存重分配次数。

【空间预分配和惰性空间释放】
通过sds中free属性定义的未使用空间,sds可以实现空间预分配和惰性空间释放两种优化策略
1、空间预分配策略——可以降低字符串增长操作引起的内存重分配
当需要修改sds的内容,且需要进行空间扩展的时候,程序不仅会为sds分配修改所需的必须空间,还会为sds分配额外的未使用空间。
其中,额外分配的未使用空间数量由以下公式决定:
  如果对sds进行修改之后,sds的长度(即len属性的值)将小于1mb,那么程序将分配和len属性同样大小的未使用空间,这时sds len属性的值将和free属性的值相同。
  如果对sds进行修改后,sds的长度将大于等于1mb,那么程序会分配1mb的未使用空间。

【进一步说明】
  如果对一个字符串的末尾持续追加内容,当字符串整体大小大于1mb时,即使只追加一字节的字符,程序也会额外分配1mb的空间,当再次追加一字节的字符时,程序不会再额外分配1mb的空间,而是使用已有的空闲空间。
  即在扩展空间之前,会先检查未使用的空间是否足够,如果足够,是不会额外再扩展的
  通过空间预分配策略,sds将连续增长n次字符串所需的内存重分配次数从必定n次降低为最多n次。

2、惰性空间释放策略——可以降低字符串缩短操作引起的内存重分配
  当sds中的字符串长度被缩短时,程序并不会立即使用内存重分配来回收缩短后多出来的字节空间,而是使用free属性将这些字节的数量记录起来,以备将来使用。
  当然,redis提供了相应的命令来真正释放这些未使用空间,避免不必要的内存浪费。

四、二进制安全
  c字符串中的字符必须符合某种编码(比如ascii),并且除了字符串的末尾之外字符串里面不能包含空字符,如果字符串除末尾外还有其它空字符,那么最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得c字符串只能保存文本数据,而不能保存图片、音频、视频、压缩文件这样的二进制数据
  为了确保redis可以适用于各种不同的使用场景,sds的api都是二进制安全的(binary-safe),所有sds api都会以处理二进制的方式来处理sds存放buf数组里的数据,程序不会对其中的数据做任何限制、过滤或者假设,数据在写入时是什么样的,它被读取时就是什么样。
  这也是sds的buf属性被称为字节数组的原因——redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据

五、兼容部分c字符串函数
  sds遵循空字符串结尾这一惯例,好处是可以直接重用c字符串函数库里的函数,从而避免了不必要的代码重复

【embstr编码】

  如果字符串对象保存的是长度小于等于32字节的字符串,那么将会使用embstr编码,embstr编码是专门用来保存短字符串的一种优化编码方式。embstr编码与raw编码对应的字符串对象,都是由对象结构(redisobject)和数据结构(sdshdr)组成的
  区别在于用raw编码的字符串对象会调用两次内存分配函数来分别创建redisobject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中一次包含redisobject和sdshr两个结构,embstr编码的字符串对象结构图如下所示:

                     学习之Redis(一)

两者的区别
embstr编码的字符串对象在执行命令时,产生的效果和raw编码的字符串对象执行命令时产生的效果是相同的,但使用embstr编码的字符串对象来保存短字符串值有以下好处:
  1、embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次
  2、释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数
  3、embstr编码的字符串对象的所有数据都保存在一块连续的内存里,结构更加紧凑,而raw编码是分散开的,redisobject对象结构和sdshdr数据结构彼此间是用指针相关联的,embstr编码的对象比raw编码的对象能够更好的利用缓存带来的优势。

【编码的转换】

  int编码的字符串对象和embstr编码的字符串对象在条件满足的情况下,会被转换成raw编码的字符串对象。encoding命令可以查看键对应的值,底层用的是什么编码。
int转换为raw:
  对于int编码的字符串对象来说,如果我们向对象执行了一些命令,使得这个对象保存的不再是整数值,而是一个字符串值,那么字符串对象的编码将从int变为raw

127.0.0.1:6379> set a 100
ok
127.0.0.1:6379> object encoding a
"int"
127.0.0.1:6379> append a 'a'
(integer) 4
127.0.0.1:6379> get a
"100a"
127.0.0.1:6379> object encoding a
"raw"

int编码的字符串,存储的是long类型的整数,范围是2^63-1(2的63次方减一) ~ -2^63(2的63次方),当存储的整数在该范围内时,编码为int,当值超过该范围,编码将转换为embstr

127.0.0.1:6379> set number1 9223372036854775807
ok
127.0.0.1:6379> object encoding number1
"int"
127.0.0.1:6379> set number1 9223372036854775808
ok
127.0.0.1:6379> object encoding number1
"embstr"
127.0.0.1:6379> set number -9223372036854775808
ok
127.0.0.1:6379> object encoding number
"int"
127.0.0.1:6379> set number -9223372036854775809
ok
127.0.0.1:6379> object encoding number
"embstr"

embstr转换为raw:
  embstr编码的字符串对象无法被修改(redis没有为embstr编码的字符串对象编写任何响应的修改程序),只有int、raw编码的字符串对象可以被修改,所以embstr编码的字符串实际上是只读的
  当对embstr编码的字符串对象执行任何修改命令时,程序都会先将对象的编码从embstr转换为raw然后再执行修改命令。所以一旦embstr编码的字符串被修改,它的数据结构就会变成raw编码的格式。

127.0.0.1:6379> set a 'ab'
ok
127.0.0.1:6379> object encoding a
"embstr"
127.0.0.1:6379> append a 'c'
(integer) 3
127.0.0.1:6379> get a
"abc"
127.0.0.1:6379> object encoding a
"raw"