redis相关问题

redis是一个基于内存的高性能key-value数据库,将数据缓存在内存中,并周期性把当前的数据写入磁盘或者把修改操作写入追加的记录文件(与memcached区别),并且在此基础上实现了master-slave(主从)同步。此外单个value的最大限制是512M,不像 memcached只能保存1MB的数据。

memcached也支持key-value形式的数据但value形式较单一只支持字符串,redis还支持list、hash、set、sort set等数据结构

memcached数据仅存储在内存中,而redis可以周期性把数据存储进磁盘,或把修改操作追加进操作记录文件中

memached的value最大为1M,redis key value最大限制均为是512M

redis的过期数据的淘汰机制是怎样的?

1.惰性删除

Redis很懒,有些Key直到被查询的时候,才会拿去判断是否已经超过了有效期,如果已经超过了有效期,那么才会被删除。也就是说,Redis里面,有可能存在已经过期的数据。

2.主动删除

主动删除功能会定时来把过期的数据清除掉。Redis每秒都会进行10次下面这样的操作:

(1)随机抽取20个设置了有效期的key

(2)把其中已经过期了的key删除掉

(3)如果过期的key超过25%,那么重新进行第一次操作。

因为Redis是单线程的,为了避免资源都浪费在淘汰过期数据上,Redis限制了每次执行时间都是25毫秒,如果超过了这个时间,那么就直接退出。毕竟缓存的主要任务还是缓存服务,不能因小失大。如果Redis在某一段时期变得非常卡顿呢?原因就有可能是有大批量的key在用一个时间过期,那么就会有大量资源在删除过期数据。虽然每次只有25ms,十次也就是250ms了,但相当于1/4的资源都拿去删除过期数据而不是在提供服务,更要命的事,如果删除过多数据可能还会触发内存重新整理,进一步造成卡顿。所以,我们不要批量的设置一批数据在同一个时间过期,最好加个随机值,或者延后。

Redis内存不足的缓存淘汰策略,LRU(最久没有使用的)、LFU(使用频率最少的)

  • noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
  • allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
  • volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
  • allkeys-random:加入键的时候如果过限,从所有key随机删除
  • volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
  • volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
  • volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
  • allkeys-lfu:从所有键中驱逐使用频率最少的键

Redis的缓存淘汰策略LRU与LFU - 简书深入理解Redis数据缓存的LRU实现机制_azurelaker的博客-优快云博客_redis的lru

redis三种模式:主从、哨兵、集群

为了避免单个redis服务器挂掉,造成服务不可用,可以考虑以集群的方式保证服务的正常使用。比较常见有三种方式:

(1)主从的方式:在这种方式中采取1主1备或多备的方式,redis主服务器提供读写服务,从服务器提供读备份的服务,它的实现过程如下:

a、当从服务器启动后,向主服务器发送sync命令

b、主服务器收到sync命令后,会执行bgsave命令生成rdb文件,同时会将执行bgsave命令过程中,数据发生的修改操作保存在一个缓冲区中

c、bgsave命令执行完成后会将rdb文件和缓冲区中缓存的数据修改操作发送给从服务器,此时从服务器根据rdb文件和缓存操作将自己的服务器数据状态更新至主服务器状态

d、在此之后当主服务器接收到客户端的命令后会将处理结果返回,之后会异步将写操作同步给从服务器

(2)哨兵(sentinel)

在主从模式中,当主服务器出现故障时,需要手动的指定一个从服务器变为新的主主服务器,为了实现master出现故障时,集群自动选择新master,提出了哨兵的概念。哨兵的就是会监视每个主从服务器是否可以正常运行,当主服务器出现故障时,自动选举出新的master节点。

工作过程:

a、每个哨兵每隔10秒都会向master节点发送info replication命令,用于获取当前主从服务的最新拓扑结构,这样每个哨兵就可以获取到达slave节点的路径

