Redis 底层数据结构
Redis 数据库里面的每个键值对(key-value) 都是由对象(object)组成的:
数据库键总是一个字符串对象(string object);
数据库的值则可以是字符串对象、列表对象(list)、哈希对象(hash)、集合对象(set)、有序集合(sort set)对象这五种对象中的其中一种。
这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
Redis 底层数据结构有以下数据类型:
简单动态字符串、链表、字典、跳跃表、整数集合、 压缩列表、对象
简单动态字符串(simple dynamic string)SDS,除了用来保存字符串以外,SDS还被用作缓冲区(buffer)AOF模块中的AOF缓冲区。
三个重要属性:free len buf
当我们需要对一个SDS 进行修改的时候,redis 会在执行拼接操作之前,预先检查给定SDS 空间是否足够,如果不够,会先拓展SDS 的空间,然后再执行拼接操作
链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。
链表在Redis 中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表作为列表键的底层实现。
双端:链表节点带有prev 和next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是O(N)
无环:表头节点的 prev 指针和表尾节点的next 都指向NULL,对立案表的访问时以NULL为截止
表头和表尾:因为链表带有head指针和tail 指针,程序获取链表头结点和尾节点的时间复杂度为O(1)
长度计数器:链表中存有记录链表长度的属性 len
多态:链表节点使用 void* 指针来保存节点值,并且可以通过list 结构的dup 、 free、 match三个属性为节点值设置类型特定函数
字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种用于保存键值对的抽象数据结构。
在字典中,一个键(key)可以和一个值(value)进行关联,字典中的每个键都是独一无二的。
在插入一条新的数据时,会进行哈希值的计算,如果出现了hash值相同的情况,Redis 中采用了连地址法(separate chaining)来解决键冲突。每个哈希表节点都有一个next 指针,多个哈希表节点可以使用next 构成一个单向链表,被分配到同一个索引上的多个节点可以使用这个单向链表连接起来解决hash值冲突的问题。
随着对哈希表的不断操作,哈希表保存的键值对会逐渐的发生改变,为了让哈希表的负载因子维持在一个合理的范围之内,我们需要对哈希表的大小进行相应的扩展或者压缩,这时候,我们可以通过 rehash(重新散列)操作来完成。
渐进式rehash 的详细步骤:
1、为ht[1] 分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2、在几点钟维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash 开始
3、在rehash 进行期间,每次对字典执行CRUD操作时,程序除了执行指定的操作以外,还会将ht[0]中的数据rehash 到ht[1]表中,并且将rehashidx加一
4、当ht[0]中所有数据转移到ht[1]中时,将rehashidx 设置成-1,表示rehash 结束
采用渐进式rehash 的好处在于它采取分而治之的方式,避免了集中式rehash 带来的庞大计算量。
持久化:RDB 和 AOF
http://www.ymq.io/2018/03/24/redis/
RDB(默认):快照方式,允许你每隔一段时间对内存数据做一次快照然后存储到硬盘中。
RDB 可以通过在配置文件中配置时间或者改动键的个数来定义快照条件,redis.conf
save9001#15分钟之内至少有一个建被更改则进行快照
save30010#5分钟之内至少有10个建被更改则进行快照
save6010000#1分钟之内至少有1000个建被更改则进行快照
RDB快照过程:
1、Redis使用fork函数复制一份当前的父进程作为子进程
2、父进程继续处理用户的请求,子进程开始把内存中的数据持久化到磁盘上
3、当子进程把内存中的数据写入临时文件完成之后,会把该临时文件替换掉旧的RDB文件
AOF:
通过将发送到服务器的写操作命令记录下来,形成AOF文件,文件默认名称是appendonly.aof
原理:直接把用户插入到服务器的命令追加到结尾,那么文件会越来越大,一些重复的写命令也会越来越多,这时利用BGREWRITEAOF 命令来重写AOF
AOF在同步内存数据到磁盘上时,并不是马上把文件写入到磁盘中,而是先把文件缓存到系统,然后每隔30秒将文件写入到磁盘中
同步策略:
appendfsync always#每次都同步,保证数据不会丢失,但会慢
appendfsync everysec#每秒同步,系统默认同步策略
appendfsyncno#不主动同步,由操作系统决定,快,但数据容易丢失
AOF同步过程:
1、Redis执行fork(),同时拥有父进程和子进程
2、子进程开始将新AOF文件的内容写入到临时文件
3、对于所有新执行的写入命令,父进程一边将它们积累到一个内存缓存中,一边将这些改动追加到现有AOF文件的末尾
4、当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新AOF文件的末尾
二者区别:
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
比较:
redis还可以同时使用AOF持久化和RDB持久化,在这种情况下,当redis重启时,它会优先使用AOF文件来还原数据集,因为AOF文件保存的数据集通常比RDB文件所保存的数据集更加完整
https://juejin.im/post/5ab5f08e518825557f00dfac
Redis常见性能问题和解决方案:
1、Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件
(Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照;AOF文件过大会影响Master重启的恢复速度)
2、如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
3、为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
4、尽量避免在压力很大的主库上增加从库
5、主从复制不要用图状结构,用单向链表结构更为稳定,即Master<-Slave1<-Slave2<-Slave3
这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立即启用Slave1做Master,其他不变。
Redis的回收策略
volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru:从数据集中挑选最近最少使用的数据淘汰
allkeys-random:从数据集中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据
Redis是单进程单线程的,利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销
Redis 通过MULTI、DISCARD、EXEC 和WATCH 四个命令来实现事务功能。
事务提供了一种“将多个命令打包,然后一次性、按顺序地执行”的机制,并且事务在执行的期间不会主动中断--服务器在执行完事务中的所有命令之后,才会继续处理其他客户端的其他命令。
一个事务从开始到执行会经历以下三个阶段:
1、开始事务 2、命令入队 3、执行事务
MULTI命令的执行标记着事务的开始
Redis 中有个设置时间过期的功能,即通过setex或者expire实现,目前redis没有提供hsetex()这样的方法,redis中过期时间只针对顶级key类型,对于hash类型是不支持的。
一、有效时间设置:
redis对存储值的过期处理实际上是针对该值的键key处理的,即时间的设置也是设置key的有效时间。Expires字典保存了所有键的过期时间,Expires也被称为过期字段。
四种处理策略:
1、Expire将key的生产时间设置为ttl秒
2、Pexpire将key的生成时间设置为ttl毫秒
3、Expireat将key的过期时间设置为timestamp所代表的秒数的时间戳
4、Pexpireat将key的过期时间设置为timestamp所代表的毫秒数的时间戳
二、过期处理
过期键的处理就是把过期键删除,这里的操作主要是针对过期字段处理的。
Redis中有三种处理策略:定时删除、惰性删除和定期删除
定时删除:在设置键的过期时间的时候创建一个定时器,当过期时间到的时候立马执行删除操作。不过这种处理方式是即时的,不管这个时间内有多少过期键,不管服务器现在的运行状况,都会立马执行,所以对CPU不是很友好。
惰性删除:惰性删除策略不会在键过期的时候立马删除,而是当外部指令获取这个键的时候才会主动删除。处理过程为:接收get执行、判断是否过期(这里按过期判断)、执行删除操作、返回nil(空)。
定期删除:定期删除是设置一个时间间隔,每个时间段都会检测是否有过期键,如果有执行删除操作。这个概念应该很好理解。通过TTL命令用于获取键到期的剩余时间(秒)。 判断大于0还是其他,这样可以实现定期删除。
三、主从服务器删除过期键处理
Redis采用的过期策略
懒汉式删除+定期删除
懒汉式删除流程:
在进行get或setnx等操作时,先检查key是否过期;
若过期,删除key,然后执行相应操作;
若没过期,直接执行相应操作;
定期删除流程(简单而言,对指定个数个库的每一个库随机删除小于等于指定个数个过期key):
遍历每个数据库(就是redis.conf中配置的"database"数量,默认为16)
检查当前库中的指定个数个key(默认是每个库检查20个key,注意相当于该循环执行20次,循环体是下边的描述)
如果当前库中没有一个key设置了过期时间,直接执行下一个库的遍历
随机获取一个设置了过期时间的key,检查该key是否过期,如果过期,删除key
判断定期删除操作是否已经达到指定时长,若已经达到,直接退出定期删除。
Redis对于过期键有三种清除策略:
被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key
主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key
当前已用内存超过maxmemory限定时,触发主动清理策略
RDB持久化、AOF持久化和复制功能
RDB:
1、主服务器模式运行在载入RDB文件时,程序会检查文件中的键,只会加载未过期的,过期的会被忽略,所以RDB模式下过期键不会对主服务器产生影响。
2、从服务器运行载入RDB文件时,会载入所有键,包括过期和未过期。当主服务器进行数据同步的时候,从服务器的数据会被清空,所以RDB文件的过期键一般不会对从服务器产生影响。
AOF:
AOF文件不会受过期键的影响。如果有过期键未被删除,会执行以下动作:
1、从数据库删除被访问的过期键
2、追加一条DEL命令到AOF文件
3、向执行请求的客户端恢复nil
复制:
1、主服务器删除过期键之后,向从服务器发送一条DEL指令,告知删除该过期键;
2、从服务器接收到get指令的时候不会对过期键进行处理,只会当做未过期键一样返回;
3、从服务器只有接到主服务器发送的DEL指定后才会删除过期键
Redis集群,高可用,原理
redis主从复制
redis主从配置比较简单,基本就是在从节点配置文件加上:slaveof 192.168.33.130 6379
redis复制过程如下:
1、slave server启动连接到master server之后,salve server主动发送SYNC命令给master server
2、master server接受SYNC命令之后,判断,是否有正在进行内存快照的子进程,如果有,则等待其结束,否则,fork一个子进程,子进程把内存数据保存为文件,并发送给slave server
3、master server子进程做数据快照时,父进程可以继续接收client端请求写数据,此时,父进程把新写入的数据放到待发送缓存队列中
4、slave server 接收内存快照文件之后,清空内存数据,根据接收的快照文件,重建内存表数据结构
5、master server把快照文件发送完毕之后,发送缓存队列中保存的子进程快照期间改变的数据给slave server,slave server做相同处理,保存数据一致性
6、master server 后续接收的数据,都会通过步骤1建立的连接,把数据发送到slave server
需要注意:slave server如果因为网络或其他原因断与master server的连接,当slave server重新连接时,需要重新获取master server的内存快照文件,slave server的数据会自动全部清空,然后再重新建立内存表,这样会让slave server 启动恢复服务比较慢,同时也给master server带来较大压力,可以看出redis的复制没有增量复制的概念,这是redis主从复制的一个主要弊端,在实际环境中,尽量规避中途增加从库
redis2.8之前不支持增量,到2.8之后就支持增量了!
Redis缓存分片
https://blog.csdn.net/houjixin/article/details/38304081
(1)在客户端做分片;这种方式在客户端确定要连接的redis实例,然后直接访问相应的redis实例;
(2)在代理中做分片;这种方式中,客户端并不直接访问redis实例,它也不知道自己要访问的具体是哪个redis实例,而是由代理转发请求和结果;其工作过程为:客户端先将请求发送给代理,代理通过分片算法确定要访问的是哪个redis实例,然后将请求发送给相应的redis实例,redis实例将结果返回给代理,代理最后将结果返回给客户端。
(3)在redis服务器端做分片;这种方式被称为“查询路由”,在这种方式中客户端随机选择一个redis实例发送请求,如果所请求的内容不再当前redis实例中它会负责将请求转交给正确的redis实例,也有的实现中,redis实例不会转发请求,而是将正确redis的信息发给客户端,由客户端再去向正确的redis实例发送请求。
Redis高并发问题
redis是一种单线程机制的nosql数据库,基于key-value,数据可持久化落盘。由于单线程所以redis本身并没有锁的概念,多个客户端连接并不存在竞争关系,但是利用jedis等客户端对redis进行并发访问时会出现问题。发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。
同时,单线程的天性决定,高并发对同一个键的操作会排队处理,如果并发量很大,可能造成后来的请求超时。
在远程访问redis的时候,因为网络等原因造成高并发访问延迟返回的问题。
解决方法:
在客户端将连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized。
服务器角度,利用setnx变向实现锁机制。
Redis 缓存雪崩和缓存穿透
缓存雪崩
缓存雪崩是由于原有缓存失效(过期),新缓存未到期间。所有请求都去查询数据库,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。
1. 碰到这种情况,一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
2. 加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法。
还有一个解决办法解决方案是:给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
public object GetProductListNew()
{
const int cacheTime = 30;
const string cacheKey = "product_list";
//缓存标记。
const string cacheSign = cacheKey + "_sign";
var sign = CacheHelper.Get(cacheSign);
//获取缓存值
var cacheValue = CacheHelper.Get(cacheKey);
if (sign != null)
{
return cacheValue; //未过期,直接返回。
}
else
{
CacheHelper.Add(cacheSign, "1", cacheTime);
ThreadPool.QueueUserWorkItem((arg) =>
{
cacheValue = GetProductListFromDB(); //这里一般是 sql查询数据。
CacheHelper.Add(cacheKey, cacheValue, cacheTime*2); //日期设缓存时间的2倍,用于脏读。
});
return cacheValue;
}
}
缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存。
缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。
缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
解决的办法就是:如果查询数据库也为空,直接设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。也可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放行给后面的正常缓存处理逻辑。
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样避免,用户请求的时候,再去加载相关的数据。
解决思路:
1,直接写个缓存刷新页面,上线时手工操作下。
2,数据量不大,可以在WEB系统启动的时候加载。
3,定时刷新缓存,
缓存更新
缓存淘汰的策略有两种:
(1) 定时去清理过期的缓存。
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。
如何使用Redis来实现分布式锁
http://mzorro.me/2017/10/25/redis-distributed-lock/
1、互斥性。在任意时刻,只有一个客户端能持有锁。
2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3、具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
4、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
使用命令:SETNX
如果当前中没有值,则将其设置为并返回1,否则返回0。
Redis的并发竞争问题如何解决
watch 监听,multi 执行,保证事务,
redis提供了一个事务操作的机制,MULTI 命令用于开启一个事务,它总是返回 OK 。
MULTI执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当EXEC命令被调用时, 所有队列中的命令才会被执行。
10个常见的redis面试刁难问题:
1、redis有哪些数据结构
字符串String,字典Hash,列表List,集合Set,有序集合SortedSet
高级用法:GEO、Pub/Sub、HyperLog
另外Redis Module,像BloomFilter、RedisSearch、Redis-ML
2、使用过redis分布锁么,它是怎么回事
先拿setNx来争抢锁,抢到以后,再用expire给锁加一个过期时间防止锁忘记了释放
如果在setnx之后执行expire发生crash或者重启,这样锁就无法释放了,利用set方法设置参数,可以将setNx和expire合成一条指令
3、假如redis里面有1亿个key,其中10w个key是固定已知的前缀开头,如何将他们找出来
使用keys命令扫描指定模式的列表
如果正在给线上业务提供服务,keys指令会有什么问题
redis是单线程的,keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完成,服务才会恢复。这个时候使用scan命令,可以无阻塞的扫描指定的列表,scan会造成一定概率的重复,需要在客户端做一次去重,但整体时间比keys要长。
4、如果有大量key需要设置同一过期时间,需要注意什么
如果大量的key的过期时间过于集中,到过期的那个时间点,redis可能出现卡顿,一般需要在过期时间上加一个随机值,使得过期时间分散一些。
5、redis如何做持久化的
bgsave做镜像全量持久化,aof做增量持久化。因为bgsave耗时较长,不够实时,在停顿的时候会导致大量数据丢失,所以需要aof配合。redis重启时,优先使用aof恢复,然后再用rdb恢复。
如果aof文件过大怎么办?redis默认会定期做aof重写,压缩aof文件大小。redis4.0之后有了混合持久化,rdb全量和aof增量融合处理,既保证恢复的速率又兼顾了数据的安全性。
如果突然掉电:取决于aof的fsync策略,如果是每次写,就不会丢数据,但是在高并发下每次sync不现实,一般采用每秒写。
6、Pipeline有什么好处,为什么要用Pipeline
可以将多次IO返回的时间缩为1次,
7、redis的同步机制了解么
redis可以使用主从同步、从从同步,第一次同步时,主节点做一次bgsave,并同时将后续操作记录到内存,待完成后将rdb文件同步到复制节点,复制节点接收rdb文件并镜像到内存,加载完成后,在通知主节点将期间同步的操作命令同步到复制节点完成同步。
8、是否使用过redis集群,集群的原理
redis sentinal找眼于高可用,在master宕机时,会自动加slave升级成master
redis cluser 找眼于扩展性,在单个redis内存不够时,使用cluser进行分片存储。