当前位置:   article > 正文

【book】redis的底层实现和使用_redis中的hash值以秒为单位变化

redis中的hash值以秒为单位变化

目录

数据结构与对象

a.简单动态字符串(SDS)

b.链表

c.字典

d.跳跃表

e.整数集合

f.压缩列表

g.对象

字符串对象

列表对象

哈希对象

集合对象

有序集合对象

单机数据库的实现

redis过期时间 

1.redis过期时间介绍

2.redis过期时间相关命令

3.redis如何清理过期key 

redis的持久化

RDB持久化

AOF持久化

多机数据库的实现

复制

sentinel(哨兵)

集群

发布与订阅

redis的事务

应用场景:

场景实例:

一个简单的论坛系统分析

文章信息

点赞功能

对文章进行排序

实时排行榜

缓存雪崩和缓存穿透

疑问:


redis的底层实现总是看了又忘,过一段时间不用就啥也记不清了,还是把他写一写吧。文中内容取自《redis设计与实现》

目录 /usr/local/bin

sudo ./redis-server redis.conf

数据结构与对象

a.简单动态字符串(SDS)

    redis没有直接使用c语言传统的字符串表示,而是自己构建了一种名为简单动态字符串的抽象类型,并将其作为redis的默认字符串表示。

    SDS的定义:

        

保留空字符的1字节空间不计算在SDS的len属性里面,为空字符分配额外的1字节空间、添加空字符到字符串末尾等操作都由SDS函数自动完成,所以这个空字符对于使用者来说是完全透明的哦。这样做的好处是,SDS可以直接重用一部分c字符串的库函数。

使用SDS的好处:

  • 常数复杂度获取字符串长度

    只要访问SDS的len属性,就能在O(1)的时间复杂度内得到当前字符串的长度信息。len属性的设置和更新操作是SDS的API自动完成的。

  • 杜绝缓冲区溢出

    c字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出。例如,strcat函数可以将src字符串的内容拼接到dest的末尾:

    

而SDS 的API需要对其进行修改时,会先检查SDS的空间是否满足修改所需的要求,如果不满足就会自动将SDS空间扩展至执行修改所需的大小,然后才执行实际的修改操作。 

 

  • 减少修改字符串时带来的内存重分配次数

通过free属性,SDS实现了空间预分配和惰性空间释放两种优化策略,以应对速度要求严苛、数据被频繁修改的场合

1.空间预分配

        

通过空间预分配策略,redis可以减少连续执行字符串增长操作所需的内存重分配次数。其实跟stl的vector动态空间增长是类似的。

2.惰性空间释放

缩短SDS时,程序并不立即回收多出来的字节空间,而是使用free属性记录下来空闲的字节数,在真正有需要时,可以调用API来释放SDS的未使用空间,避免多次内存重分配。

 

  • 二进制安全

    

举个例子,如果有一种使用‘\n’来分隔单词的特殊数据格式,就不能用c语言字符串来存储:

                        

c字符串会忽略后面的“cluster”。

 

 

b.链表

redis的链表是用双端链表+管理struct实现的。

        

其特性如下:

         

 

c.字典

redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

哈希表的定义:

dictht  ==> dict hash table 

table属性是一个数组,数组中的每个元素都是一个指向dictEntry结构的指针,每个dictEntry结构保存着一个键值对。

size属性记录了table数组的大小,即哈希表的大小。

used属性记录了哈希表目前已有键值对的数量。

sizemark属性的值总等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面。

一个例子:大小为4的哈希表

其中,哈希表节点用dictEntry结构表示:

*next是用来解决键冲突的问题(拉链法)

好,介绍完哈希表,现在可以看看redis的字典是怎么实现的:

字典 dict

type属性和pricdata是针对不同类型的键值对,为创建多态字典而设置的:

下面这个例子展示了一个普通状态下(没有进行rehash)的字典:

            

 

rehash:

随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子维持在一个合理的范围内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。这个操作可以通过执行rehash来完成。步骤如下:

备用的ht[1]派上了用场

渐进式rehash:

当哈希表中存放了几千万个键值对时,一次性、集中性地rehash操作的计算量太大,可能会导致服务器在一段时间内停止服务。

因此,将ht[0]中的键值对rehash到 ht[1]的工作是分多次、渐进式进行的。步骤如下:

d.跳跃表

跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

zset常用。

跳跃表的实现:

redis跳跃表由zskiplistNode和zskiplist两个结构定义。如下:

      

其中,

        

        

具体看看这两个结构体:

level属性用于在O(1)复杂度内获取跳跃表中层高最大的那个节点的层数,注意表头节点的层高并不计算在内。

多放几个例子:

e.整数集合

当一个集合(set)只包含整数值元素,并且这个集合的元素数量不多时,redis就会使用整数集合作为集合键的底层实现。

intset是redis用于保存整数值的集合抽象数据结构,他可以保存类型为int16_t,int32_t,int64_t的整数值,并且保证集合中不会出现重复元素:

升级:当我们要将一个新元素添加到整数集合中,但此元素的类型比集合中所有元素的类型都长时,需要进行升级

升级的好处:

1.提升灵活性。整数集合通过自动升级底层数组来适应新元素,使得我们使用起来非常灵活。

2.节约内存。不用一开始就将所有元素的类型都设为最大的int64_t,只在需要的时候做就行。

 

没有降级操作。

 

f.压缩列表