b、每个哨兵每隔1秒会向集群中matser和salve节点发送心跳,根据回应判断redis节点是否存活,如果长时间节点没有回复,那么哨兵就认为该节点已宕机

c、同时每个哨兵每隔2秒会向redis中指定频道发送消息,通知master和salve的存活情况,且所有的哨兵都会订阅这个频道,因此所有哨兵都知道其他哨兵对集群中主从节点存活状况的判断

d、当其中一个哨兵发现某主节点出现故障时,会查看其他哨兵对该节点的判断,如果认定其出现故障的哨兵数量达到一定限制之后,它就会被移除,挑选一个salve节点成为新的master

e、其余salve与新的master建立联系,更新主从映射

为什么要使用多个哨兵?

当只有一个sentinel的时候,如果这个sentinel挂掉了,那么就无法实现自动故障切换了。在sentinel网络中,只要还有一个sentinel活着,就可以实现故障切换。

客户端如何知道master节点挂了?

客户端不与Redis数据节点打交道,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者。

(3)redis集群的方式:当缓存中数据量比较大的情况下,如果单个服务器不足以存放所有的数据,更不用说主从都要保存完整的数据了,此时可以使用redis集群,它是以主从模式为基础的,需要至少有3个master节点,每个主节点都有自己的从节点。在这种情况下对数据进行分片,将数据存储在不同的redis节点中。在这里就使用到了redis中的槽slot,redis中共有2048*8=16384个槽,我们可以根据每个redis主节点的服务器性能,为它分配一段连续的槽,每个key-value对想要put进缓存的时候,需要对key进行crc16(key)计算它的hash值,然后再对16384进行取模得到它到底在哪个槽上,如果该槽正好是本redis服务器那么进行数据操作,否则就需要转到槽所在的redis服务器进行数据操作。当有新节点添加或节点删除时,需要我们手动的移动槽和对应的数据。

一致性hash可能会存在数据倾斜的情况,大量的hash在一个范围内,导致数据分配的不均匀

一致性哈希和哈希槽 - 简书  一致性hash和redis 槽

redis缓存雪崩

即大量的请求没有走缓存或缓存中没有该数据,导致请求全部打在了数据库上,导致数据库崩溃,从而服务不可用。造成这种情况的原因有可能是因为redis中的大量数据在同一时间段内过期,从而导致查询缓存时找不到所需数据,从而查询数据库;或者redis集群中某一段hash槽所属的主从节点出现故障,其主从服务都不可用,导致其所属hash槽上的数据均不可查询,不过这种情况出现的频率不高

解决方式:(1)项目中如果有批量对redis中数据设置相同过期时间的情况,可以在过期时间时加上一个合适(如1到5秒)的随机数,避免大量缓存在同一时间点失效(2)当发现一个缓存失效后,对于经常会被使用到数据,还需要将它再存入缓存,甚至可以将热点数据设置为永不过期

缓存击穿:是指缓存中某一条数据过期(一般是热点数据),但由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力,它与缓存雪崩的不同是缓存击穿指并发查同一条数据,缓存雪崩是不同数据在redis中都查不到从而查数据库

解决方式:(1)最简单的方式就是设置热点数据永远不过期(2)还有一种常见的作法,对于热点数据过期后如果获取不到值,并不立即去查询数据库,而是先尝试获取一个锁,如redis的SETNX(SET if Not eXists),如果成功就说明我们获取了重新设置热点数据缓存的权限,此时再查询数据库并更新缓存最后再把setnx的key对应值删掉,如果setnx操作失败说明有其他线程正在更新缓存,此时就等待一会然后再重新尝试从缓存中获取数据,避免大量请求直接查询数据库造成压力过大。

public String get(key) {
    String value = redis.get(key);
    if (value == null) { //代表缓存值过期
        //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
        if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
            value = redis.get(key);
            if(value != null) { //双重检查
               redis.del(key_mutex);
               return value;
            }
            value = db.get(key);
            redis.set(key, value, expire_secs);
            redis.del(key_mutex);
            } else {  //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
                    sleep(50);
                    get(key);  //重试
            }
        } else {
            return value;      
        }
}

