Redis深入学习
一、数据结构类型
- string ,set get ex
- list :lpush lpop 用于消息队列
- set :sadd、smenbers 一键多值,用于标签
- zset : zadd user:rank score menber
- zadd user:rank 1 james 21 kate 32 jack 46 frank 258 tom;` james获得1票,kate21票…
- zrange user:rank james ---- 找到james的票数 --dict
- zrange user:rank 0 10 — 找到前10的人的名字
- zrank user:rank james 返回它的排名
- zscore user:rank james 返回它的分数值
- zrange user:rank 0 10 with scores —就是把前10的人票数和名字都列出来
- hash:hset user:001 name huanghuo age 1 / hgetall user:001 / hget user:001 name
二、过期策略
最近最少使用原则LRU.、 深入
随机时间 – 深入
设置的过期时间
永不过期
三、持久化
-
RDB
他会fork一个进程,在指定时间内把内存的数据备份到dump.gdb文件中,再次启动的时候就会把读取日志到内存中。缺点就是无法实时化,进程占用内存比较大。主要用于大规模的数据恢复
-
AOF,二者可以同时存在,但是是先去读取AOF文件的
以日志的形式记录每个写操作。
缺点就是文件会很大。优点就是数据完整性高,当发生宕机的时候,可以用它来恢复数据就和mysql的redolog一个作用
四、事务
multi 开启事务、exec 执行事务、discard 回滚事务
redis是部分支持事务的,最后执行的时候,成功的就成功,失败的就失败
redis没有隔离性因为它是每个命令进入队列,最后一起执行的
五、主从复制(高可用)
slave of 本质是从库心跳线程不断去主库同步数据,通过RDB文件
-
分为两种:全量复制(只在第一次的时候),增量复制
-
先说两个参数:runid是每台机的id,用来标志他是否是第一次请求同步数据。offset:是主库和从库都有的参数,比如主库发送了N个字节的数据过来,那么主库和从库的offset就是N,在增量同步的时候会使用到。
-
过程:
-
这一切都是刚刚连上主库,发送slaveof的初始化操作
**一种总结:**从库发送slaveof给主库,判断自己是否是有runid,如果没有那么就是第一次同步,会发送PSYNC ? -1指令,主库会把快照发给从库,从库进行复制,并记录offset和runid。如果不是第一次复制(主库比较从库发过来的runid和自己的runid是否匹配),那么从库需要重新发送一个指令PSYNC RUNID OFFSET给主库,主库根据offset,发送快照给从库。
**补充总结:**配置好slave后,slave与master建立连接,然后发送sync命令. 无论是第一次连接还是重新连接,master都会启动一个后台进程,将数据库快照保存到文件中,同时master主进程会开始收集新的写命令并缓存. 后台进程完成写文件后,master就发送文件给slave,slave将文件保存到硬盘上,再加载到内存中, 接着master就会把缓存的命令转发给slave,后续master将收到的写命令发送给slave. 如果master同时收到多个slave发来的同步连接命令,master只会启动一个进程来写数据库镜像, 然后发送给所有的slave
**再次总结:**可以总结为下面三个过程:
- 同步快照阶段:Master创建并发送快照给Slave,Slave再入快照并解析。Master同时将此阶段产生的新的命令写入到积压缓冲区中。----区分第几次
- 同步写缓冲阶段:Master向Slave同步存储在缓冲区的写操作命令。—主库在想从库发送快照的时候,发生的写操作,就进入缓冲区。
- 同步增量阶段:Master向SLave同步写操作命令。
-
正常每一次:
主库有了写操作,那么就会主动推送给所有从库。
-
六、哨兵模式(高可用,和主从复制是一伙的)
哨兵有多个,一般就是几个redis几个哨兵,他监控所有的redis服务器(在sentinel.conf中配置),每次ping一下各个服务器,如果超过半数哨兵都判断不ok,那么就投票出来换一个redis’做主库。
七、redis的集群
根据分区规则,比如哈希算法,我们把数据放到不同的redis服务器中。
具体实现cluster-enabled : true \ cluster-config-file
多个master,然后每个master又有自己的slave。
八、Zset实现原理
ZSET(SORTED SET)在redis中的编码可以是ziplist或者是skiplist
-
跳表的原理skiplist
参考视频:https://www.bilibili.com/video/BV1LJ411t7Ai 和https://www.bilibili.com/video/BV18J411s7eN 两个非常好的skiplist原理讲解,这方面视频很少。
skiplist的java实现:https://blog.csdn.net/bohu83/article/details/83654524
public Integer insert(int key, int value) { SkipListEntry p, q; int i = 0; // 查找适合插入的位子 p = findEntry(key); // 如果跳跃表中存在含有key值的节点,则进行value的修改操作即可完成 if(p.key ==key) { Integer oldValue = p.value; p.value = value; return oldValue; } // 如果跳跃表中不存在含有key值的节点,则进行新增操作 q = new SkipListEntry(key, value); q.left = p; q.right = p.right; p.right.left = q; p.right = q; //本层操作完毕,看更高层操作 //抛硬币随机决定是否上层插入 while ( r.nextDouble() < 0.5 /* Coin toss */ ) { if ( i >= h ) // We reached the top level !!! { //Create a new empty TOP layer addEmptyLevel(); } /* ------------------------------------ Find first element with an UP-link ------------------------------------ */ while ( p.up == null ) { p = p.left; } /* -------------------------------- Make p point to this UP element -------------------------------- */ p = p.up; /* --------------------------------------------------- Add one more (k,*) to the column Schema for making the linkage: p <--> e(k,*) <--> p.right ^ | v q ---------------------------------------------------- */ SkipListEntry e; // 这里需要注意的是除底层节点之外的节点对象是不需要value值的 e = new SkipListEntry(key, null); /* --------------------------------------- Initialize links of e --------------------------------------- */ e.left = p; e.right = p.right; e.down = q; /* --------------------------------------- Change the neighboring links.. --------------------------------------- */ p.right.left = e; p.right = e; q.up = e; //把q执行新插入的节点: q = e; // level增加 i = i + 1; } n = n+1; //更新链表长度 return null;
redis的skiplist参考文章:https://blog.csdn.net/weixin_38008100/article/details/94629753
跳表跳表的查找插入都是logN复杂度。
-
压缩表ziplist
压缩表是用字节组成双向链表,在内存中一起占用一块区域.下图是一个真实的结构
解释- 这个ziplist一共包含33个字节。字节编号从byte[0]到byte[32]。图中每个字节的值使用16进制表示。
- 头4个字节(0x21000000)是按小端(little endian)模式存储的字段。什么是小端呢?就是指数据的低字节保存在内存的低地址中(参见维基百科词条Endianness)。因此,这里的值应该解析成0x00000021,用十进制表示正好就是33。
- 接下来4个字节(byte[4…7])是,用小端存储模式来解释,它的值是0x0000001D(值为29),表示最后一个数据项在byte[29]的位置(那个数据项为0x05FE14)。
- 再接下来2个字节(byte[8…9]),值为0x0004,表示这个ziplist里一共存有4项数据。
- 接下来6个字节(byte[10…15])是第1个数据项。其中,prevrawlen=0,因为它前面没有数据项;len=4,相当于前面定义的9种情况中的第1种,表示后面4个字节按字符串存储数据,数据的值为”name”。
- 接下来8个字节(byte[16…23])是第2个数据项,与前面数据项存储格式类似,存储1个字符串”tielei”。
- 接下来5个字节(byte[24…28])是第3个数据项,与前面数据项存储格式类似,存储1个字符串”age”。
- 接下来3个字节(byte[29…31])是最后一个数据项,它的格式与前面的数据项存储格式不太一样。其中,第1个字节prevrawlen=5,表示前一个数据项占用5个字节;第2个字节=FE,相当于前面定义的9种情况中的第8种,所以后面还有1个字节用来表示真正的数据,并且以整数表示。它的值是20(0x14)。
- 最后1个字节(byte[32])表示,是固定的值255(0xFF)。
hash结构如果数据少,那么也是采用的ziplist结构.下图的示意图其实是来自以下命令:当数据大的时候就会变成dict结构
hset user:001 name tielei hset user:001 age 20
一些问题:
- 怎么实现双向? 我们使用遍历的方法找到指定位置,然后因为每个压缩表节点存放了前一个节点的长度,那么我们就能够往前找元素.
- 怎么实现从头或者从尾巴插入数据?ziplist存放了zltail,表示最后一个数据在什么位置上,那么我们就能在尾巴上插入数据.
- 为么数据元素变多或者变大,就不用ziplist了?因为他还是使用遍历的手段,但数据量大的时候,效率是不高的.另外他插入元素的时候,过程是进行内存拷贝的,但数据量大的时候,效率不高
- 优点是什么?占用内存小,是一整块内存,不会有内存碎片问题.
-
redis的zset(sort set)
-
zset的编码可以是ziplist或者是skiplist。同时满足以下条件时使用ziplist编码 :下面的具体限制数字都是可以在配置文件中更改的
- 元素数量小于128个
- 所有menber长度都小于64字节
-
redis的skiplistNode是存放key(score)和value(menber)的,但是实现上不是放在一起的,是同时包含一个字段dict和skiplist的,skiplist就是score的多层链表,dict就存放menber值,和skiplist的score有对应关系,这样查找score是O(logN),找对应的menber就只是O(1)。因此也可以有重复的score,就不会想hash表一样。
-
skipList的随机数是<0.25; 最高层限制是32层
-
有了上面的预备知识我们可以知道下面命令的过程:
- zscore 是查询menber的score是由dict来提供。但是如果是ziplist怎么办呢?他的key value是连在一起的,因此得到key的位置以后,下一个就是value
- zrange 是查询就是差skiplist
- zrank 是查询menber的排名,那么就是先去dict,然后再去skiplist
-
-
skiplist和平衡树、哈希表对比
- 哈希表不是有序排列的,只能找到key对应的值,无法做范围查询,而平衡树skiplist能够实现范围查询。
- 在做范围查找的时候,平衡树操作相对复杂,先查找出小值,之后中序遍历查找不超过范围最大值的节点。如果不是个完全平衡二叉树,这里的中序遍历并不简单。而跳表找到最小值以后,直接进行若干个遍历就ok找到范围最大值。
- 平衡树插入删除操作都可能引发子树的调整,逻辑调整,左旋右旋之类的。而跳表只要改指针
- 查找单个key,hash的时间复杂度是O(1),是最快的。
- 算法实现上,跳表简单得多。
范围查询,而平衡树skiplist能够实现范围查询。
-
在做范围查找的时候,平衡树操作相对复杂,先查找出小值,之后中序遍历查找不超过范围最大值的节点。如果不是个完全平衡二叉树,这里的中序遍历并不简单。而跳表找到最小值以后,直接进行若干个遍历就ok找到范围最大值。
-
平衡树插入删除操作都可能引发子树的调整,逻辑调整,左旋右旋之类的。而跳表只要改指针
-
查找单个key,hash的时间复杂度是O(1),是最快的。
-
算法实现上,跳表简单得多。
-
缓存失效
缓存穿透:key不在缓存中存在,也不存在于数据库,但是每次都会会去数据库找–这个有点扯,办法是给他缓存一个空值。
缓存击穿:缓存存的值过期了,这个时候有大量的并发过来,就会压垮DB。 用setnx锁的方式,来锁住访问请求数量。
缓存雪崩:假如缓存层宕机了,全部的key都没了,这个时候db的压力也就大了。解决办法:一个是集群,一个是在启动redis的时候限流,一个是数据预热,把可能的缓存先加载一次。