压缩列表是列表键和哈希键的底层实现之一,当一个列表键只包含少量列表项(或哈希键只包含少量键值对),且每个列表项(键值对)都为小整数值或者比较短的字符串,那么redis就会使用压缩列表来作为其底层实现。

一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。

直接看定义:

       

看一个具体的例子:

        

压缩列表节点的构成

每个压缩列表节点可以保存一个字节数组或者一个整数值,其中字节数组和整数值种类如下:

previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。previous_entry_length属性可以为1字节或者5字节。

encoding记录了节点的content属性所保存的数据类型和长度:

content属性负责保存节点的值。值的类型和长度由encoding决定

 连锁更新

添加、删除一个节点可能会引起其附近多个节点属性的更新操作。

g.对象

    在上文中我们陆续介绍了redis用到的主要数据结构,有SDS、双端链表、字典、压缩链表、整数集合等。

    redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含了字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种我们前面所介绍的数据结构。

    Redis对象还实现了引用计数技术的内存回收技术,当不再使用某个对象时,可以及时释放其内存;通过了引用计数实现了对象共享机制,节约内存;Redis的对象带有访问时间记录信息,该信息可用于计算该对象空转时间,在启动了maxmemroy功能下,空转时间较长的键优先被删除。

    redis使用对象来表示数据库中的键和值,每当创建一个新的键值对时,至少会创建两个对象,分别用作键对象和值对象。

对象的结构如下:

                      

对象的类型:

注意:键总是一个字符串对象,而值可以是任意一种对象。

            

对象的编码:

每种类型的对象都至少用了两种不同的编码。

通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了redis的灵活性和效率。

  • 字符串对象

    字符串对象可以是int、raw或者embstr。如果一个字符串时整数,并且可用long型表示,那么该字符串对象编码就是int。如果字符串长度大于39字节,那么将使用一个简单动态字符串(sds)保存,并将对象编码设置为raw。如果字符串长度小于等于39字节,则字符串以编码方式embstr来保存该字符串值。

    embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,其中分别包含redisObject结构和sdshdr结构。并且在对embstr的字符串进行修改后,总会编程一个raw编码的字符串对象(因为是先做编码转换再做修改)。

三者的结构如下:

                                                           

字符串命令的实现:

                

  • 列表对象

    列表对象的编码可以是ziplist或者linkedlist。

   ziplist 编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点保存一个列表元素。

    linkedlist 编码的列表对象使用双端链表作为底层实现,每个双端链表节点保存一个字符串对象,而每个字符串对象都保存了一个列表元素。

    列表既然有ziplist和linkedlist两种底层实现,那么列表到底使用哪一种呢?

    列表对象保存的所有字符串长度都小于64字节并且列表保存的元素数量小于512个时使用ziplist编码实现,否则使用linkedlist编码实现。注意这个512的值是可以修改的,具体参见配置项list-max-ziplist-value和list-max-ziplist-entries选项。

完整表示如下:

列表命令的实现:

           

  • 哈希对象

    哈希对象的编码可以是ziplist和hashtable。ziplist编码的哈希对象使用压缩列表作为底层实现,当有新的键值对要加入哈希对象时,会先将保存了键的压缩列表节点推入到压缩列表表尾,再将保存了值的压缩列表节点推入到列表表尾。这样的话,一对键值对总是相邻的,并且键节点在前值节点在后。

如果hashtable编码的哈希对象使用字典作为底层实现,则哈希对象中的每个键值对都是字典键值对来保存。

    哈希对象既然有ziplist和hashtable两种底层实现,那么其到底使用哪一种呢?

    当哈希象保存的所有字符串长度都小于64字节并且列表保存的元素数量小于512个时使用ziplist编码实现,否则使用hashtable编码实现。注意这个512的值是可以修改的,具体参见配置项hash-max-ziplist-value和hash-max-ziplist-entries选项。

具体实现如下:

字典实现:

哈希命令的实现:

                

 

  • 集合对象

集合对象的编码可以是intset和hashtable。intset编码的集合对象使用整数集合作为底层实现,所有元素都保存在整数集合中。另一方面,使用hashtable的集合对象使用字典作为底层实现,字典中每个键都是一个字符串对象,即一个集合元素,而字典的值都是NULL的。

             

集合命令的实现:

  • 有序集合对象

    有序集合对象的编码可以是ziplist和skiplist。ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨着的压缩列表节点保存,第一个保存集合元素,第二个保存集合元素对应的分值。压缩列表内集合元素按照分值大小进行排序,分值较小的在前,分值大的在后。(元素数量小于128个且每个元素成员的长度都小于64字节)

 

skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。

zset中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点保存一个集合元素,跳跃表节点的object属性保存元素的成员,score属性保存元素的分值。通过该跳跃表,可以对有序集合进行范围型操作,比如zrank、zrange命令就是基于跳跃表实现的。

zset中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素,字典的键保存集合元素的成员,字典的值保存集合成员的分值。通过该字典,可以O(1)复杂度查找到特定成员的分值,zscore命令就是根据这一特性来实现的。通过字典+skiplist作为底层实现,各取所长为我所用。

  1. typedef struct zset {
  2. dict *dict;
  3. zskiplist *zsl;
  4. } zset;

                       

有序集合命令的实现方法

 

对象的其他特性:

对象空转时长

redisObject结构中有一项(unsigned lru;)是记录对象最后一次访问的时间,使用命令object idletime key可以显示对象空转时长。

