Redis知识汇总


目录

  1. Redis简介

  2. 数据结构和对象

  3. 数据库

  4. RDB

  5. AOF

  6. 事件

  7. 客户端

  8. 复制

  9. 哨兵机制

  10. 集群

  11. 发布与订阅

  12. 事务

  13. 缓存问题

  14. 内存淘汰机制

  15. Redis并发竞争key

  16. 缓存与数据库一致性问题

  17. Redis实现消息队列

参考资料

  • 《Redis设计与实现》
  • JavaG

Redis 简介

简单来说redis就是一个数据库,不过与传统数据库不同的是redis的数据是存在内存中的,所以读写速度非常快,因此redis被广泛应用于缓存方向。另外,redis也经常用来做分布式锁。redis提供了多种数据类型来支持不同的业务场景。除此之外,redis支持事务、持久化、LUA脚本、LRU驱动事件、多种集群方案。

数据结构和对象

redis数据库里面的每个键值对都是由对象组成的,其中,

​ · 数据库键总是一个字符串对象

​ · 而数据库键的值则可以是字符串对象列表对象哈希对象集合对象有序集合对象

SDS简单动态字符串

SDS定义

img img

保留空字符“\0”作为字符串的结尾,兼容C语言,但是空字符的1个字节不计入SDS的len属性中。

与C串的区别

(1)通过常数复杂度回去字符串长度,len属性

(2)字符串拼接、修改等操作时,杜绝缓存区溢出,自动修改大小。

(3)减少修改时内存重新分配次数

​ SDS通过未使用空间(free属性记录)解除了字符串长度和底层数组长度之间的关联。SDS实现了空间预分配和惰性空间释放两种优化策略。

· 空间预分配:优化SDS字符串增长操作。对象与修改len后,若len小于1MB,则free = len;若len大于等于1MB,则free = 1MB。故将连续增长N次字符串所需的内存重分配次数从必定N次降低为最多N次。

· 惰性空间释放:优化SDS字符串缩短操作。利用free属性将不适用的数据大小记录下来,等将来使用。

(4)二进制安全

​ 所有的SDS API都会以处理二进制的方式来处理SDS存放的buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设,数据在写入时是什么样的,它被读取时就是什么样。

链表

定义

img img img

特性

(1)双端

(2)无环

(3)带有表头和表尾指针

(4)带链表长度计数器

(5)多态,通过dup、free、match设置类型特定的函数

字典

定义

Redis的字典使用哈希表作为底层实现。

(1)哈希表节点

img

next指针将多个哈希值相同的键值对连接在一起,以此来解决键冲突问题(collision)。

(2)哈希表

img

(3)字典

img

ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。

img img

哈希算法

(1)计算hash值,采用MurmurHash2算法计算。

(2)计算在哈希表中的位置index = hash & sizemask

解决键冲突

(1)使用链地址法解决冲突

(2)因为dictEntry节点组成的链表没有表尾指针,故将新加节点加到链表的表头位置。

rehash重新散列

哈希表保存的键值对随着操作会逐渐增多或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,需要重新散列。

步骤

img

负载因子

(1)load_factor = ht[0].used / ht[0].size

(2)还要结合服务器是否在执行BGSAVE或者BGREWRITEAOF指令,在load_factor >= 1(否)或者load_factor >= 5(是)时进行rehash。

渐进式rehash

(1)rehash动作分多次、渐进式地完成.

(2)渐进式rehash期间,字典的增删查改在两个哈希表上进行。

skiplist跳跃表

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

定义

(1)zskiplistNode

img

· 层用来加速访问其他节点,一般层的数量越多,访问其他节点的速度越快

· 层的前进指针:用于从表头向表尾访问节点

· 跨度:用来计算排位的,查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在条鱼表中的排位。

· 后退指针:用于从表尾向表头方向访问节点,每次只能后退至前一个节点。

