Redis相关知识

一、redis的String、List、Hash底层数据结构

1、String

redis构建了一个叫做简单动态字符串(Simple Dynamic String),简称SDS

代码结构:

struct sdshdr{
    //  记录已使用长度
    int len;
    // 记录空闲未使用的长度
    int free;
    // 字符数组
    char[] buf;
};

 动态扩展:

1、计算出大小是否足够

2、开辟空间至满足所需大小

3、开辟与已使用大小len相同长度的空闲free空间(如果len < 1M)开辟1M长度的空闲free空间(如果len >= 1M)

性能优势:

1、快速获取字符串长度

2、避免缓冲区溢出

3、性能提升(字符串追加与缩减)

  • 空间预分配(可以减少内存分配次数)
  • 惰性空间回收

参考:redis 内部是怎么实现它的字符串的 - WalkingCamel - 博客园

2、List

1、LinkedList(双向链表)

typedf struct list{
    //头指针
    listNode *head;
    //尾指针
    listNode *tail;
    //节点拷贝函数
    void *(*dup)(void *ptr);
    //释放节点函数
    void *(*free)(void *ptr);
    //判断两个节点是否相等的函数
    int (*match)(void *ptr,void *key);
    //链表长度
    unsigned long len;
}
//定义链表节点的结构体 
typedf struct listNode{
    //前一个节点
    struct listNode *prev;
    //后一个节点
    struct listNode *next;
    //当前节点的值的指针
    void *value;
}listNode;

2、zipList

typedf struct ziplist<T>{
    //压缩列表占用字符数
    int32 zlbytes;
    //最后一个元素距离起始位置的偏移量,用于快速定位最后一个节点
    int32 zltail_offset;
    //元素个数
    int16 zllength;
    //元素内容
    T[] entries;
    //结束位 0xFF
    int8 zlend;
}ziplist
typede struct entry{
    //前一个entry的长度
    int<var> prelen;
    //元素类型编码
    int<var> encoding;
    //元素内容
    optional byte[] content;
}entry

 

连锁更新

entry中有一个prelen字段,它的长度要么是1个字节,要么都是5个字节:

  • 前一个节点的长度小于254个字节,则prelen长度为1字节;
  • 前一个节点的长度大于254字节,则prelen长度为5字节;

假设现在有一组压缩列表,长度都在250~253字节之间,突然新增一个entry节点,这个entry节点长度大于等于254字节。由于新的entry节点大于等于254字节,这个entry节点的prelen为5个字节,随后会导致其余的所有entry节点的prelen增大为5字节。

linkedList与zipList的对比

  • 双向链表linkedList便于在表的两端进行pushpop操作,在插入节点上复杂度很低,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还有额外保存两个指针;其次,双向链表的各个节点都是单独的内存块,地址不连续,容易形成内存碎片
  • zipList存储在一块连续的内存上,所以存储效率很高。但是它不利于修改操作,插入和删除操作需要频繁地申请和释放内存。特别是当zipList长度很长时,一次realloc可能会导致大量的数据拷贝。
  • 当列表对象中元素的长度较小或者数量较少时,通常采用zipList来存储;当列表中元素的长度较大或者数量比较多的时候,则会转而使用双向链表linkedList来存储。

3、quickList

Redis3.2版本之后,list的底层实现方式又多了一种,quickListqucikList是由zipList和双向链表linkedList组成的混合体。它将linkedList按段切分,每一段使用zipList来紧凑存储,多个zipList之间使用双向指针串接起来。示意图如下所示:

typedf struct quicklist{
    //指向头结点
    quicklistNode* head;
    //指向尾节点
    quicklistNode* tail;
    //元素总数
    long count;
    //quicklistNode节点的个数
    int nodes;	
    //压缩算法深度
    int compressDepth;		
    ...
}quickList
typedf struct quicklistNode{
    //前一个节点
    quicklistNode* prev;
    //后一个节点
    quicklistNode* next;
    //压缩列表
    ziplist* zl;	
    //ziplist大小
    int32 size;		
    //ziplist 中元素数量
    int16 count;
    //编码形式 存储 ziplist 还是进行 LZF 压缩储存的zipList
    int2 encoding;			
    ...
}quickListNode

 4、总结

 参考:Redis底层数据结构之list - Reecelin - 博客园

3、hash

1、zipList

前面已介绍

2、dict

typedf struct dict{
    dictType *type;//类型特定函数,包括一些自定义函数,这些函数使得key和
                   //value能够存储
    void *private;//私有数据
    dictht ht[2];//两张hash表 
    int rehashidx;//rehash索引,字典没有进行rehash时,此值为-1
    unsigned long iterators; //正在迭代的迭代器数量
}dict;
typedf struct dictht{
    dictEntry **table;//存储数据的数组 二维
    unsigned long size;//数组的大小
    unsigned long sizemask;//哈希表的大小的掩码,用于计算索引值,总是等于 
                           //size-1
    unsigned long used;//// 哈希表中中元素个数
}dictht;
typedf struct dictEntry{
    void *key;//键
    union{
        void val;
        unit64_t u64;
        int64_t s64;
        double d;
    }v;//值
    struct dictEntry *next;//指向下一个节点的指针
}dictEntry;

 扩容与缩容

负载因子

哈希表中已保存节点数量/哈希表的大小(load factor = ht[0].used / ht[0].size)

触发规则

  • 没有执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的负载因子大于等于1时进行扩容;
  • 正在执行BGSAVE和BGREWRITEAOF指令的情况下,哈希表的负载因大于等于5时进行扩容;
  • 负载因子小于0.1时,Redis自动开始对哈希表进行收缩操作;

数量规则

  • 扩容:扩容后的dictEntry数组数量为第一个大于等于ht[0].used * 22^n
  • 缩容:缩容后的dictEntry数组数量为第一个大于等于ht[0].used2^n

渐进式rehash

  • 假设当前数据在dictht[0]中,那么按照扩容和缩容规则为dictht[1]分配足够的空间;
  • 设置rehashidx=0,表示rehash正式开始;
  • rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将dictht[0]哈希表在rehashidx索引上的所有键值对rehashdictht[1],当一次rehash工作完成之后,程序将rehashidx属性的值+1。同时在serverCron中调用rehash相关函数,在1ms的时间内,进行rehash处理,每次仅处理少量的转移任务(100个元素);
  • 随着字典操作的不断执行,最终在某个时间点上,dictht[0]的所有键值对都会被rehashdictht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。

某一时间段并没有任何请求命令应该怎么办?

Redis在有一个定时器,会定时去判断rehash是否完成,如果没有完成,则继续进行rehash。

在维护两个dictht的时候,此时哈希表如何正常对外提供服务?

对于添加操作,会将新的数据直接添加到dictht[1]上面,这样就可以保证dictht[0]上的数量只减少不增加。而对于删除、更改、查询操作,会直接在dictht[0]上进行,尤其是这三个操作,都会涉及到查询,当在dictht[0]上查询不到时,会接着去dictht[1]上查找,如果再找不到,则表明不存在该K-V值。

渐进式rehash的优缺点

优点采用了分而治之的思想,将rehash 操作分散到每一个对该哈希表的操作上以及定时函数上,避免了集中式rehash 带来的性能压力;

缺点在 rehash 的时间内,需要保存两个 hash 表,对内存的占用稍大,而且如果在 redis 服务器本来内存满了的时候,突然进行 rehash 会造成大量的 key 被抛弃。

3、总结

参考:Redis底层数据结构之hash - Reecelin - 博客园

4、zset

参考:https://segmentfault.com/a/1190000037473381?utm_source=tag-newest

二、Redis两种持久化机制RDB和AOF

