Redis深入分析

Scroll Down

title: Redis深入分析
author: Mood
tags:

  • NOSQL
    categories:
  • Redis
    date: 2019-03-05 21:48:00

基本数据类型

rdb
redis的这几种Type,字面意思都可以理解了分别代表了对应的几个redis数据结构(字符串 列表 集合 有序集 哈希表)已经对应的编码方式,其中
ZIPMAP,ZIPLIST分别为压缩Map,压缩列表。SKIPLIST为跳跃表。EMBSTR为动态字符串。

String字符串

Redis的存储都是Key-Value类型,不同的数据类型代表的是Value的值得存储类型,Redis的字符串是最基本的数据类型,使用的也是最广泛的一种,缓存用户session信息,如Spring-Seesion提供的把用户的信息使用JSON系列化之后存放到Redis,在读取用户信息反序列化直接拿到。redis的字符串是动态的字符串,其内存为预分配状态,如果说该当分配value的值超过了实际容量时,就会去扩容,其扩容的机制是超过多少扩容多少,最大为512M,

使用方式

最简单的要说SET,GET,也就是存取,这只是针对单个key的读写,如果要使用批量获取是可以使用MGET命令来读取,而高级的用法则就是给key设置过期时间,过期自动删除,setex 或者set + expire来处理,针对set还有 setnx,该命令则是如果对应的key已存在,那么则会创建不成功。还有一个使用点在于计数,范围在long之内递增或者递减。最大值为9223372036854775807。

内部构造

字符串有字节组成,一个字节等于8个bit,字符串就是无数个bit组合在一起,就是bitmap的数据结构。

List列表

这个list相当于LinkList是个链表结构,这就说明了插入和删除非常的快,但是随机访问的性能比较差,当list的最后一个元素被弹出的时候,这个list也会被回收回去。再去访问返回的是nil。

使用方式

针对list而言主要是他的二种使用模式队列和栈,队列就是使用的{(lpush,rpop||rpush lpop)先进先出},栈则是{(lpush,lpop||rpush ppop)先进后出}。

内部构造

其底层存储并不是简单的单链表构造,其数据结构被称为快速链表(quicklist),当list元素比较少的时候会使用一块连续内存存储(ziplist),

| ----ziplist------|pre   | ------ziplist----|
| NODE->NODE->NODE |<——>| NODE->NODE->NODE |
| -----------------|next  | -----------------|

当其中的元素变多的时候会在多个ziplist之间加入双向指针(pre|next)用来满足快速插入。

Hash

这个hash相当于Java的HashMap,无序以键值对存在,实现的结构也是hash数组+链表的结构,使用拉链法来解决hash冲突,hash和list一样,当最后一个元素被移除之后,整个数据key也会被删除。

使用方式

针对hash模式一般都会和字符串作比较,因为字符串而言是把序列化的数据存放,如果读取则需要全部读取,而hash则是可以把每个字段单独存放,这样的话,意味着资源占用情况就比较高,基本使用的命令是HSET HGET HMET HGEALL

内部构造

数据加链表的处理hash冲突的情况,第一个要面对的是在扩容的时候要去rehash,所以hashMap和扩容有关的是三个属性1 size 2 threshold 3 DEFAULT_LOAD_FACTOR(0.75),size 已使用hash数组的长度。threshold 是阈值 当size达到这个阈值的时候,就要去进行扩容。
负载因子越大,hash数组的使用率也就越高。而每个数组元素对应的链表也就越长,负载因子越小则会发生频繁的扩容还没怎么用呢,就要扩容了。0.75这个负载因子是一个时间和空间平衡值。就是说浪费1/4的空间。这个内部的链表不至于很长。hashMap这个扩容呢是整体扩容,而redis的Hash数据类型为了追求性能则是采用的渐进式的rehash,redis的hash的结构体中存储二份hash结构,一般只会使用第一块hash,当扩容的时候会把rehash的结果放入第二块中,当操作hash的指令的时候的时候会把旧的hash放入第二块并且移除第一块旧的,如果没有访问的话会有一个定时任务去吧这个迁移到第二块hash中,当数据量减少的时候换回缩容,查询的时候会查询二块hash结构的内容,然后返回值。

Set

这个hash相当于Java的hashSet,无序而且集合中存在形式是惟一的,当最后一个元素被移除之后,整个数据key也会被删除。

使用方式

介于其去重的能力,可以使用抽签,投票之类的设计场景中。因为一个用户只能投一次或者抽取一次。常用的指令就是SADD SMEMBER SPOP SCARD

内部构造

set的内部构造和hash一模一样,只不过set的每个value都是NULL。

ZSET

value有序而且不重复,内部实现为跳跃表,当最后一个元素被移除之后,整个数据key也会被删除。在设置的时候会为value设置一个评分用来排序,zset使用的场景如王者荣耀好友段位实时排行榜。