· 分值:跳跃表中的所以节点按照分值从小到大排序

· 成员对象:指向一个字符串对象SDS,且对象必须唯一

(2)zskiplist

typedef struct zskiplist {
  // 表头节点和表尾节点
  structz skiplistNode *header, *tail;
  // 表中节点的数量
  unsigned long length;
  // 表中层数最大的节点的层数
  int level;
} zskiplist;
img

应用

(1)有序集合键

(2)集群节点中用作内部数据结构

intset整数集合

定义

img

· encoding属性决定contents数组真正的类型可以为INTSET_ENC_INT16(int16_t),INTSET_ENC_INT32(int32_t),INTSET_ENC_INT64(int64_t)

· length属性是contents数组的长度

· contents数组是整数集合的底层实现,各个按值大小从小到大有序地排列,不包含重复项

升级

当新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级。

(1)步骤

​ · 根据新元素的类型,扩展整数集合底层数组的空间大小

​ · 底层数组现有的所有元素都转换成与新元素相同的类型,并保持数组的有序性。

​ · 将新元素添加到底层数组里面

(2)新元素插入的位置

​ 要么是最大值要么是最小值,故只会在表头和表尾插入。

(3)优点

​ · 提升整数集合的灵活性

​ · 尽可能地节约内存

ziplist压缩列表

定义

(1)ziplist

img img

(2)节点entry

img

· previous_entry_length:记录了压缩列表中前一个节点的长度。压缩列表从表尾向表头遍历操作就是依赖这个属性。

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

· content 保存节点的值,可以是一个字节数组或者一个整数。

连锁更新

当添加或者删除节点的时候,导致previous_entry_length所占字节空间发生变化(1字节或者5字节),新节点的后续节点都要重新分配空间。

对象

包含5中类型的对象。基于引用计数计数进行内存回收和对象共享机制。

redisObject

(1)结构定义

img

(2)type

img

(3)encoding

img img

字符串对象

(1)可以使用的encoding类型

​ REDIS_ENCODING_INT、REDIS_ENCODING_EMBSTR、REDIS_ENCODING_RAW

(2)编码转换

​ int编码和embstr编码的字符串对象在条件满足的情况下,被转换为raw对象。

列表对象

(1)可以使用的encoding类型

​ REDIS_ENCODING_ZIPLIST、REDIS_ENCODING_LINKEDLIST

(2)编码转换

img

哈希对象

(1)可以使用的encoding类型

​ REDIS_ENCODING_ZIPLIST、REDIS_ENCODING_HT

(2)编码转换

img

集合对象

(1)可以使用的encoding类型

​ REDIS_ENCODING_INTSET、REDIS_ENCODING_HT

(2)编码转换

img

有序集合对象

(1)可以使用的encoding类型

​ REDIS_ENCODING_ZIPLIST、REDIS_ENCODING_SKIPLIST

(2)底层结构zset

typedef struct zset{
  zskiplist *zsl;
  dict *dict;
}

· zsl按分值从小到大保存所有集合元素

· dict保存成员到分值的映射,键为成员,值为分值。

这两种数据结构通过指针来共享相同元素的成员和分值,不会浪费额外的内存。

(3)编码转换

img

多态命令

redis除了会根据值对象的类型来判断键是否能够执行指定命令外,还会根据值对象的编码方式,选择正确的命令实现代码来执行命令。

内存回收

引用计数法:利用redisObject中的refcount属性,当计数值为0时,回收内存。

对象共享

· 将数据库键的值指针指向一个向右的值对象

· 将被共享的值对象的引用计数增1

img

· 注意:redis只对包含整数值的字符串对象进行共享,其他字符串可能比较值相同较耗CPU。

空转时长

· redis利用redisObject中的lru属性,记录对象最后一次被命令程序访问的时间。

· 空转时长,就是通过将当前时间减去键的值对象的lru时间计算得出