1、RDB机制

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。

三种触发方式

save触发方式

该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。具体流程如下:

bgsave触发方式

执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。阻塞只发生在fork阶段,一般时间很短。具体流程如下:

自动触发方式

自动触发是由我们的配置文件来完成的。在redis.conf配置文件中,里面有如下配置,我们可以去设置:

save:这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。

RDB的优势与劣势

优势

(1)RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。

(2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。

(3)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快

劣势

当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据

2、AOF机制

全量备份总是耗时的,有时候我们提供一种更加高效的方式AOF,工作机制很简单,redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。

持久化原理

每当有一个写命令过来时,就直接保存在我们的AOF文件中。

文件重写原理

AOF的方式也同时带来了另一个问题。持久化文件会变的越来越大。为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。

重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。

三种触发机制

(1)always每修改同步:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好

(2)everysec每秒同步:异步操作,每秒记录 如果一秒内宕机,有数据丢失

(3)no不同:从不同步

AOF的优势与劣势

优势

(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。

(2)AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损

(3)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

劣势

(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大

(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的

(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。

3、总结

参考:百度安全验证

三、Redis过期删除策略和内存淘汰策略

1、过期时间判定

在Redis内部,每当我们设置一个键的过期时间时,Redis就会将该键带上过期时间存放到一个过期字典中。当我们查询一个键时,Redis便首先检查该键是否存在过期字典中,如果存在,那就获取其过期时间。然后将过期时间和当前系统时间进行比对,比系统时间大,那就没有过期;反之判定该键过期。

2、过期删除策略

惰性删除

设置该key 过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。

定期删除

每隔一段时间,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。

注意:并不是一次运行就检查所有的库,所有的键,而是随机检查一定数量的键。

定期删除函数的运行频率,在Redis2.6版本中,规定每秒运行10次,大概100ms运行一次。在Redis2.8版本后,可以通过修改配置文件redis.conf 的 hz 选项来调整这个次数。

3、内存淘汰策略

当现有内存大于 maxmemory 时,便会触发redis主动淘汰内存方式,通过设置 maxmemory-policy ,有如下几种淘汰方式:

  1)volatile-lru   利用LRU算法移除设置过过期时间的key (LRU:最近使用 Least Recently Used ) 。

  2)allkeys-lru   利用LRU算法移除任何key (和上一个相比,删除的key包括设置过期时间和不设置过期时间的)。通常使用该方式

  3)volatile-random 移除设置过过期时间的随机key 。

  4)allkeys-random  无差别的随机移除。

  5)volatile-ttl   移除即将过期的key(minor TTL) 

  6)noeviction 不移除任何key,只是返回一个写错误 ,默认选项,一般不会选用

4、总结

Redis过期删除策略是采用惰性删除和定期删除这两种方式组合进行的,惰性删除能够保证过期的数据我们在获取时一定获取不到,而定期删除设置合适的频率,则可以保证无效的数据及时得到释放,而不会一直占用内存数据。

但是我们说Redis是部署在物理机上的,内存不可能无限扩充的,当内存达到我们设定的界限后,便自动触发Redis内存淘汰策略,而具体的策略方式要根据实际业务情况进行选取。

参考:Redis详解(十一)------ 过期删除策略和内存淘汰策略 - YSOcean - 博客园

四、Redis三种集群模式

1、主从复制

2、哨兵模式

3、Cluster集群

参考:Redis ==> 集群的三种模式 - 破解孤独 - 博客园

五、Redis分布式锁应用场景

1、抢不到锁的请求,允许丢弃

比如:一些不是很重要的场景,比如“监控数据持续上报”,某一篇文章的“已读/未读”标识位更新,对于同一个id,如果并发的请求同时到达,只要有一个请求处理成功,就算成功。

2、并发请求,不论哪一条都必须要处理的场景

比如:一个订单,客户正在前台修改地址,管理员在后台同时修改备注。地址和备注字段的修改,都必须正确更新。