当Redis打开maxmemory选项时,并且Redis用于回收内存的算法为volatile-lru或者allkey-lru时,那么当Redis占用内存超过了maxmemory选项设定的值时,空转时长较高的那部分键会优先被Redis释放,从而回收内存。

内存回收

C不具备内存回收功能,Redis在自己对象机制上实现了引用计数功能,达到内存回收目的,每个对象的引用计数值在redisObject中的(int refcount;)来记录。当创建一个对象或者该对象被重新使用时,它的引用计数++;当一个对象不再被使用时,它的引用计数--;当一个对象的引用计数为0时,释放该对象内存资源。

对象共享

对象的应用计数另外一个功能就是对象的共享,当一个对象被另外一个地方使用时,可以直接在该对象引用计数上++就行。注意:Redis只对包含整数值的字符串对象进行共享。

 

单机数据库的实现

使用: 我的机器上是到/usr/local/redis/bin目录下

启动服务器程序: ./redis-server redis.conf

启动客户端程序: ./redis-cli -h 127.0.0.1 -p 6379     

当然ip可以不使用本地回环的。因为我只在本机上面自己用的。

 

Redis支持多个数据库,并且每个数据库的数据是隔离的不能共享,并且基于单机才有,如果是集群就没有多个数据库的概念

    Redis是一个字典结构的存储服务器,而实际上一个Redis实例提供了多个用来存储数据的字典,客户端可以指定将数据存储在哪个字典中。这与我们熟知的在一个关系数据库实例中可以创建多个数据库类似,所以可以将其中的每个字典都理解成一个独立的数据库。

    每个数据库对外都是一个从0开始的递增数字命名,Redis默认支持16个数据库(可以通过配置文件支持更多,无上限),可以通过配置databases来修改这一数字。客户端与Redis建立连接后会自动选择0号数据库,不过可以随时使用SELECT命令更换数据库,如要选择1号数据库:

  1. redis> SELECT 1
  2. OK
  3. redis [1] > GET foo
  4. (nil)

    然而这些以数字命名的数据库又与我们理解的数据库有所区别。首先Redis不支持自定义数据库的名字,每个数据库都以编号命名,开发者必须自己记录哪些数据库存储了哪些数据。另外Redis也不支持为每个数据库设置不同的访问密码,所以一个客户端要么可以访问全部数据库,要么连一个数据库也没有权限访问。最重要的一点是多个数据库之间并不是完全隔离的,比如FLUSHALL命令可以清空一个Redis实例中所有数据库中的数据。综上所述,这些数据库更像是一种命名空间,而不适宜存储不同应用程序的数据。比如可以使用0号数据库存储某个应用生产环境中的数据,使用1号数据库存储测试环境中的数据,但不适宜使用0号数据库存储A应用的数据而使用1号数据库B应用的数据,不同的应用应该使用不同的Redis实例存储数据。由于Redis非常轻量级,一个空Redis实例占用的内在只有1M左右,所以不用担心多个Redis实例会额外占用很多内存。 

                                 

数据库键空间:

redis是一个 键值对数据库服务器,服务器中的每个数据库都由一个redisDb结构表示,其中redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典成为键空间。

那么这些命令执行之后,数据库的键空间将为:

      

 

redis过期时间 

1.redis过期时间介绍

  有时候我们并不希望redis的key一直存在。例如缓存,验证码等数据,我们希望它们能在一定时间内自动的被销毁。redis提供了一些命令,能够让我们对key设置过期时间,并且让key过期之后被自动删除。

2.redis过期时间相关命令

  1.EXPIRE PEXPIRE 

  EXPIRE 接口定义:EXPIRE key "seconds"
  接口描述:设置一个key在当前时间"seconds"(秒)之后过期。返回1代表设置成功,返回0代表key不存在或者无法设置过期时间。

 
  PEXPIRE 接口定义:PEXPIRE key "milliseconds"
  接口描述:设置一个key在当前时间"milliseconds"(毫秒)之后过期。返回1代表设置成功,返回0代表key不存在或者无法设置过期时间。

                                                             

2.EXPIREAT PEXPIREAT

  EXPIREAT 接口定义:EXPIREAT key "timestamp"
  接口描述:设置一个key在"timestamp"(时间戳(秒))之后过期。返回1代表设置成功,返回0代表key不存在或者无法设置过期时间。


  PEXPIREAT 接口定义:PEXPIREAT key "milliseconds-timestamp"
  接口描述:设置一个key在"milliseconds-timestamp"(时间戳(毫秒))之后过期。返回1代表设置成功,返回0代表key不存在或者无法设置过期时间 

                                                              

 3.TTL PTTL

  TTL 接口定义:TTL key
  接口描述:获取key的过期时间。如果key存在过期时间,返回剩余生存时间(秒);如果key是永久的,返回-1;如果key不存在或者已过期,返回-2。


  PTTL 接口定义:PTTL key
  接口描述:获取key的过期时间。如果key存在过期时间,返回剩余生存时间(毫秒);如果key是永久的,返回-1;如果key不存在或者已过期,返回-2。

                                                               

 4.PERSIST

  PERSIST 接口定义:PERSIST key
  接口描述:移除key的过期时间,将其转换为永久状态。如果返回1,代表转换成功。如果返回0,代表key不存在或者之前就已经是永久状态。  

 5.SETEX

  SETEX 接口定义:SETEX key "seconds" "value"
  接口描述:SETEX在逻辑上等价于SET和EXPIRE合并的操作,区别之处在于SETEX是一条命令,而命令的执行是原子性的,所以不会出现并发问题。

 