· 如服务器打开maxmemory选项,且回收内存算法为volatile-lru或者allkeys-lru,则当内存超过maxmemory时,空转时长较高的部分将优先被服务器释放,回收内存。

数据库

定义

(1)redisServer

struct redisServer{
  // 保存服务器中所有数据库的数组
  redisDb *db;
  // 服务器数据库数量
  int dbnum;
  // ....
}
img

dbnum的数量默认为16.

(2)redisClient

typedef struct redisClient{
  // 记录客户端当前正在使用的数据库
  redisDb *db;
  // ....
}

默认使用0号数据库。可以通过SELECT命令切换数据库。

(3)redisDb

typedef struct redisDb{
  // 数据库键空间
  dict *dict
  // 过期时间
  dict *expires;
  // ...
}

· dict字典中保存一个数据库中的所有键值对。

· 脏键,客户端使用WATCH命令监视的键,服务器每次修改一个键之后,都会对脏键计数器的值加1,计数器会触发服务器的持久化和复制操作。

过期时间

(1)EXPIRE/PEXPIRE命令

​ 设置TTL生存时间,EXPIRE单位为s,PEXPIRE单位为ms。

(2)EXPIREAT/PEXPIREAT命令

​ 设置过期时间,后面跟UNIX时间戳。

(3)使用 redisDb的expires字典保存过期时间,键为对象,值为过期时间。

(4)redis的过期删除策略

  • 惰性删除:在查询key时,若过期才删除

  • 定期删除:当服务器周期操作serverCron函数执行时删除。每次删除随机抽取部分,并维护一个进度记录,知道过期键全部清除。

(5)RDB功能和过期键

​ · 生成RDB文件时,检测键,过期的键不会被保存到文件中。

​ · 载入RDB文件时,主服务器忽略过期键,从服务器直接载入(主从同步时会删除)。

(6)AOF功能和过期键

​ 过期键对AOF无影响,当过期键被删除式,AOF文件追加DEL语句。

RDB

概念

(1)RDB持久化是将某个时间点上的数据库状态保存到一个RDB文件中。

(2)RDB文件是一个经过压缩的二进制文件,通过该文件可以还原生成RDB文件时的数据库状态。

创建和载入

创建

(1)SAVE命令

阻塞Redis服务器,直到RDB文件创建完毕为止。

(2)BGSAVE命令

派生出一个子进程,子进程负责创建RDB,服务器进程继续处理命令。

· 注意:如果服务器开启了AOF持久化功能,那么服务器会优先使用AOF文件来还原数据。当AOF关闭时,才会使用RDB方式。

载入

在服务器启动时自动执行。

自动间隔性保存

(1)配置

在redis.windows.conf中,通过save选项设定服务器自动执行BGSAVE命令的间隔时间。

img

img

· 900秒内,对数据库修改1次,创建RDB

· 300秒内,对数据库修改10次,创建RDB

· 60秒内,对数据库修改10000次,创建RDB

(2)原理

​ · save选项会设置到redisServer的saveparam属性中。

​ · redisServer的dirty计数器记录一次SAVE/BGSAVE后,数据库修改次数

​ · redisServer的lastsave属性记录上一次SAVE/BGSAVE执行时间

· 服务器周期性操作函数serverCron默认每个100ms执行一次,其中一项工作为检测save选项条件是否满足

RDB文件结构

img

(1)REDIS用来快速检测所载入的文件是否是RDB文件

(2)db_version长度为4字节,记录RDB文件版本号

(3)databases包含0个或多个数据库,以及各数据库中的键值对数据

(4)EOF长度为1字节,标志RDB文件正文内容的结束

(5)check_sum长度为8字节无符号整数,保存一个校验和,用来检测RDB文件是否有出错或者损坏的情况

AOF

概念

AOF持久化是通过保存Redis服务器锁执行的写命令来记录数据库状态的

创建步骤

命令追加(append)