解决思路:A,B二个请求,谁先抢到分布式锁谁先处理(A),抢不到的(B)在一旁不停等待重试,重试期间一旦发现B获取锁成功,即表示A已经处理完,把锁释放了。

但有二点要注意:

a、设置重试次数或设置重试超时时间。否则如果A处理过程中有bug,一直卡死,或者未能正确释放锁,B就会一直会等待重试,但是又永远拿不到锁。

b、等待最长时间,必须小于锁的过期时间。否则,假设锁2秒过期自动释放,但是A还没处理完(即:A的处理时间大于2秒),这时锁会因为redis key过期“提前”误释放,B重试时拿到锁,造成A,B同时处理。

参考:基于redis的分布式锁二种应用场景 - 菩提树下的杨过 - 博客园

六、进程内缓存

1、与进程外缓存相比

  • 节省内存带宽
  • 响应时延降低

2、进程内缓存缺点

  • 数据存了多份,一致性比较难保障

3、进程缓存如何保证数据一致性

  • 单节点通知其他节点。缺点:多个节点相互耦合
  • 通过MQ通知其他节点。缺点:引入了MQ,使得系统更复杂
  • 每个节点启动一个timer,定时从后端拉取最新数据,更新内存缓存。缺点:放弃了“实时一致性”

4、为什么不能频繁使用进程内缓存

答:分层架构设计,有一条准则:站点层、服务层要做到无数据无状态,这样才能任意的加节点水平扩展,数据和状态尽量存储到后端的数据存储服务,例如数据库服务或者缓存服务。

可以看到,站点与服务的进程内缓存,实际上违背了分层架构设计的无状态准则,故一般不推荐使用。

5、什么时候可以使用进程内缓存

  • 只读数据
  • 极其高并发的,如果透传后端压力极大的场景
  • 允许数据不一致业务

参考:进程内缓存,究竟怎么玩?

七、选redis还是memcache

1、什么情况倾向于选择redis

复杂数据结构:memcache仅支持KV结构

持久化:memcache无法满足持久化需求

天然高可用:而memcache,要想要实现高可用,需要进行二次开发,例如客户端的双读双写,或者服务端的集群同步。

存储内容比较大:memcache的value存储,最大为1M

2、什么情况倾向于选择memcatche

纯KV,数据量非常大,并发量非常大的业务,使用memcache或许更适合。

内存分配

memcache使用预分配内存池的方式管理内存,能够省去内存分配时间。

redis则是临时申请空间,可能导致碎片。

从这一点上,mc会更快一些

虚拟内存使用

memcache把所有的数据存储在物理内存里。

redis有自己的VM机制,理论上能够存储比物理内存更多的数据,当数据超量时,会引发swap,把冷数据刷到磁盘上。

从这一点上,数据量大时,mc会更快一些

网络模型

memcache使用非阻塞IO复用模型,redis也是使用非阻塞IO复用模型。

但由于redis还提供一些非KV存储之外的排序,聚合功能,在执行这些功能时,复杂的CPU计算,会阻塞整个IO调度。

从这一点上,由于redis提供的功能较多,mc会更快一些

线程模型

memcache使用多线程,主线程监听,worker子线程接受请求,执行读写,这个过程中,可能存在锁冲突。

redis使用单线程,虽无锁冲突,但难以利用多核的特性提升整体吞吐量。

从这一点上,mc会快一些

参考:选redis还是memcache,源码怎么说?

八、缓存,你真的用对了么?

误用一:把缓存作为服务与服务之间的传递数据的媒介

存在问题:1、MQ更合适,2、服务耦合

误用二:使用缓存未考虑雪崩

存在问题:缓存挂掉,可能会把数据库压垮

解决方案:1、高可用缓存,2、缓存水平切分

误用三:调用方缓存数据

存在问题:数据不一致