缓存穿透:

一般是因为查询参数不合法,或者对应的key在数据库中不存在,从而缓存中没有对应的查询结果,如果大量有这样的请求,每一次都会查询数据表,就有可能将数据库搞崩。

解决方式:(1)既然是非法参数,那么我们就需要加强对参数的校验规则,提前发现(2)一般我们缓存数据时,如果为返回结果为空是不会进行记录的,现在可以将这些为空的查询也进行存储,给它设置一个较短的过期时间,避免缓存穿透发生。(3)使用布隆过滤器Boolean Filter,将数据库所有的查询key都预先放入布隆过滤器中,然后当查询到来时,先对key进行一次判断过滤,减少不存在value值key进行查询的概率

布隆过滤器解决缓存穿透的原理_jazon@的博客-优快云博客

redis缓存和数据库的数据一致性:

当数据库中的数据发生改变时,需要执行两步:更新数据库中的数据,删除缓存(更新缓存就算了,直接删除具体可用去查一下)。而不管这两步执行的先后顺序,都会有一致性问题,而现在的争论点也主要在这里。但结合实际情况一般采用的先更新数据库,再删除缓存的策略,facebook的论文也指出他们采用的也是这种方式,因为这种方式出现数据不一致的几率是最小的。

假如有A、B两个线程,A需要更新数据,B是查询数据,此时redis缓存与DB出现数据不一致的情况是:

(1)恰好查询的数据在缓存中不存在或正好失效

(2)恰好B在A更新之前将旧数据从数据库中查了出来

(3)A更新数据

(4)A删除缓存

(5)B将旧数据存入缓存

要造成这种情况需要满足缓存不存在、B在A之前查询了数据、B在A之后插入了数据,但我们知道查询是比更新要快的,因此一般是(5)在(4)之前,这种情况发生的几率是比较小的。如果有人说一定要解决怎么办,我们可以采用双重删除的方式,在A删除缓存之后,在启动一个线程,经过几百MS左右的延迟后再执行删除操作,如果失败则重复执行删除操作,直到成功。

而如果是先删除缓存再更新数据库,不一致的情况则会容易出现的多,还是假如有A、B两个线程,A需要更新数据,B是查询数据:

(1)请求A删除缓存
(2)请求B查询发现缓存不存在
(3)请求B去数据库查询得到旧值
(4)请求B将旧值写入缓存
(5)请求A更新数据

只需要B查询在A删除缓存和更新数据之间进行便可以,是不是概率大了很多。它也可以使用双重删除来进行处理。但还是更推荐先更新DB再删除缓存。

redis事务:redis命令可以由multi开启一个事务,插入命令,由exec执行操作,事务可以保证命令被有序的执行,且不会别其他线程的命令插入,但与mysql事务不同的是,当其中的某条命令执行失败时,并不会造成前面的命令回滚,并且其后的命令依然会执行。同时可以使用watch命令监控一个变量,它会一直监控到exec命令,如果在执行的过程中被监视的变量被其他线程修改,那么事务将被打断,不在执行。Redis的事务和watch - 沐雨橙风丶 - 博客园

redis持久化方式:数据持久化包括RDB和AOF两种方式:RDB会根据设置,一段时间间隔后将当前数据持久化到磁盘文件中;而AOF是将redis修改操作日志追加到操作记录中,不会记录查询操作。官方建议是两种方式同时使用,没有数据持久化存储的redis和memcache一样,是一个单纯的内存缓存。

(1)RDB:该种方式会fork一个子进程来专门进行数据的保存工作,将数据保存在一个新的数据文件中;RDB会生成多个数据文件,每个文件都代表了redis在某一时刻的完整数据快照,redis可以自由设置进行备份的时间间隔,非常适合做备份,且利用备份文件恢复速度较快;缺点也很明显,如果在数据保存之前出现故障,当redis恢复后会丢失一部分数据,且如果数据集是比较大的情况,备份时可能会导致整个服务暂停几百毫秒。