过期时间的保存是通过“过期字典”来实现的:

                   

3.redis如何清理过期key 

  redis出于性能上的考虑,无法做到对每一个过期的key进行即时的过期监听和删除。但是redis提供了其它的方法来清理过期的key。

  1.被动清理

  当用户主动访问一个过期的key时,redis会将其直接从内存中删除。

  2.主动清理

  在redis的持久化中,我们知道redis为了保持系统的稳定性,健壮性,会周期性的执行一个函数。在这个过程中,会进行之前已经提到过的自动的持久化操作,同时也会进行内存的主动清理。
  在内存主动清理的过程中,redis采用了一个随机算法来进行这个过程:简单来说,redis会随机的抽取N(默认100)个被设置了过期时间的key,检查这其中已经过期的key,将其清除。同时,如果这其中已经过期的key超过了一定的百分比M(默认是25),则将继续执行一次主动清理,直至过期key的百分比在概率上降低到M以下。

  3.内存不足时触发主动清理

  在redis的内存不足时,也会触发主动清理。

 

redis的持久化

Redis 中的数据类型都支持 push/pop、add/remove 及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis 支持各种不同方式的排序。与 Memcached 一样,为了保证效率,数据都是缓存在内存中。

对,数据都是缓存在内存中的,当你重启系统或者关闭系统后,缓存在内存中的数据都会消失殆尽,再也找不回来了。所以,为了让数据能够长期保存,就要将 Redis 放在缓存中的数据做持久化存储。

Redis 怎么实现持久化?

在设计之初,Redis 就已经考虑到了这个问题。官方提供了多种不同级别的数据持久化的方式:

1、RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储。

2、AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。

3、如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式。

4、你也可以同时开启两种持久化方式, 在这种情况下, 当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。

如果你不知道该选择哪一个级别的持久化方式,那我们就先来了解一下 AOF 方式和 RDB 方式有什么样的区别,并且它们各自有何优劣,学习完之后,再来考虑该选择哪一种级别。

 

RDB持久化

RDB方式,是将redis某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。

redis在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。

如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。

虽然RDB有不少优点,但它的缺点也是不容忽视的。如果你对数据的完整性非常敏感,那么RDB方式就不太适合你,因为即使你每5分钟都持久化一次,当redis故障时,仍然会有近5分钟的数据丢失。

 

Redis 提供了savebgsave这两种不同的保存方式,并且这两个方式在执行的时候都会调用rdbSave函数,但它们调用的方式各有不同:

  • save 直接调用 rdbSave方法 ,阻塞 Redis 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。

  • bgsave 则 fork 出一个子进程,子进程负责调用 rdbSave ,并在保存完成之后向主进程发送信号,通知保存已完成。因为 rdbSave 在子进程被调用,所以 Redis 服务器在 bgsave 执行期间仍然可以继续处理客户端的请求。子进程进行大量的IO操作。

save 是同步操作,bgsave 是异步操作。

RDB 的保存有方式分为主动保存与被动保存。主动保存可以在 redis-cli 中输入 save 即可;被动保存需要满足配置文件中设定的触发条件,目前官方默认的触发条件可以在 redis.conf 中看到:

  1. save 900 1
  2. save 300 10
  3. save 60 10000

其含义为:

  1. 服务器在900秒之内,对数据库进行了至少1次修改
  2. 服务器在300秒之内,对数据库进行了至少10次修改。
  3. 服务器在60秒之内,对数据库进行了至少10000次修改。

满足条件时,就会触发被动RDB快照保存。

触发保存条件后,会在指定的目录生成一个名为 dump.rdb 的文件,等到下一次启动 Redis 时,Redis 会去读取该目录下的 dump.rdb 文件,将里面的数据恢复到 Redis。

这个目录在哪里呢?

我们可以在客户端中输入命令config get dir查看:

  1. gannicus@$ src/redis-cli
  2. 127.0.0.1:6379> config get dir
  3. 1) "dir"
  4. 2) "/home/gannicus/Documents/redis-5.0.0"
  5. 127.0.0.1:6379>

返回结果中的"/home/gannicus/Documents/redis-5.0.0"就是存放 dump.rdb 的目录。

全量复制与部分复制:

来源:https://mp.weixin.qq.com/s/0VVYTyAI1egfs2Fxcrme3A

Redis 使用 psync 命令完成主从数据同步,同步过程分为:全量复制和部分复制。

全量复制:一般用于初次复制场景,它会把主节点全部数据一次性发送给从节点发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。

部分复制:用于处理在主从复制中因网络闪断等原因造成的网络丢失场景,当从节点再次连接上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。

psync 命令运行需要以下组件支持:

  • 主从节点各自复制偏移量

  • 主节点复制积压缓冲区

  • 主节点运行 id

    参与复制的从节点都会维护自身复制偏移量。主节点在处理完写命令后,会把命令的字节长度做累加记录,统计在 info replication 中的 masterreploffset 指标中。从节点在接收到主节点发送的命令后,也会累加记录自身的偏移量,并且会每秒钟上报自身的复制偏移量给主节点。通过对比主从节点的复制偏移量,可以判断主从节点数据是否一致

    复制积压缓冲区是保存在主节点的一个固定长度的队列,默认大小为 1MB,当主节点有连接的从节点时被创建。主节点响应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区中。

    复制积压缓冲区大小有限,只能保存最近的复制数据,用于部分复制和复制命令丢失时的数据补救。

    每个 Redis 节点启动后都会动态分配一个 40 位的十六进制字符串作为运行 ID。运行 ID 的主要作用是用来唯一标识 Redis 节点,比如说从节点保存主节点的运行 ID 来识别自己正在复制的是哪个主节点。

 