服务器在执行完一个写命令后,会以协议格式将被执行的写命令追加到redisServer的aof_buf缓冲区(SDS类型)的末尾。

文件写入与同步

服务器在每个事件处理结束时,调用flushAppendOnlyFile函数将aof_buf缓冲区中的内容写入和保存到AOF文件里面。flushAppendOnlyFile函数的appendfsync参数决定同步(何时将操作系统内存缓存区的内容写入磁盘)方式:

img

everysec为默认设置。

载入

步骤:

(1)创建一个不带网络连接的伪客户端

(2)从AOF文件中分析并读取一条写命令(包括新增,修改,删除)

(3)使用伪客户端执行写命令

(4)一直执行(2)和(3)直到所有命令被处理完。

AOF重写

(1)原因:为了解决长时间后AOF文件体积膨胀的问题(主要由于Redis的内存淘汰机制,一定时间后,大量数据被淘汰,使得原本的AOF存在大量之前的写记录,变得冗长)

(2)实现:创建一个新的AOF文件代替现有AOF文件。新旧两个AOF文件保存的数据库状态相同,但新AOF文件不包含任何浪费空间的冗命令

(3)原理:从数据库读取键现在的值,用一条命令去记录键值对,代替之前记录这个键值对的多条命令。

(4)后台重写:创建子进程执行AOF重写程序(因为Reids采用单线程模式工作)。

事件

Redis服务器是一个事件驱动程序

文件事件

构成

img

(1)套接字 socket

​ Redis服务器通过套接字与客户端连接。

(2)I/O多路复用

img

​ 将所有产生的套接字都放在一个队列里面,然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字。

​ Redis以单线程模式运行。

​ 多路复用的具体实现,详见:深度解析单线程的 Redis 如何做到每秒数万 QPS 的超高处理能力,主要是通过epoll模型进行多路复用。

图片

(3)文件事件分派器

​ 根据IO复用器传过来的套接字产生的事件类型,调用相应的事件处理器

(4)事件处理器

​ · 命令请求处理器

​ · 命令回复处理器

​ · 连接应答处理器

​ · 。。。

事件类型

(1)AE_READABLE

​ 套接字可读。即客户端对套接字执行write操作,或者close操作,或者有新的可应答套接字出现。

(2)AE_WRITABLE

​ 套接字可写,即客户端对套接字执行read操作。

当两种事件同时发生时,文件事件分派器优先处理AR_READABLE事件。

时间事件

分为定时事件和周期性事件两类。

分类

(1)定时事件:让一段程序在指定的时间后执行一次。事件处理器返回AE_NOMORE

(2)周期性事件:让程序每隔指定时间就执行一次。事件处理器返回非AE_NOMORE整数值。

目前版本Redis只使用周期性事件。

时间事件的属性

· id 服务器为时间事件创建的全局唯一ID

· when 毫秒精度的UNIX时间戳,记录时间事件的到达时间

· timeProc 时间时间处理器,当时间事件到达时,服务器会调用相应的处理器来处理事件

实现

服务器将所有时间事件放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

img

· 新的时间事件总是插入到链表的表头。

· 正常模式下的Redis服务器只使用serverCron一个时间事件,故无序链表并不影响事件执行性能。

serverCron函数

(1)Redis需要定期对自身的资源和状态进行检查和调整

(2)主要工作:

​ · 更新服务器的各类统计信息,如时间、内存占用等

​ · 清理数据库中的过期键值对

​ · 关闭和清理连接失效的客户端

​ · 尝试进行AOF或RDB持久化操作

​ · 如果服务器是主服务器,对从服务器进行定期同步

​ · 如果处于集群模式,对集群进行定期同步和连接测试

(3)默认serverCron平均每间隔100ms运行一次(Redis2.6),Redis2.8开始,用户通过修改redis.windos.conf的hz选项来调整每秒执行次数。

事件的执行原则

