redis

一、数据类型

1、redisObject

redis 针对不同数据类型,定义了统一的数据结构redisObject 来进行管理,所以,redisObject是所有数据类型最外层的数据定义;

redisObject 结构如下:

    typedef struct redisObject {
        unsigned type:4;
        unsigned encoding:4;
        unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
        int refcount;
        void *ptr;
    } robj;

type:当前值对象的数据结构类型

encoding:当前值对象底层储存的一个编码

lru:记录当前值对象最后一次被访问的时间(时间戳,用于LRU算法)

regcount:当前值对象被引用的次数(用于LFU算法)

ptr:真实数据引用地址

2、基本数据类型

1、SDS:简单动态字符串
struct sdshdr {
    int len;
    int free;
    char buf[];
};

len:记录 buf 数组中已使用字节的数量,即SDS 所保存字符串的长度

free:记录 buf 数组中未使用字节的数量,为0时表示没有未分配可以使用的空间

buf[]:字节数组,用于保存字符串

redis使用自定义的sds,而不使用c语言的字符串,是因为

①使用c语言的字符串,在扩容时,需要频繁分配内存;

②获取字符串长度时,需要遍历整个字符串;

③c语言的字符串,在以标记'\0'为结束,容易将有意义的'\0'给截断

2、dict 字典
typedef struct dict {
   dictType *type;
   void *privdata;
   dictht ht[2];
   long rehashidx; /* rehashing not in progress if rehashidx == -1 */
   unsigned long iterators; /* number of iterators currently running */
} dict;

type:一个指向dictType构的指针,它使得dict的key和value能够储存任意的类型的数据;

privdata:一个私有数据指针,由调用者在创建dict时传进来的,配合type字段指向函数一起使用;

ht[2]:一个字典中存放俩个hash表,平常使用的是ht[0]。只有在扩容时才会使用到ht[1];

rehashidx:值为-1时,表示没有进行过rehash,当有进行rehash时,该值用来表示ht[0]rehash到哪个元素,并且记录该元素的数组下标值;

iterators:用来记录当前运行的安全迭代器数,当不为0时,表明由迭代器正在运行,会停止rehash.

dictht 哈希表(dict hash table)
/*
* 哈希表
*/
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

/*
 * 哈希表节点
 */
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    struct dictEntry *next;
} dictEntry;

table:哈希表指针数组,key的哈希值最终映射到这个数组的某个位置上,如果映射到同一个位置,则造成哈希冲突,会形成一个链表;

size:dictEntry指针数组长度,总是2的指数次幂;

sizemask:用于将哈希值映射到table的位置索引;

used: 记录dict中现有的个数

普通状态下的字典(没有rehash)

3、zipList

ziplist是由一系列特殊编码的连续内存块组成的顺序存储结构,类似于数组,ziplist在内存中是连续存储的,但是不同于数组,为了节省内存 ziplist的每个元素所占的内存大小可以不同(数组中叫元素,ziplist叫节点entry),每个节点可以用来存储一个整数或者一个字符串。

struct ziplist<T> {
  int32 zlbytes;   
  int32 zltail_offset; 
  int16 zllength; 
 T[] entries; 
 int8 zlend; 
}

zlbytes:整个压缩列表占用字节数,包含本身
zltail_offset:最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点,从而可以在ziplist尾部快速的执行push, pop操作
zllength:元素个数,该字段只有16bit所以可以表达的最大值为2^16-1,如果ziplist元素超了该值呢?这里规定,如果zllength小于等于 2^16-2,该字段表示为ziplist中元素的个数,否则想知道ziplist长度需要遍历整个ziplist
entries:元素内容列表,挨个挨个紧凑存储
zlend:ziplist最后一个字节,标志压缩列表的结束,值恒为 0xFF(255)

二、基本指令

SET:用于分配 一个键值对,需要注意的是,如果重新为某个键值赋值,set会覆盖前一个值,成功返回“OK”;

GET:用于获取之前的存储

MSET:一次设置多个键值

MGET:一次获取多个键值