部分复制流程:

    部分复制主要是 Redis 针对全量复制的过高开销做出的一种优化措施,使用 psync {runId} {offset} 命令实现。当从节点正在复制主节点时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区存在这部分数据则直接发送给从节点,这样就保证了主从节点复制的一致性。补发的这部分数据一般远远小于全量数据,所以开销很小。

  • 1) 当主从节点之间网络出现中断时,如果超过了 repl-timeout 时间,主节点会认为从节点故障并中断复制连接。

  • 2) 主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在复制积压缓冲区( repl-backlog-buffer ),依然可以保存最近一段时间的写命令数据,默认最大缓存 1MB。

  • 3) 当主从节点网络恢复后,从节点会再次连上主节点。

  • 4) 当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们作为 psync 参数发送给主节点,要求进行补发复制操作。

  • 5) 主节点接到 psync 命令后首先核对参数 runId 是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数 offset 在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送 +CONTINUE 响应,表示可以进行部分复制。

  • 6) 主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。

 

    需要注意,对于数据量较大的主节点,比如生成的 RDB 文件超过 6GB 以上时要格外小心。如果传输 RDB 的时间超过 repl-timeout 所配置的值,从节点将发起接收 RDB 文件并清理已经下载的临时文件,导致全量复制失败。

  • 对于主节点开始保存 RDB 快照到从节点接收完成期间,主节点仍然响应读命令,因此主节点会把这期间写命令保存在复制客户端缓冲区内,当从节点加载完 RDB 文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性。

如果主节点创建和传输 RDB 的时间过长,可能会出现主节点复制客户端缓冲区溢出。默认配置为 client-output-buffer-limit slave 256MB 64MB 60,如果60s内缓冲区消耗持续大于64MB或者直接超过256MB时,主节点将直接关闭复制客户端连接,造成全量同步失败。

 

 

AOF持久化

AOF,英文是Append Only File,即只允许追加不允许改写的文件。

如前面介绍的,AOF方式是将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序再将指令都执行一遍,就这么简单。

我们通过配置redis.conf中的appendonly yes就可以打开AOF功能。如果有写操作(如SET等),redis就会被追加到AOF文件的末尾。

默认的AOF持久化策略是每秒钟fsync一次(fsync是指把缓存中的写指令记录到磁盘中),因为在这种情况下,redis仍然可以保持很好的处理性能,即使redis故障,也只会丢失最近1秒钟的数据。

如果在追加日志时,恰好遇到磁盘空间满、inode满或断电等情况导致日志写入不完整,也没有关系,redis提供了redis-check-aof工具,可以用来进行日志修复。