(1)一次文件事件之后,仍然没有时间事件到达,那么服务器将再次等待并处理文件事件。

(2)事件的处理都是同步、有序、原子地执行的。

(3)时间事件在文件事件之后执行,通常执行时间会比时间事件设定的到达时间稍晚一些。

客户端

redisClient

struct redisServer {
  // ...
  // 一个链表,保存了所有客户端状态
  list *client
  // 套接字描述符
  int fd;
  //...
}

客户端分类

(1)普通客户端

​ · fd > -1的整数,来源于网络

​ · 创建时,添加到链表表尾

(2)伪客户端

​ · fd = -1,不是来源于网络

​ · AOF在载入AOF文件时创建。在载入完成后,伪客户端关闭。

​ · Lua脚本执行时创建。在服务器关闭时,伪客户端关闭。

复制

概念

(1)”SLAVEOF ip port”命令或者配置文件中的slaveof选项

(2)主从服务器的数据库将保存相同的数据

旧版(Redis2.8之前)

分为两个步骤进行,同步和命令传播。

同步

img

(1)从服务器向主服务器发送SYNC命令

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

(3)主服务器将RDB文件发送给从服务器,从服务器收入并载入RDB文件。

(4)主服务器将记录在缓冲区里面的所有写命令发送给从服务器。

命令传播

同步之后,每次将主服务器的写命令发送给从服务器。

新版

为了解决旧版复制功能在处理断线重复制情况时的低效问题。

实现

使用PSYNC命令,具有完整重同步和部分重同步两种模式

(1)完整重同步,用于初次复制情况

​ 和SYNC命令类似,传送RDB文件和写命令缓冲区。

(2)部分重同步,用于处理断线后重复制情况

​ 主服务器将主从服务器连接断开期间执行的写命令发送给从服务器。

心跳检测

(1)在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:

​ REPLCONF ACK

(2)作用:

​ · 检测主从服务器的网络连接状态

​ · 辅助实现min-slaves选项

​ · 检测命令丢失

哨兵机制

意义

sentinel是Redis的高可用性解决方案。监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。

若之前的主服务器重新上线,则自动成为现存主服务器的从服务器。

img

启动sentinel

(1)初始化服务器

​ · Sentinel本质上是一个运行在特殊模式下的Redis服务器

​ · Sentinel不适用数据库,不会载入RDB或者AOF文件

(2)将普通Redis服务器使用的代码替换为sentinel专用代码

(3)初始化sentinel状态

(4)根据配置文件,初始化Sentinel的监视主服务器列表

(5)创建连向主服务器的网络连接

​ · 命令连接:向服务器发送命令和接收命令回复

​ · 订阅连接:订阅服务器的_sentinel_:hello频道

获取服务器信息

(1)Sentinel默认以十秒一次的频率,通过命令连接向被监视的主服务器发送INFO命令

可以获取以下消息:

​ · 主服务器本身的信息

​ · 所有从服务器的信息

(2)Sentinel默认以十秒一次的频率,通过命令连接向被监视的从服务器发送INFO命令

发送和接收

(1)sentinel每两秒一次发送命令给监视的主从服务器的_sentinel_:hello频道

(2)订阅连接建立之后,通过_sentinel_hello频道获取信息。

检测是否下线

(1)主观下线

​ Sentinel默认每次一秒的频率向建立了命令连接的Redis实例发送PING命令。若在down-after-milliseconds选项配置的时间内没有有效回复,认为为主观下线状态。

(2)客观下线

​ 当Sentinel将一个主服务器判断为主观下线后,为了确认是否真的下线了,会向同样监视这一主服务器的其他Sentinel进行询问,若其他Sentinel也认为为下线状态,在接收到足够数量的下线判断后,Sentinel认为主服务器为客观下线,并进行故障转移操作。

选举领头Sentinel

当一个主服务器为客观下线时,监视这个主服务器的所有Sentinel选举一个领头Sentinel对主服务器执行故障转移操作。