INCR:自增

DECR:自减

INCBY:根据某个参数进行增加;

DECRBY:根据某个参数进行减少;

DEL:删除某个键值对,成功返回1;

EXISTS:查询某个键是否存在,存在返回1,否则返回0;

TYPE:查询某个键对应值的类型,成功返回类型,否则返回0;

EXPIRE:设置某个键值对多久后过期,单位为秒和毫秒,默认为秒;

PEXPIRE:设置某个键值对永久保持,如果该键值对本身不过期,返回0,键值对本身有过期,返回1;

TTL:查询某个键值对还有多久过期,单位默认为秒,成功返回结果,如果键值对永久保持,返回-1,键值对不存在返回-2;

三、高级特性

一、Lua

lua:是一种轻巧的脚本语言,而且会将整个Lua脚本当作一个命令进性执行,所以具备原子性,可以解决安全问题;

优点:

①减少网络消耗,多个命令一次性传输;

②具有原子性:所有命令要么全部执行成功,要么全部失败;

③可复用,客户端发送的脚本会永久保存在redis中,有相似的需求可以直接复用。

二、事务

1、机制(执行过程)

MULTI进入事务(MULTI标记事务开始) --> 输入命令会被加入commands队列 ---> EXEC开启并执行事务

1、特性

①不支持回滚,redis单线程执行事务,不存在并发问题,只有命令错误或类型错误,这些错误开发时应该发现并解决,因此不需要回滚;

②写入日志后行:与其他数据库日志先行特性不同,redis是日志后行,redis要保证轻量,必须保证吞吐量和速度足够快,日志先行会拖慢写入速度;但日志后行会带来写入一半没有日志可以回放恢复,因此需要使用redis-check-aof直接删除出错的数据。

③WATCH机制:redis事务是具备原子性和隔离性,但这进队EXEC阶段而言,MULTI将命令入队后执行,存在执行前相关的key被其它命令修改导致命令错误的风险,redis提供WATCH机制,在MULTI开始后,会使用WATCH机制关注某个key,在EXEC前,如果key被修改,执行时会失败退出。

2、redis的事务和数据库事务的不同(以mysql为例)

①实现过程:

mysql:

Begin - 显式的开启一个事务

Commit - 提交事务,将对数据库进行的所有修改变成永久性;

Rollback - 结束用户的事务,并撤销现在正在进行的,未提交的修改。

redis:

Mulit - 标记事务的开始

Exec - 执行事务的commands队列

Discard - 结束事务,并清除commands队列

②实现原理

mysql:

msyql实现事务,是基于undo/redo日志实现,

undo记录修改前的状态,rollback基于undo基于undo实现;

redo记录修改后的状态,commit基于redo日志实现;

基于redo日志实现记录修改后的状态,因为redo日志是innodb专有的,所以innodb会支持事务;

在mysql中无论是否开启事务,sql都会被立即执行并返回执行结果,只是事务开启后执行后的状态只是记录在redo日志,执行commit后,才会被写入磁盘。

redis:

redis实现事务,是基commands队列,如果没有开启事务,command将会被立即执行并返回执行结果,并且直接写入磁盘,如果事务开启,command不会立即被执行,而是排入队列,并返回排队状态。

③四大特性

mysql: mysql事务必须保证四大特性,即:原子性,一致性,隔离性和持久性;

redis:redis事务只保证了其中的一致性和隔离性;

redis中如果一个命令执行失败,事务中的其它命令会继续执行,所以不保证原子性;

事务中的命令行都会被序列化,按顺序执行,在执行的过程总不会被其它客户端发来的命令打断,队列中的命令在事务们没有被提交之前不会实际执行,因此不保证隔离性。

四、底层机制

(1)内存模型

1、内存的划分

redis内存分为数据库内存、缓冲内存和redis自身内存

①数据库内存

数据库内存指用于存放用户数据所占用的内存,会算在used_memory里面,redis有16个数据库,默认只是用db0,集群模式下不支持多数据库。

②缓冲内存