因为采用了追加方式,如果不做任何处理的话,AOF文件会变得越来越大,为此,redis提供了AOF文件重写(rewrite)机制,即当AOF文件的大小超过所设定的阈值时,redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。举个例子或许更形象,假如我们调用了100次INCR指令,在AOF文件中就要存储100条指令,但这明显是很低效的,完全可以把这100条指令合并成一条SET指令,这就是重写机制的原理。(如对一个key incrby  1 一百次,那么aof重写就会将这一百条命令合并成一条

AOF重写

AOF重写的内部运行原理,我们有必要了解一下。

在重写即将开始之际,redis会创建(fork)一个“重写子进程”,这个子进程会首先读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。

与此同时,主工作进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。

当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中。

当追加结束后,redis就会原子地用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中了。

在子进程中执行 AOF 重写操作有如下的好处:

  • 子进程进行 AOF 重写期间,Redis 进程可以继续处理客户端命令请求。

  • 子进程带有父进程的内存数据拷贝副本,在不适用锁的情况下,也可以保证数据的安全性。

但是,在子进程进行 AOF 重启期间,Redis接收客户端命令,会对现有数据库状态进行修改,从而导致数据当前状态和 重写后的 AOF 文件所保存的数据库状态不一致。

为此,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在服务器创建子进程之后开始使用,当 Redis 执行完一个写命令之后,它会同时将这个写命令发送给 AOF 缓冲区和 AOF 重写缓冲区。

aof持久化策略见下表:

    

 

值得一提的是,linux系统对write系统调用操作会触发延迟写( delayed write )机制。Linux 在内核提供页缓存区用来提供硬盘 IO 性能。write操作在写入系统缓冲区之后直接返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区页空间写满或者达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。

fsync 针对单个文件操作,对其进行强制硬盘同步, fsync 将阻塞直到写入磁盘完成后返回,保证了数据持久化。

 

 

多机数据库的实现

复制

持久化保证了即使 redis 服务重启也会丢失数据,因为 redis 服务重启后会将硬盘上持久化的数据恢复到内存中,但是当 redis 服务器的硬盘损坏了可能会导致数据丢失,如果通过 redis 的主从复制机制就可以避免这种单点故障,如下图:

                                       Redis主ä»å¤å¶çè¿ç¯å°±å¤äº

在redis中,用户可以通过执行SLAVEOF命令或者设置slaveof选项,让一个服务器去复制另一个服务器,我们称被复制的服务器为主服务器(master),而对主服务器进行复制的服务器成为从服务器(slave)。

主从复制原理:

2.8版本以前:

1.从服务器向主服务器发送SYNC命令。

2.主服务器执行BGSAVE,后台生成一个RDB文件,并使用一个缓冲区记录从现在时刻开始执行的所有写命令。

3.BGSAVE执行完毕时,将生成的EDB文件发给从服务器,从服务器接受并载入这个RDB文件。

4.主服务器将上面提到的缓冲区内所有写命令发给从服务器,从服务器也执行这些写命令,完成同步。

缺点:每次都要全量复制(包括初次复制和断线后重连复制),效率低下。

 

redis 2.8版本以上使用psync命令完成同步,过程分“全量”与“部分”复制
全量复制:一般用于初次复制场景(第一次建立SLAVE后全量)
部分复制:网络出现问题,从节点再次连接主节点时,主节点补发缺少的数据,每次数据增量同步(通过复制偏移量、复制积压缓冲区、服务器运行ID来实现)
心跳:主从有长连接心跳,主节点默认每1S向从节点发ping命令,repl-ping-slave-period控制发送频率。

 

sentinel(哨兵)

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。当主节点出现故障时,由Redis Sentinel自动完成故障发现和转移,并通知应用方,实现高可用性。

哨兵模式是一种特殊的模式,本质上是一个运行在特殊模式下的redis服务器。哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

哨兵进程使用的端口是26379

        

 

 

这里的哨兵有两个作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。

  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

用文字描述一下故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。

             

哨兵的作用:

1、监控

     sentinel会每秒一次的频率与之前创建了命令连接的实例发送PING,包括主服务器、从服务器和sentinel实例,以此来判断当前实例的状态。down-after-milliseconds时间内PING连接无效,则将该实例视为主观下线。之后该sentinel会向其他监控同一主服务器的sentinel实例询问是否也将该服务器视为主观下线状态,当超过某quorum后将其视为客观下线状态。

     当一个主服务器被某sentinel视为客观下线状态后,该sentinel会与其他sentinel协商选出领头sentinel进行故障转移工作。每个发现主服务器进入客观下线的sentinel都可以要求其他sentinel选自己为领头sentinel,选举是先到先得同时每个sentinel每次选举都会自增配置纪元,每个纪元中只会选择一个领头sentinel。如果所有超过一半的sentinel选举某sentinel领头sentinel。之后该sentinel进行故障转移操作。

     如果一个Sentinel为了指定的主服务器故障转移而投票给另一个Sentinel,将会等待一段时间后试图再次故障转移这台主服务器。如果该次失败另一个将尝试,Redis Sentinel保证第一个活性(liveness)属性,如果大多数Sentinel能够对话,如果主服务器下线,最后只会有一个被授权来故障转移。 同时Redis Sentinel也保证安全(safety)属性,每个Sentinel将会使用不同的配置纪元来故障转移同一台主服务器。

    2、故障转移

     首先是从主服务器的从服务器中选出一个从服务器作为新的主服务器。选点的依据依次是:网络连接正常->5秒内回复过INFO命令->10*down-after-milliseconds内与主连接过的->从服务器优先级->复制偏移量->运行id较小的。选出之后通过slaveif no ont将该从服务器升为新主服务器。

     通过slaveof ip port命令让其他从服务器复制该信主服务器。

     最后当旧主重新连接后将其变为新主的从服务器。注意如果客户端与就主服务器分隔在一起,写入的数据在恢复后由于旧主会复制新主的数据会造成数据丢失。

     故障转移成功后会通过发布订阅连接广播新的配置信息,其他sentinel收到后依据配置纪元更大来更新主服务器信息。Sentinel保证第二个活性属性:一个可以相互通信的Sentinel集合会统一到一个拥有更高版本号的相同配置上。    

 

集群

Redis在3.0版正式引入了集群这个特性。Redis集群是一个分布式(distributed)、容错(fault-tolerant)的 Redis内存K/V服务, 集群可以使用的功能是普通单机 Redis 所能使用的功能的一个子集(subset),比如Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误。

  Redis集群的几个重要特征:
  (1). Redis 集群的分片特征在于将键空间分拆了16384个槽位,每一个节点负责其中一些槽位。
  (2). Redis提供一定程度的可用性,可以在某个节点宕机或者不可达的情况下继续处理命令.
  (3). Redis 集群中不存在中心(central)节点或者代理(proxy)节点, 集群的其中一个主要设计目标是达到线性可扩展性(linear scalability)。

槽指派的信息分别存在clusterState.slots 和clusterNode.slots中。前者保存了每个槽是由哪个节点管理的,后者保存了每个节点管理了哪些槽。二者结合使用,才能达到O(1)复杂度内获得“此槽归谁管”,还有将“此节点管理哪些槽”的信息发给其他节点。

                          

集群中的每个节点都能带有若干个从节点(slave),当主节点下线时,几个从节点会投票选出一个新的主节点(由其他有投票权的主节点进行投票,即正在负责处理槽的主节点),然后此从节点执行slaveof no one 成为新的主节点。   新主节点撤销已下线主节点的槽指派,并将这些槽全部指派给自己。 然后向集群广播一条PONG消息,让集群中其他主节点知道本节点成为了新的主节点,并且这些槽现在由本节点负责,完成故障转移。

 

发布与订阅

有点类似简化版kafka的意思

这个博主摘录了这一章的内容,写的不错 redis发布与订阅

 

redis的事务

redis支持简单的事务操作,将redis事务与mysql事务对比:

在mutil后面的语句中, 语句出错可能有2种情况:

1: 语法就有问题,此时exec报错, 所有语句得不到执行:

2: 语法本身没错,但适用对象有问题,Exec之后会执行正确的语句,并跳过有问题的语句.

第一条命令执行成功,第二条命令被跳过,一条出现错误其余正确的命令仍然可以执行,即就算有命令失败,队列中的其他命令也会被执行。

discard:

 

 

 mysql的rollback与redis的discard的区别:

mysql回滚为sql全部成功才执行,一条sql失败则全部失败,执行rollback后所有语句造成的影响消失

redis的discard只是结束本次事务。就是multi后不跟exec而是跟discard

 

为什么Redis不支持回滚

Redis命令在事务中可能会执行失败,但是Redis事务不会回滚,而是继续会执行余下的命令。如果您有一个关系型数据库的知识,这对您来说可能会感到奇怪,因为关系型数据在这种情况下都是会回滚的。

Redis这样做,主要是因为:

只有当发生语法错误(这个问题在命令队列时无法检测到)了,Redis命令才会执行失败, 或对keys赋予了一个类型错误的数据:这意味着这些都是程序性错误,这类错误在开发的过程中就能够发现并解决掉,几乎不会出现在生产环境。由于不需要回滚,这使得Redis内部更加简单,而且运行速度更快。

 

在事务中还有事务监听命令:

虽然事务能保证事务内的操作是原子性的,但是无法保证在事务开启到事务提交之间事务中的key没有被其他客户端修改。

有点类似关系型数据库中的不可重复读的概念,在Redis的一个事务中是可以读取到其他事务提交的内容的。

  1. watch key1 key2 ...  #监听一个或多个key
  2. unwatch   #取消所有的监听

 Redis的事务中启用的是乐观锁,只负责监测key没有被改动.如果没变正常执行,如果有变事务取消

WATCH 说明

WATCH 到底是什么意思呢? 这个命令使得 EXEC 命令的执行必须满足一个条件:如果被WATCH的 keys 没有一个被更改(但它们可以在事务中被修改),则执行事务;不然,就不会执行这个事务。(注意,如果你 WATCH了一个有生命周期的key,并且这个key过期了, EXEC 依然会执行)

WATCH 可以被多次调用。所有的WATCH 调用都会在 EXEC 调用之前起作用。WATCH可以接收任意多的key 。

当 EXEC 被调用后, 所有的keys都将UNWATCH,不管这个事务会不会终止。同样,当一个客户端链接关闭后, 一切都将UNWATCH。

可以使用UNWATCH (没有参数)命令来刷新所有被WATCH的keys。有时会这样操作,我们乐观地锁定了几个keys,因为可能我们需要执行一个事务来修改这些keys,但是在读取了keys的当前内容之后,我们不想继续处理了。那么这个时候,我们就可以调用UNWATCH。

 

 

  1. ubuntu@VM-0-12-ubuntu:/usr/local/bin$ ps -elf | grep redis
  2. 5 S root 26005 1 0 80 0 - 11231 - 09:22 ? 00:00:00 redis-server 127.0.0.1:6379
  3. 0 S ubuntu 26049 17597 0 80 0 - 3443 pipe_w 09:22 pts/0 00:00:00 grep --color=auto redis
  4. ubuntu@VM-0-12-ubuntu:/usr/local/bin$ ps -aux | grep redis
  5. root 26005 0.0 0.2 44924 3872 ? Ssl 09:22 0:00 redis-server 127.0.0.1:6379
  6. ubuntu 26070 0.0 0.0 13772 1076 pts/0 S+ 09:23 0:00 grep --color=auto redis
  7. ubuntu@VM-0-12-ubuntu:/usr/local/bin$ sudo netstat -pltn | grep 26005
  8. tcp 0 0 127.0.0.1:6379 0.0.0.0:* LISTEN 26005/redis-server

 

应用场景:

计数器

    可以对 String 进行自增自减运算,从而实现计数器功能。

    Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。

缓存

    将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。

查找表

    例如 DNS 记录就很适合使用 Redis 进行存储。

    查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。

消息队列

    List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息

    不过最好使用 Kafka、RabbitMQ 等消息中间件。

会话缓存

    可以使用 Redis 来统一存储多台应用服务器的会话信息。

    当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。

分布式锁实现

    在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。

    可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。

分布式锁,是一种思想,它的实现方式有很多。比如,我们将沙滩当做分布式锁的组件,那么它看起来应该是这样的:

加锁

在沙滩上踩一脚,留下自己的脚印,就对应了加锁操作。其他进程或者线程,看到沙滩上已经有脚印,证明锁已被别人持有,则等待。

解锁

把脚印从沙滩上抹去,就是解锁的过程。

锁超时

为了避免死锁,我们可以设置一阵风,在单位时间后刮起,将脚印自动抹去。

 

我们来看如何通过单节点Redis实现一个简单的分布式锁。

1、加锁

加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。

SET lock_key random_value NX PX 5000

值得注意的是:
random_value 是客户端生成的唯一的字符串。
NX 代表只在键不存在时,才对键进行设置操作。
PX 5000 设置键的过期时间为5000毫秒。

这样,如果上面的命令执行成功,则证明客户端获取到了锁。

2、解锁

解锁的过程就是将Key键删除。但也不能乱删,不能说客户端1的请求将客户端2的锁给删除掉。这时候random_value的作用就体现出来。

为了保证解锁操作的原子性,我们用LUA脚本完成这一操作。先判断当前锁的字符串是否与传入的值相等,是的话就删除Key,解锁成功。(每个key实际上只存在集群中的某一台机器上,由槽分配决定,因此redis服务器对此key的操作实际上是线程安全的,因为单点机器是单线程的,不存在竞态条件)

 

 

其它

    Set 可以实现交集、并集等操作,从而实现共同好友等功能。

    ZSet 可以实现有序性操作,从而实现排行榜等功能。

 

场景实例:

一个简单的论坛系统分析

该论坛系统功能如下:

  • 可以发布文章;
  • 可以对文章进行点赞;
  • 在首页可以按文章的发布时间或者文章的点赞数进行排序显示。

文章信息

文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。

Redis 没有关系型数据库中的表这一概念来将同种类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID,通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617,其中 article 为命名空间,ID 为 92617。

       

点赞功能

当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。

为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。

对文章进行排序

为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的)

 