主要步骤如下:

(1)在一个配置纪元(计数器)里面,所有Sentinel有一次将某个Sentinel设置为局部领头Sentinel的机会,并且局部领头一旦设置,在这个配置纪元里面就不能再更改。

(2)Sentinel向另一个Sentinel发送带有自己运行ID的命令,让其设置自己为局部领头Sentinel(相当于抢票)。

(3)局部领头Sentinel规则:先到先得。已经设置为别人为Sentinel的Sentinel,拒绝后续收到的设置

(4)如果某个Sentinel被半数以上的Sentinel设置成了局部领头Sentinel,那么这个Sentinel成为领头Sentinel。

故障转移

(1)在已下线主服务器属下的所有从服务器中,挑选出一个从服务器,将其转换为主服务器。

​ 领头Sentinel按照从服务器的优先级,对列表中剩余的从服务器进行排序,并选出其中优先级最高的从服务器

(2)让已下线主服务器属下的所以从服务器改为复制新的主服务器。

(3)将已下线主服务器设置为新的主服务器的从服务器,当这个旧的主服务器重新上线时,它就会成为新的主服务器的从服务器。

集群

Redis集群是Redis提供的分布式数据库方案,集群通过分片进行数据共享,并提供复制和故障转移功能。

节点

(1)CLUSTER MEET命令

​ · 格式 CLUSTER MEET

​ · 向另一个节点发送命令,进行握手,握手成功后加入所在集群。

(2)启动节点

img

(3)集群数据结构

· clusterNode 每个节点使用其记录自己的状态,并为集群中其他节点创建一个相应的clusterNode结构

img

· clusterLink 保存了连接节点所需的有关信息

img

· clusterState 记录在当前节点的视角下,集群目前所处状态

img

(4)节点握手

img

槽指派

(1)概念

​ Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384割槽(slot),数据库中的每个键都属于整个16384个槽的其中一个,集群中的每个节点可以处理0~16384个槽。

为什么有16384个槽

image-20210701145808348

(2)数据结构

struct clusterNode{
  //....
  unsigned char slots[16384/8];
  int numslots;
  // ....
}

· 二进制数组中索引上的二进制位为1,则表示节点负责处理该槽

· numslots表示该节点负责的槽数量

(3)相关命令

CLUSTER ADDSLOTS 指派槽

(4)注意点

节点数据库只能使用0号数据库,这和单机服务器的数据库不同。

复制和故障转移

复制

img

主节点(master)用于处理槽,从节点用于复制某个主节点,在其主节点下线时可以代替主节点继续处理命令请求。

故障转移

(1)在从节点中选一个成为新的主节点

​ 新主节点的选取,类似于sentinel领头的选取,算法都是基于Raft算法实现的。

(2)新的主节点撤销对已下线主节点的槽指派,并将这些槽指派给自己

(3)新的主节点广播PONG消息,让其他节点知道自己成为新的主节点

(4)接收和处理自己的槽相关的请求,故障转移完成。

发布与订阅

概述

(1)由PUBLISH(发送消息)、SUBSCRIBE(订阅频道)、PSUBSCRIBE(订阅模式)等命令组成

(2)通过执行SUBSCRIBE命令,客户端可以订阅一个或多个频道,每当有其他客户端向被订阅的频道发送消息时,频道的所有订阅者都会受到这条消息。

(3)通过执行PSUBSCRIBE命令订阅一个或多个模式,每当有其他客户端向某个频道发送消息时,消息会被发送给与这个频道相匹配的模式的订阅者。

img

频道

数据结构

struct redisServer{
  // ...
  // 保存所有频道的订阅关系
  dict *pubsub_channels;
  // ...
}

· 字典的键是某个被订阅的频道

· 字典的值是一个链表,记录所有订阅该频道的客户端

img

订阅和退订

(1)订阅:使用SUBSCRIBE命令,在链表的尾部添加