误用四:多服务公用缓存实例

存在问题:1、不同服务吞吐量不一样,容易导致一个服务把另一个服务的热数据挤出去,2、服务耦合

参考:缓存,你真的用对了么?

九、缓存,究竟是淘汰,还是修改?

1、淘汰和修改缓存区别

  • 淘汰某个key,操作简单,直接将key置为无效,但下一次该key的访问会cache miss
  • 修改某个key的内容,逻辑相对复杂,但下一次该key的访问仍会cache hit

2、结论

  • 大部分情况,修改value成本会高于“增加一次cache miss”,因此应该淘汰缓存
  • 如果还在纠结,总是淘汰缓存,问题也不大

参考:缓存,究竟是淘汰,还是修改?

十、Cache Aside Pattern

1、介绍

旁路缓存方案的经验实践,这个实践又分为读实践,写实践

对于读请求

  • 先读cache,再读db
  • 如果,cache hit,则直接返回数据
  • 如果,cache miss,则访问db,并将数据set回缓存

对于写请求

  • 淘汰缓存,而不是更新缓存
  • 先操作数据库,再淘汰缓存

2、原因

Cache Aside Pattern为什么建议淘汰缓存,而不是更新缓存?

如果更新缓存,在并发写时,可能出现数据不一致

  1. 请求1先操作数据库,请求2后操作数据库
  2. 请求2先set了缓存,请求1后set了缓存

Cache Aside Pattern为什么建议先操作数据库,再操作缓存?

如果先操作缓存,数据不一致的时间窗口不可控

  1. 线程A淘汰缓存
  2. 线程B读缓存(cache miss)
  3. 线程B读库
  4. 线程B设置缓存
  5. 线程A写库

3、问题

如果先操作数据库,再淘汰缓存,在原子性被破坏时

  1. 修改数据库成功了
  2. 淘汰缓存失败了

导致,数据库与缓存的数据不一致

4、扩展

原子性被破坏情况下,先操作缓存,再操作数据库,不会导致数据不一致(但并发条件下会)

参考:Cache Aside Pattern

先删除缓存,后更新数据库,以下情况会出现数据库不一致

1、线程A淘汰缓存、线程B读缓存、线程B读库、线程B设置缓存、线程A更新库。(概率大)

先更新数据库,后删除缓存,以下情况会出现数据库不一致

1、线程A更新库成功,删除缓存失败;(概率小)

2、线程A更新库,线程A删除缓存,线程B读缓存、线程B读库、线程C更新库,线程C删除缓存、线程B设置缓存(概率小,需要对同一数据进行两次更新)

十一、数据库主从不一致

方案一:忽略

如果业务能接受,最推崇此法,别把系统架构搞得太复杂。

方案二:强制读主

  1. 使用一个高可用主库提供数据库服务
  2. 读和写都落到主库上
  3. 采用缓存来提升系统读性能

方案三:选择性读主

当写请求发生时

  1. 写主库
  2. 将哪个库,哪个表,哪个主键三个信息拼装一个key设置到cache里,这条记录的超时时间,设置为“主从同步时延”

当读请求发生时

  1. cache里有这个key,说明1s内刚发生过写请求,数据库主从同步可能还没有完成,此时就应该去主库查询
  2. cache里没有这个key,说明最近没有发生过写请求,此时就可以去从库查询

参考:数据库主从不一致,怎么解?

十二、redis实现分布式事务

1、使用setnx命令获取分布式锁

2、为了防止获取分布式锁后服务挂掉导致锁一直无法释放,故设置超时时间n(必须和setnx实现原子操作,redisTemplate中存在该方法)

3、为了解决程序执行时间大于超时时间,在获取到分布式锁后,起一个线程,每隔n/3秒查询下锁是否还存在,存在则超时时间为n

4、redisson封装实现了上述逻辑

十三、redis数据结构的应用

1、string

2、hash

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值