实时排行榜

redis的有序集合(sorted set)就非常适合做这件事情。有序集合和集合一样可以存储字符串,另外有序集合的成员可以关联一个分数(score),这个分数用于集合排序。(跳表+hash)

  • 基本实现原理:

1、排行榜用的数据结构是跳表 SkipList (跳表是一种有序的链表,随机检索、插入和删除的性能非常高,Redis和LevelDB都有采用跳表这种数据结构,是一种空间换时间的算法)

2、通过玩家ID快速检索用一个Map<ID,SkipListNode>

  • 过程描述:

1、服务器启动从DB中加载N个上榜的玩家

2、用跳表对其进行插入。插入完跳表是个有序的自然形成排行

3、当有玩家数据变动

  1)如果排行榜已满,先判断Score是否比最后一名低,如果是直接抛弃

  2)如果自己在排行榜时,就把自己的SkipListNode删除,然后插入

  3)如果自己不在排行榜,则直接插入自己,删除最后一名,并向数据库发出存储指令(新增自己,删除最后一名,【如果自己和最后一名是一个人则什么也不做】)

 

如果玩家数据量非常大,其实不应该把所有玩家都存进跳表里,可以多设置几个跳表,分别存放1~1000名,1001~10000名,10001~50000名这样,因为后面几万名的几分变动很难影响到前几千名的玩家,没必要放在一起维护。

 