(2)退订:使用UNSUBSCRIBE命令,从链表中删除客户端。当出现键对应空链表,要从字典中删除键

模式

数据结构

struct redisServer{
 // ...
  // 保存所有频道的订阅关系
  dict *pubsub_patterns;
 // ...
}

订阅和退订

类似频道。

发送消息

PUBLISH命令:在pubsub_channels字典里找到频道channel的订阅者名单(一个链表),然后将消息发送给名单上的所有客户端。

事务

概述

使用MULTI(事务开始)、EXEC(提交事务)、WATCH等命令来实现事务。

事务提供了一种多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求。

事务的实现

(1)事务开始

​ · MULTI命令将客户端切换至事务状态。

​ · 原理:在客户端状态的flags属性中打开REDIS_MULTI标识。

(2)命令入队

​ · 如果是EXEC、DISCARD、WATCH、MULTI四个命令,立即执行

​ · 其他命令放入事务队列,然后客户端返回QUEUED回复

(3)事务执行

​ 遍历客户端的事务队列,执行队列中保存的所有命令。

WATCH命令

(1)作用

​ 是一个乐观锁,在执行EXEC命令前,监视任意数量的数据库键。在EXEC命令执行时,若监视的键是否至少有一个已经被修改过了,如果是的话,服务器拒绝执行事务,向客户端返回空回复。

img

(2)数据结构

typedef struct redisDb{
  // ...
  // 正在被WATCH命令监视的键
  dict *watched_keys;
  // ...
}

每个数据库都保存一个字典,键为WATCH命令监视的数据库键。值为一个链表,记录所有监视相应数据库键的客户端。

(3)当被监视的键被修改,则客户端的REDIS_DITRY_CAS标识打开。

缓存问题

缓存雪崩

(1)原因

​ 缓存同一时间大面积的失效,大量的请求直接落到数据库上,造成数据库短时间内承受大量请求而崩掉。

(2)解决办法

img

· 事前:尽量保证整个Redis集群的高可用性,发现机器宕机几块补上,选择合适的内存淘汰策略。

· 事中: 本地ehcache缓存+hystrix限流&降级,避免MySQL崩掉

· 事后:利用redis持久化机制保存的数据尽快恢复缓存

缓存穿透

原因

​ 大量请求的key根本不在缓存中,导致请求直接到了数据库上。

​ 一般MySQL的最大连接数在150左右,最大连接数还只是一个指标,cpu,内存,自盘,网络等

img

解决办法

无效key的时间减短

黑客每次构建不同的请求Key,会导致redis中缓存大量无效的key,故可以将无效key的过期时间设置短一点。

布隆过滤器

(1)布隆过滤器的概念

布隆过滤器(Bloom Filter)可以看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。

img

位数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 kb ≈ 122kb 的空间。

总结:一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大。

(2)布隆过滤器的原理介绍

· 当一个元素加入布隆过滤器中的时候,会进行如下操作:

​ 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)

​ 根据得到的哈希值,在位数组中把对应下标的值置为 1

· 当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:

​ 对给定元素再次进行相同的哈希计算

​ 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中

举个简单的例子:

img

不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。

综上,我们可以得出:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

在Redis中具体工作机制如下:

img

(3)布隆过滤器的实现

可以参考:Springboot + Redis的布隆过滤器实现

内存淘汰机制

保证Redis中的数据为热点数据。

(1)volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰

(2)volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰

(3)volatile-random:从已设置过期时间的数据集中挑选任意数据淘汰

(4)allkeys-lru:内存不足时,在键空间中移除最近最少使用的key(最常用)

(5)allkeys-random:从数据集中任意选择数据淘汰

(6)no-eviction:禁止驱逐数据

redis4.0后新增

(7)volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰

(8)allkeys-lfu:内存不足时,在键空间中移除最不经常使用的key

Redis分布式锁