使用方式
添加 :ZADD {KET} {SCORE} {VALUE}
升序排序 :ZRANGE {KET} 0 -1
降序排序 :ZREVRANGE {KET} 0 -1 
总数 :ZCARD {KET} 
删除 :ZREM {KET} {VALUE} 
内部构造

跳跃列表skipList是一种典型的以空间换时间的设计方式,维护的多层索引映射,其排序根据每个节点所携带的score进行排序,所以插入的时候会根据该新插入的score和整个有序的跳表去做查询,如找到比某个位置小的,比他的前一个节点大,那么久将其插入该位置,变更对应的指针,由于这个跳表不是数组也不是一棵树所以无法使用二分查找,我们把二分查找是折半,目标值和中间值比较,可以看做是同一阶层的搜索。跳表呢则可以理解为不同阶层的搜索,其主旨都是在逐步减少查询范围和精确定位目标值。比如当有人问你你那里人啊,你会说银河系,虽然是废话,但是这是个事实当然你可以时候你是银河系->地球->中国->内蒙古->包头市->青山区->新手村->3组->02户。我们要知道你是哪个地方的就得如此一层一层的去找寻。为了满足这种条件就需要你绑定多个指向,具体到哪里人这个例子中就是米拥有多重身份,在不同层次你可以是地球人 中国人 包头市人等等,这多维护的东西在跳表中就引申为索引指针。其根据索引指针定位的过程就相当于问你老家在哪里。
跳跃表在插入的时候你可以了解所有的元素肯定在base层,也就说你首先是银河系的生命,其次根据跳跃表建立多层索引的策略,用50%的几率来选你是否可以晋升地球,接着再用25%的几率来判断你是否可以降生中国,依次类推一直随机到顶层(31层),当数据越多的时候,层次就越深,顶层的元素就会很多。这就是(概率均衡技术)

Redis持久化

关于redis作为内存数据库,只做为“缓存”服务,不持久数据,数据在服务终止后将消失,此模式下也将不存在“数据恢复”的手段,是一种安全性低/效率高/容易扩展的方式;当然在大型的系统中必须持久化存储,不然当数据服务器宕机之后,内存中数据将被flash掉,若此时该系统中亦有用户假设有订单未结算,那么数据将无法找回,尽管有redis的集群,但是单台机器的宕机,那么那台机器的数据也是无法恢复的,所以数据进行持久化是很有必要滴。如何进行持久化呢:把内存中的数据持久备份到磁盘文件,在服务重启后可以恢复。redis持久化有两种方式RDB和AOF方式

RDB工作原理

RDB :Redis DataBases方式
rdb

RDB简而言之,在不同的时间点上,打一个快照,将redis存储的数据刷到磁盘上。当重启的时候后, rdbLoad 函数就会被执行, 它读取 RDB 文件, 并将文件中的数据库数据载入到内存中。
RDB是快照方式的,具体的过程为会fork出一个子线程去去备份redis之前的数据,会形成一个临时文件,当子线程工作的时候,redis的master线程会挂起。拒绝这个时刻下的客户端请求。直到临时文件和前一个版本的dump.rdb文件比对合并之后。什么时候会触发redis执行RDB方式的持久化呢。和用户在redis.conf文件的配置有关。如果要进行大规模的数据恢复,而且数据精准性要求不高的话,rdb方式要不AOF有优势,他是保证了某一个时间段的数据完整,如果是还在内存中没有持久化的数据,那个断电重启恢复是恢复不了的。具体的RDB实现是执行了源代码rdb.c文件中的rdbSave系列方法。linux上如果日志中提示save db不成功报错的话,要把overcommit.memory设置为1。后来去百度了下,该设置实际是vm.overcommit.memory是linux内存的分配策略。在root权限下sysctl vm.overcommit_memory=1。即可

AOF工作原理

AOF:Append Only File方式
info
AOF则是换了一个角度去实现持久化,而是将redis在一段时间的写入指令都记录下来,在重启的时候,把写入指令的文件读入,然后重新在执行一边写入指令。进而实现数据恢复。在redis.conf中配置appendonly no即可打开AOF方式持久化。默认每秒去同步一次。追加的方式呢必然导致了这个AOF文件是越来越大的,所以丫的还原速度也是特别慢的。相对于大文件而言。redis针对AOF方式的还有重写机制,其实就是吧很多条的写入指令的合并为一条set操作。是将“操作 + 数据”以格式化指令的方式追加到操作日志文件的尾部,在append操作返回后(已经写入到文件或者即将写入),才进行实际的数据变更,“日志文件”保存了历史所有的操作过程;当server需要数据恢复时,可以直接replay此日志文件,即可还原所有的操作过程,当然如果误删除了数据,那么删除操作文件的最后一行重启就可以了,不过你得速度点,应为AOF机制下有一种重写机制。他会在数据超过如100条命令的时候重写整合为一条新的set进行插入。实现机制是在主redis进程中fork出一个后台进程进程大小是和主进程一样大,是根据百分比来执行重写的,所以当AOF文件特别大的时候,那么他的子线程也特别吃cpu,这就可能导致redis挂掉。