通过zrevrank可以快速得到用户排名,通过zrevrange可以快速得到top n的用户列表,它们的时间复杂度都是O(log(N))。

 

 

 

缓存雪崩和缓存穿透

缓存雪崩

对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。

这就是缓存雪崩。

                                             

缓存雪崩的事前事中事后的解决方案如下。 - 事前:redis 高可用,主从+哨兵,redis cluster(分配槽),避免全盘崩溃。 - 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。 - 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果 ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 redis 中。

限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级可以返回一些默认的值,或者友情提示,或者空白的值。

好处: - 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。 - 只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。 - 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。

 

缓存穿透

对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。

黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。

举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。

                                                

解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。

 

缓存击穿

缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

解决方式也很简单,可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。

 

 

疑问:

  • 为什么redis是单线程的?

因为Redis是基于内存的操作,CPU不是Redis的瓶颈(并不会等磁盘IO导致cpu空闲,所以无需启动另一个线程来跑cpu),Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,且不过有多线程竞态和切换开销的问题

(1) 绝大部分请求是纯粹的内存操作(非常快速)

(2) 采用单线程,避免了不必要的上下文切换和竞争条件

(3) 非阻塞IO - IO多路复用

 

  • sds中结构体为何采用char[]来存放字符串,而不是char*?这二者有什么区别?

     看这部分的源码时,产生了这个疑问,发现自己也一直没搞清楚这二者在结构体或类中出现时有什么使用上的区别。

有一篇博文写得不错: https://www.cnblogs.com/focus-z/p/11397340.html

1.char[]型:

  1. struct mystr1 {
  2. int len;
  3. char s[0];
  4. };
  5. int main() {
  6. int length = 10;
  7. struct mystr1 *s1 = (mystr1*)malloc(sizeof(mystr1) + length + 1);
  8. s1->len = length;
  9. memset(s1->s, 'a', length);
  10. s1->s[length] = '\0';
  11. cout << s1->s << endl;
  12. system("pause");
  13. return 0;
  14. }

上面这段代码的意思是:我想分配一个不定长的数组,于是我有一个结构体,其中有两个成员,一个是length,代表数组的长度,一个是s,代表数组的内容。后面代码里的 length(长度是10)代表是我想分配的数据的长度。(这看上去是不是像一个C++的类?)这种玩法英文叫:Flexible Array,中文翻译叫:柔性数组。

2.char*型:

  1. struct mystr2 {
  2. int len;
  3. char* s;
  4. };
  5. int main() {
  6. struct mystr2 *s2 = (mystr2*)malloc(sizeof(mystr2));
  7. s2->s = (char*)malloc(length + 1);
  8. memset(s2->s, 'a', length);
  9. s2->s[length] = '\0';
  10. cout << s2->s << endl;
  11. system("pause");
  12. return 0;
  13. }

这样比较清晰易懂。

这个事情出来的原因是——我们想给一个结构体内的数据分配一个连续的内存!这样做的意义有两个好处:

第一个意义是,方便内存释放。如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配(char*的malloc),并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。(读到这里,你一定会觉得C++的封闭中的析构函数会让这事容易和干净很多,但是redis是纯c实现的)

第二个原因是,这样有利于访问速度。连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/157390
推荐阅读
相关标签
  

闽ICP备14008679号