分布式锁一般有三种方式实现:数据库乐观锁,基于Redis的分布式锁,基于zookeeper的分布式锁

<dependency>
  <groupId>redis.clients</groupId>
  <artifactId>jedis</artifactId>
  <version>2.9.0</version>
</dependency>
 public class RedisTool {
   private static final String LOCK_SUCCESS = "OK";
   private static final String SET_IF_NOT_EXIST = "NX";
   private static final String SET_WITH_EXPIRE_TIME = "PX";
   /**
   \* 尝试获取分布式锁
   \* @param jedis Redis客户端
   \* @param lockKey 锁
   \* @param requestId 请求标识
   \* @param expireTime 超期时间
   \* @return 是否获取成功
   */
   public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
    String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    if (LOCK_SUCCESS.equals(result)) {
      return true;
    }
    return false;
   }
}

这个set()方法一共有五个形参:

· 第一个为key,我们使用key来当锁,因为key是唯一的。

· 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

· 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

· 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

· 第五个为time,与第四个参数相呼应,代表key的过期时间。

方法底层主要使用Redis的Setnx 命令实现。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

缓存与数据库一致性问题

首先写入缓存中的数据,都设置失效时间,这样一来,缓存中不经常访问的数据,随着时间的推移,都会逐渐「过期」淘汰掉,最终缓存中保留的,都是经常被访问的「热数据」,缓存利用率得以最大化。

在满足实时性的条件下,不存在两者完全保存一致的方案,只有最终一致性方案。

我们对比以下 6 种方案:

  1. 先写 Redis,再写 MySQL

    这种方案,我肯定不会用,万一 DB 挂了,你把数据写到缓存,DB 无数据,这个是灾难性的;

    我之前也见同学这么用过,如果写 DB 失败,对 Redis 进行逆操作,那如果逆操作失败呢,是不是还要搞个重试?

  2. 先写 MySQL,再写 Redis

    对于并发量、一致性要求不高的项目,很多就是这么用的,我之前也经常这么搞,但是不建议这么做;

    当 Redis 瞬间不可用的情况,需要报警出来,然后线下处理。

  3. 先删除 Redis,再写 MySQL

    这种方式,我还真没用过,直接忽略吧。

  4. 先删除 Redis,再写 MySQL,再删除 Redis

    这种方式虽然可行,但是感觉好复杂,还要搞个消息队列去异步删除 Redis。

  5. 先写 MySQL,再删除 Redis

    比较推荐这种方式,删除 Redis 如果失败,可以再多重试几次,否则报警出来;

    这个方案,是实时性中最好的方案,在一些高并发场景中,推荐这种。

  6. 先写 MySQL,通过 Binlog,异步更新 Redis

    对于异地容灾、数据汇总等,建议会用这种方式,比如 binlog + kafka,数据的一致性也可以达到秒级;

    纯粹的高并发场景,不建议用这种方案,比如抢购、秒杀等。

个人结论:

  • 实时一致性方案:采用“先写 MySQL,再删除 Redis”的策略,这种情况虽然也会存在两者不一致,但是需要满足的条件有点苛刻,所以是满足实时性条件下,能尽量满足一致性的最优解。
  • 最终一致性方案:采用“先写 MySQL,通过 Binlog,异步更新 Redis”,可以通过 Binlog,结合消息队列异步更新 Redis,是最终一致性的最优解。

详情看:https://mp.weixin.qq.com/s/RL4Bt_UkNcnsBGL_9w37Zg

另外,Redis如果写数据库成功了,写缓存失败了怎么办?

  • 高峰期的时候让缓存的过期时间小一点,但是明显不能完全解决这个问题~

  • 可以单独写一个定时任务,定时对比数据库和Redis中的数据,数据中可以加入版本号,查阅版本号是否一致等等。

Redis实现消息队列

参考:Redis实现消息队列的4种方案


文章作者: 小小千千
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小小千千 !
评论
  目录