缓冲内存指进行I/O操作设置缓冲区占用的内存,大小取决于I/O数量,会算在used_memory中,只要分为AOF缓冲区、复制积压缓冲区和客户端缓冲区

AOF缓冲区

进行AOF持久化时用到的缓冲区

AOF:是redis持久化的手段,每次处理完请求后,都会将命令写进AOF文件末端;

AOF的同步策略有:

每秒同步:每秒异步同步,效率高,但在发生宕机时那一秒的数据会丢失;

每次修改:每次发生数据修改时同步;

不同步:效率最低,不同步数据。

复制积压缓冲区(backlog)

进行节点复制时用到的缓冲区,是保存在主节点的一个固定大小的队列,默认大小为1m,当主节点有连接的slave时,主节点回应写请求,不仅会将命令发给slave,并且会写进自己的backlog。

客户端缓冲区

客户端和服务端连接并进行读写时用到的缓冲区,分为普通客户端缓冲区,从客户端缓冲区和订阅客户端缓冲区

普通客户端缓冲区:用在普通连接上,可以设置最大链接数来控制输出缓冲区大小;

从客户端缓冲区:用在主节点和从节点命令复制上,可以直接设置输出缓冲区大小;

订阅客户端缓冲区:用在订阅模式上,也可以直接设置输出缓冲区大小。

③redis自身内存

存放redis自身运行所需代码,数据占用的内存,不会算在used_memory里面。

2、内存释放

1、过期删除机制

redis的keys的过期时间未Unix时间戳,不用时,有效时间也是一直在流逝;

淘汰过期key的方式:主动和被动

①、被动 - 惰性删除:用户访问时,key会被发现并主动的过期。

②、主动 :

​ (1)定时删除:对内存是最友好的,通过使用定时器,定时删除策略对key进行检查,并且将过期的key删除;

​ (2)定期删除:定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对cpu时间的影响(最好的选择)。

检测策略:测试随机的20个key进行相关过期检测,删除已经过期的key,如果多于25%的key过期,则重复进行随机检测。

2、内存淘汰机制

在回收过期key后,内存依旧不够用的情况下,需要队内存数据进行淘汰。

①LRU算法

原理:在redisObject数据类型中,定义了lru字段,用来记录数据最后最近被访问的时间,redis的LRU并不进行维护队列,只是根据配置的策略要么从所有的key中随机选择N个(N可以配置)要么从所有设置了过期时间的key中选出N个件,然后从N个键中选出最久没有使用的key进行淘汰,后续如果要继续淘汰,在挑选N个数据集合时,只挑选比淘汰的数据的lru的数值要小的数据去进行LRU算法淘汰。

缺点:会出现频率更低但最近被访问的数据被保留,而频率更高,但最近未被访问的数据被清除的现象。

②LFU算法

原理:在LRU的基础上,将lru字段划分为俩部分,前16位代表时钟,后8位代表计数器,表示当前key对象的访问频率,8位只能表示255,但redis没有采用线性上升的方式,而是通过一个复杂的公式调整数据的递增速度,最后,redis会队内部时钟最小的key进行淘汰。

规则:如果一个key经过几分钟没有命中,那么后8位的值是需要递减几分钟,具体递减几分钟,根据衰减因子lfu-decay-time来控制,具体时高16位时种差*递增因子。

如果新分配的key经过几分钟没有命中,那么很有可能在内存不足的时候会直接给淘汰掉了,所以默认情况下新分配的key的后8位计数的值位5(可修改配置),防止因为访问频率过低被直接淘汰

③随机算法
④激进删除
⑤直接报错

不设置过期,内存不足时,直接报错,重启解决

3、碎片问题

内存碎片不计入used_memory,redis没有内存碎片处理机制,通常都是直接重启。

(2)线程模型

redis整个应用是多线程的,因为处理命令的执行,还需fork子线程进行持久化和复制等,但就命令的执行而言,redis是单线程的。

redis 单线程执行命令:

redis内部使用文件事件处理器 file event handler,它是单线程的,所以redis才叫做单线程模型。它采用IO多路复用机制,同时监听多个socket,将产生事件的socket压入内存队列中,事件分派根据socket上的事件类型来选择对应的事件处理器进行处理。

文件事件处理器包括:多个socket、io多路复用程序、文件事件分派器、事件处理器(包括连接应答处理器,命令请求处理器、命令回复处理器)

如下图:

redis单线程模型的效率快的原因

1、所有操作都是内存操作;

2、核心是基于非阻塞的IO多路复用机制;

3、单线程反而避免了多线程的频繁上下文切换问题

五、高可用

1、模式

redis有主从模式,哨兵模式和集群模式

(1)主从模式

主从模式一般是一个主节点和俩个从节点,从节点可以用作读写分离和主机宕机替代

策略: 主从刚连接的时候,进行全量同步,全量同步结束后,进行增量同步;如果有需要,从节点slave任何时候都可以发起全量同步,redis的策略是,无论如何,首先尝试进行增量同步,如果不成功,要求从机济宁全量同步。

(2)哨兵模式

哨兵模式是一种特殊的模式,首先redis提供了哨兵的命令,哨兵是一个独立的进程。原理是哨兵通过发送命令,等待redis服务器响应,从而监控运行的多个redis实例;

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

故障切换的过程:当主机宕机时,哨兵1先检测到,系统并不会马上进行切换,仅仅是哨兵1主观的认为主服务器不可用,这个现象称为主观下线。当后面其它哨兵也监测到主机宕机不可用,并且哨兵的数量达到一定的值时,哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作(切换主机),这个过程称为客观下线。

(3)集群模式

多态从节点,组成集群,每台从节点都存储不同的数据内容。

redis集群没有使用一致性hash,而是引入了哈希槽的概念,redis集群有16383个哈希槽,每个key通过CRC16校验后,对16384取模来决定放置那个槽,集群的每一个节点负责一部分的hash槽。

哈希槽的优点:容易添加或删除节点。如,当需要添加新的节点时,从其他已存在的节点中获得部分槽转移到新加入的节点即可;当需要删除某个节点时,只需要将被删除节点的槽转移到其他节点中。在槽的转移过程中并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽数量都不会造成集群不可用的状态。

2、持久化

(1)RDB

RDB即快照模式,它是redis默认的数据持久化方式,它会将数据库的快照保存在dump.rdb这个二进制文件中(快照:就是把内存数据以二进制文件的形式保存起来).

RDB实际上就是redis内部的一个定时器时间,它每隔一段固定时间就去检查当前数据发生改变的次数和改变的频率,看它们是否满足配置文件中规定的持久化触发条件,当满足条件时,redis会通过操作系统调用fork()来创建一个子进程,该子进程与父进程享有相同的地址空间。

优点:①调用fork()方法创建子线程完成持久化过程,不会阻塞主线程工作;②体积小,复制和重建快。

局限:生成快照需要时间,一旦在过程中系统出错将会导致丢失上次快照到当前时间的所有数据,损失很大。

(2)AOF

AOF持久化模式就是以追加日志的方式是不是

保存操作,来达到持久化数据。

优点:每次执行时间段,不容易丢失大量数据。

局限:①体积大;②复制和重建满,容易影响主线程

追加模式:①no-从不追加;②always-每执行一个命令追加一次;③ererysec-每秒追加一次。

(3) RDB与AOF混用

将数据以RDB的方式写进文件,再将后续的操作命令行以AOF的格式存进文件,既保证了redis的重启速度,有降低了数据丢失的风险。

六、缓存问题:雪崩、击穿、穿透

穿透:缓存不存在,数据库不存在,高并发,少量key

击穿:缓存不存在,数据库存在,高并发,少量key

雪崩:缓存不存在,数据库存在,高并发,大量key

解决方式:都可以使用限流的互斥锁,保障数据库的稳定;即只允许一个请求去访问数据库,然后更新缓存,已提供给其他请求使用。

posted @ 2022-04-12 11:26  彬哙  阅读(101)  评论(0)    收藏  举报