(2)AOF:该种方式是默认关闭的,有三种形式

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;

  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;

  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘

可以将修改操作记录追加在日志中。事实上,每秒同步也是异步完成的,其效率也是非常高的。出现故障后丢失的仅仅当前未来得及记录的操作。根据历史记录进行数据恢复,数据丢失较少,且文件不易破损,即使出现破损也很容易修复,缺点是保存的历史记录文件较大,且恢复速度比较慢

当aof操作日志文件过大时,会开启一个线程对其中的指令进行压缩,创建出一份恢复全量数据所需的最少指令文件,然后对原文件进行替换

redis为什么那么快:

(1)redis数据存储在内存之中,大部分是内存操作,速度很快

(2)redis主要工作,包括接收客户端链接请求、解析请求命令和进行数据读写等操作主流程,都没有创建新线程,只使用了单线程的方式来执行,减少了线程间上下文切换和竞争造成的资源消耗(持久化、异步删除、集群数据同步等,其实是由额外的线程执行的,从这个角度来说,Redis又不能算单线程程序,它还是有多线程)

(3)redis采用了非阻塞、IO多路复用的方式,由一个redis服务线程去处理多个网络连接请求,尽量减少网络IO的时间消耗。因为redis的目的是为了快速的访问内存,所以为了减少多线程上下文切换以及锁的时间,才使用了单线程IO多路复用的模型处理事件,这样只有网络带宽及内存大小才是redis的瓶颈,而不用额外加上线程切换的资源消耗。

在阻塞模式下如果一个客户端与redis服务端建立连接并发起了写请求,但是数据却没有准备好,这种情况下redis就要陷入阻塞状态,去等待客户端准备数据,即使其他的客户端已经完全准备就绪也无法去处理它们的请求,在多客户端的情况下显然这种方式不可取;而如果仅仅是采用非阻塞模式,也要不断循轮询多个客户端socket连接,看它们是否有请求准备就绪,也会存在很多无效的操作。

而在IO多路复用的方式中,由内核帮助我们完成socket事件监控的。redis利用Reactor(事件驱动)模式实现了自己的文件事件处理器,整体流程中可以分为四个模块,socket连接、IO多路复用程序、文件事件分发器、不同事件的处理器。一个客户端与redis服务建立连接的后,需要调用IO多路复用程序,将自己socket信息以fd(文件描述)的形式放入IO多路复用器中,交由内核采用select或epoll的方式进行监控(linux上的redis使用的是epoll),对socket的连接、读取、修改请求进行监听,当没有事件准备好时,redis服务线程就陷入阻塞状态,当某一连接的请求已准备就绪,那么就会将该请求对应的读写事件和连接信息放入事件分发器中,并将redis服务线程唤醒,redis线程会从事件分发器中获取相应读写事件,交由对应事件处理器进行处理并返回,redis文件事件分派器的消费是单线程的,当一个事件分发处理完成后才会处理下一个事件,由于redis大部分操作在内存之上,且redis使用单线程进行数据处理避免了线程间竞争及切换造成的资源浪费,所以速度较快。

(4)reids还提供了压缩表、跳表等数据结构,也对redis的性能由较多提升

为什么redis使用单线程:

根据官网的介绍限制redis性能的不是cpu,它的瓶颈是内存大小和网络带宽,并且redis官网的介绍它的读写性能在普通电脑上可以轻松达到每秒几十万,同时单线程的实现比较简单,不用担心线程安全问题,所以应该是这个原因使用单线程。

多线程访问redis:当多线程访问时,需要对访问的过程来加锁,因为redis虽然速度很快,但是它是单线程来依次处理各个线程的链接请求的,因此可能会造成,在后面的链接请求超时

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值