Redis的事务

MULTI:标记开始
EXEC:执行,只能执行被标记的命令。
DISCARD:取消
WATCH:用来监视一些key如果在事务执行前被改变就取消事务啥的执行。CHECK-AND-SET类似jvm的CAS监控如果当前key被别的客户端修改了的话,就事务执行失败。

Redis的淘汰策略

Redis的淘汰策略是在配置中进行的就是MAXMEMORY POLICY配置如下:如果达到内存限制了,Redis如何选择删除key。默认是不删除。直接返回一个写错误
rdb

 volatile-lru -> 根据LRU算法删除带有过期时间的key。
 allkeys-lru -> 根据LRU算法删除任何key。
 volatile-random -> 根据过期设置来随机删除key, 具备过期时间的key。 
 allkeys->random -> 无差别随机删, 任何一个key。 
 volatile-ttl -> 根据最近过期时间来删除(辅以TTL), 这是对于有过期时间的key 
 noeviction -> 谁也不删,直接在写操作时返回错误。

说到要去删除Key就必须要接触两个C语言的结构体redisServer,redisObject 都存在于redis.c中。

redisServer是贯穿于整个redisserver启动之后相当于全局变量一样,包含RedisServer的PID,配置文件的各个配置,
rdb

rdb
redis是由C语言写的没有高级语言的类,没有对象这个概念,redisObject为了解决这个问题,Redis创建了自己的类型redisObjec结构体,如上图所示,由type和encoding两个来确定是redis的哪一种数据类型(字符串 列表 集合 有序集 哈希表).当然C没有java的垃圾回收机制,如何释放不用的内存redis使用的是利用引用计数refcount,每个对象结构都有一个ref引用计数,当计数递减为0时,这个redisObject结构以及它所引用的底层数据结构的内存都会被释放。最后一个ptr是句柄指针,指向实际保存的数据结构,lru则是该redisObject与server.lruclock的时间差值,应为redis是内存数据库,在配置文件中设置了最大MAXMEMORY,当配置了数据淘汰策略为-lru算法时就要使用该值了。当客户端执行一个写指令,创建一个(Key-Value)时候调用了调用*createObject方法,该方法位于object.c文件中。代码如下,在创建时候指定type并且拿到于redisServer全局变量中lruclock的差值。用作之后的释放内存。
rdb
当一个redis对象被创建之后,如果一直没有更新他的lru值就确认了,一旦有更新操作该lru值会更新,但是全局变量的server.lruclock确实一直在更新的。redis有一个类似于linuxcron的定时任务在定时刷新,就是serverCron方法中的server.lruclock = getLRUClock();getLRUClock获取当前系统的毫秒数。,好先了解了这些概念,以及redis如何来定义最近使用最少的思路之后。我们接下来就去寻觅一下当内存数据库redis存储的数据达到了MAXMEMORY之后如何处理。其实每一个redis命令执行是都会去执行下processCommand方法
rdb
去判断最大内存即if块中的freeMemoryIfNeeded方法
rdb
上图的代码表明redis在freeMemoryIfNeeded中是先去计算出了当前一用的内存,用使用的内存减去丛机的缓存。然后要去判断下是否是AOF的持久化方式,是的话还要前去aof的缓存区,然后判断下已用内存是否还没超过设定MAXMEMORY,没有的话进行一个短路操作,返回一个成功表明还有足够的内存允许存储,如果超过了配置文件设定的最大内存的话要去判断设置的淘汰策略。默认是 谁也不删,直接在写操作时返回错误。然后用已使用内存减去最大内存去和0比较。如果大于那么就去循环所有的redis数据节点,去执行数据淘汰。

redis高可用

主从模式

主要是通过master server持久化的rdb文件实现的。master server 先dump出内存快照文件,然后将rdb文件传给slave server,slave server 根据rdb文件重建内存表。由slave发送同步请求到master,然后master在rdb进程空闲是fork一个出来进行快照,然后保存为dump.rdb发送给slave。
前提都需要讲redis设置为守护进程模式。Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程,daemonize yes如果是在同一一台机器上搭建的主从的话,就要配置不同的端口(默认端口6379),设置不同的pid文件,不同的日志打印路径等。

在slave的redis.conf去设置master的ip和端口,我以本机为例:slaveof 127.0.0.1 6666

如果你的master设置了密码,那么你slave上要去配置master的密码,我的没密码所以不用打开注释。

然后就是用指定我们写好的配置文件分别去启动Master和slave 。我是进入该目录去执行的,当然绝对路径也是没有毛病的。
样例
/opt/mood/master/redis-server ./redis.conf

/opt/mood/slave/redis-server ./redis.conf

主从配置完毕之后,当主挂掉我们可以连接slave。我们可以手动执行命令,redis-cli -p 6666 slaveof 127.0.0.1 6379, 只是一种方式,实际开发中我们不可能去手动执行。这要交给哨兵模式去执行。

集群模式