redis综合讲解之问题篇

摘之:https://blog.csdn.net/xlgen157387/article/details/79530877 天给大家整理一篇关于Redis经常被问到的问题:redis特性、redis为啥这么快、缓存雪 崩、缓存穿透、缓存预热、缓存更新、缓存降级等概念的入门及简单解决方案。

一、redis特性

Redis是一个开源的内存中的数据结构存储系统,它可以用作:数据库、缓存和消息中间件。

它支持多种类型的数据结构,如字符串(Strings),散列(Hash),列表(List),集合 (Set),有序集合(Sorted Set或者是ZSet)与范围查询,Bitmaps,Hyperloglogs 和 地理空间(Geospatial)索引半径查询。其中常见的数据结构类型有:String、List、 Set、Hash、ZSet这5种。

Redis 内置了复制(Replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(Transactions) 和不同级别的磁盘持久化(Persistence),并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(High Availability)。 Redis也提供了持久化的选项,这些选项可以让用户将自己的数据保存到磁盘上面进行存 储。根据实际情况,可以每隔一定时间将数据集导出到磁盘(快照),或者追加到命令日志 中(AOF只追加文件),他会在执行写命令时,将被执行的写命令复制到硬盘里面。您也可 以关闭持久化功能,将Redis作为一个高效的网络的缓存数据功能使用。

二、redis为什么会快

Redis到底有多快,Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库, 由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。这个数据 不比采用单进程多线程的同样基于内存的 KV 数据库 Memcached 差!有兴趣的可以参考 官方的基准程序测试
横轴是连接数,纵轴是QPS。此时,这张图反映了一个数量级,希望大家在面试的时候可以 正确的描述出来,不要问你的时候,你回答的数量级相差甚远!

Redis为什么这么快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致 的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出

现死锁而导致的性能消耗;

4、使用多路I/O复用模型,非阻塞IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样, Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去 移动和请求; 以上几点都比较好理解,下边我们针对多路 I/O 复用模型进行简单的探讨:

(1)多路 I/O 复用模型 多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在 空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤 醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只 依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。 这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用 技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈, 主要由以上几点造就了 Redis 具有很高的吞吐量。

三、那么为什么Redis是单线程的

我们首先要明白,上边的种种分析,都是为了营造一个Redis很快的氛围!官方FAQ表示, 因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的 大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单 线程的方案了(毕竟采用多线程会有很多麻烦!)。看到这里,你可能会气哭!本以为会有 什么重大的技术要点才使得Redis使用单线程就可以这么快,没想到就是一句官方看似糊弄 我们的回答!但是,我们已经可以很清楚的解释了为什么Redis这么快,并且正是由于在单 线程模式的情况下已经很快了,就没有必要在使用多线程了!

但是,我们使用单线程的方式是无法发挥多核CPU 性能,不过我们可以通过在单机开多个 Redis 实例来完善!

警告1:这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来 处理,一个正式的Redis Server运行的时候肯定是不止一个线程的,这里需要大家明确的注 意一下!例如Redis进行持久化的时候会以子进程或者子线程的方式执行(具体是子线程还 是子进程待读者深入研究);例如我在测试服务器上查看Redis进程,然后找到该进程下的 线程:

在这里插入图片描述
ps命令的“-T”参数表示显示线程(Show threads, possibly with SPID column.)“SID”栏表示线程ID,而“CMD”栏则显示了线程名称。

警告2:在上图中FAQ中的最后一段,表述了从Redis 4.0版本开始会支持多线程的方式, 但是,只是在某一些操作上进行多线程的操作!所以该篇文章在以后的版本中是否还是单线 程的方式需要读者考证!

六、注意点
1、我们知道Redis是用”单线程-多路复用IO模型”来实现高性能的内存数据服务的,这种 机制避免了使用锁,但是同时这种机制在进行sunion之类的比较耗时的命令时会使redis的 并发下降。因为是单一线程,所以同一时刻只有一个操作在进行,所以,耗时的命令会导致 并发的下降,不只是读并发,写并发也会下降。而单一线程也只能用到一个CPU核心,所以 可以在同一个多核的服务器中,可以启动多个实例,组成master-master或者master-slave 的形式,耗时的读命令可以完全在slave进行。 需要改的redis.conf项:

  1. pidfile /var/run/redis/redis_6377.pid #pidfile要加上端口号
  2. port 6377 #这个是必须改的
  3. logfile /var/log/redis/redis_6377.log #logfile的名称也加上端口号
  4. dbfilename dump_6377.rdb #rdbfile也加上端口号

2、“我们不能任由操作系统负载均衡,因为我们自己更了解自己的程序,所以,我们可以 手动地为其分配CPU核,而不会过多地占用CPU,或是让我们关键进程和一堆别的进程挤在 一起。”。 CPU 是一个重要的影响因素,由于是单线程模型,Redis 更喜欢大缓存快速 CPU, 而不是 多核 在多核 CPU 服务器上面,Redis 的性能还依赖NUMA 配置和处理器绑定位置。最明显的影 响是 redis-benchmark 会随机使用CPU内核。为了获得精准的结果,需要使用固定处理器 工具(在 Linux 上可以使用 taskset)。最有效的办法是将客户端和服务端分离到两个不同 的 CPU 来高校使用三级缓存。

四、缓存雪崩

缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓 存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存 的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕 机。从而形成一系列连锁反应,造成整个系统崩溃。 缓存正常从Redis中获取,示意图如下:
在这里插入图片描述
缓存失效瞬间示意图如下:

在这里插入图片描述
缓存失效时的雪崩效应对底层系统的冲击非常可怕!大多数系统设计者考虑用加锁或者队列 的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发 请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,比如我们可以在 原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的 重复率就会降低,就很难引发集体失效的事件。

以下简单介绍两种实现方式的伪代码: (1)碰到这种情况,一般并发量不是特别多的时候,使用最多的解决方案是加锁排队,伪代码如下:

//伪代码 
public object GetProductListNew() {     
int cacheTime = 30;     
String cacheKey = "product_list";     
String lockKey = cacheKey;
    String cacheValue = CacheHelper.get(cacheKey);     
    if (cacheValue != null) {         
    return cacheValue;     
    } else {
        synchronized(lockKey) {             
          cacheValue = CacheHelper.get(cacheKey);             
             if (cacheValue != null) { 
                 return cacheValue;             
             } else {               
                 //这里一般是sql查询数据                 
                 cacheValue = GetProductListFromDB();                  
                 CacheHelper.Add(cacheKey, cacheValue, cacheTime);             
                 }         
             }         
                 return cacheValue;     
          } 
}

加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建 期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这 是个治标不治本的方法! 注意:加锁排队的解决方式分布式环境的并发问题,有可能还要解决分布式锁的问题;线程 还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!

(2)还有一个解决办法解决方案是:给每一个缓存数据增加相应的缓存标记,记录缓存的 是否失效,如果缓存标记失效,则更新数据缓存,实例伪代码如下:

//伪代码
public object GetProductListNew() {     
        int cacheTime = 30;     
        String cacheKey = "product_list";     
        //缓存标记     
        String cacheSign = cacheKey + "_sign";
        String sign = CacheHelper.Get(cacheSign);     
        //获取缓存值     
        String cacheValue = CacheHelper.Get(cacheKey);     
            if (sign != null) {         
                return cacheValue; 
                    //未过期,直接返回     
            } else {         
                CacheHelper.Add(cacheSign, "1", cacheTime);         
                ThreadPool.QueueUserWorkItem((arg) -> {       
                //这里一般是 sql查询数据             
                cacheValue = GetProductListFromDB();            
                //日期设缓存时间的2倍,用于脏读           
                CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);                          
                });         
                return cacheValue;     
            } 
		}

解释说明:
1、缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际 key的缓存;
2、缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据 缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用 端,直到另外的线程在后台更新完成后,才会返回新缓存。 关于缓存崩溃的解决方法,这里提出了三种方案:使用锁或队列、设置过期标志更新缓存、 为key设置不同的缓存失效时间,还有一各被称为“二级缓存”的解决方法,有兴趣的读者 可以自行研究。

五、缓存穿透

缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询 的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次 无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存 在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉, 从而避免了对底层存储系统的查询压力。 另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还 是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分 钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继 续访问数据库,这种办法最简单粗暴!

//伪代码
 public object GetProductListNew() {     
        int cacheTime = 30;     
        String cacheKey = "product_list";     
        String cacheValue = CacheHelper.Get(cacheKey);     
            if (cacheValue != null) {         
                return cacheValue;     
            }     
            cacheValue = CacheHelper.Get(cacheKey);     
            if (cacheValue != null) {         
            return cacheValue;     
            } else {         
            //数据库查询不到,为空         
                cacheValue = GetProductListFromDB();         
                if (cacheValue == null) {
            //如果发现为空,设置个默认值,也缓存起来             
                    cacheValue = string.Empty;         
                }         
                CacheHelper.Add(cacheKey, cacheValue, cacheTime);         
                return cacheValue;     
            } 
        } 

把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,即可以避免当查询的 值为空时引起的缓存穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行 预先校验,然后再放行给后面的正常缓存处理逻辑。

六、缓存预热

缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存 预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求 的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
1、直接写个缓存刷新页面,上线时手工操作下;
2、数据量不大,可以在项目启动的时候自动进行加载;
3、定时刷新缓存;

七、缓存更新

除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可 以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系 统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次 用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己 的应用场景来权衡。

八、缓存降级

当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性 能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自 动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入 购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓 死保护,哪些可降级;比如可以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级 或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系 统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。

九、过期键删除策略

如果一个键过期了,那么他什么时候会被删除

  1. 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来 临时,立即执行对键的删除操作。
  2. 惰性删除:放任键过期不管,但是每次从键空间获取键时,都检测获取得的键是否过 期,如果过期的话,就删除该键,如果没有过期,就返回该键。
  3. 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要 删除多少过期键,以及要检查多少数据库,由程序算法决定。 这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。

Redis的过期键删除策略 我们讨论了定时删除,惰性删除,定期删除三种过期键策略,Redis服务器实际使用的是惰 性删除和定期删除两种策略,通过配合使用者两种策略,服务器可以很好的合理的使用CPU 和避免浪费内存空间之间取得平衡。

惰性删除策略的实现

1.过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有的读写数据库的Redis命 令在执行之前都会调用expireIfNeeded函数对输入键进行检查:
如果输入键已经过期,那么expireIfNeeded函数将做输入键从数据库中删除。
如果键未过期,那么expiredNeeded函数不做任何操作。

在这里插入图片描述

AOF,RDB和复制功能对过期键的处理
我们将探讨过期键对Redis服务器中其他模块的影响,看看RDB持久化功能,AOF持久化功 能以及复制功能是如何处理数据库中的过期键的。
生成RDB文件
在执行Save命令或者bgsave命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中。
例如:如果数据库中包含三个键,k1,k2,k3.并且k2已经过期,那么当执行Save命令或者 bgsave命令时,程序只将k1,k3的数据保存到RDB文件中,而K2则会被忽略。
因此,数据库中包含过期键不会对生成新的RDB文件造成影响。
载入RDB文件
在启动Redis服务器时,如果服务器开启了RDB功能,那么服务器对RDB文件进行载入 如果服务器以主服务器模式运行,那么载入RDB文件时,程序会对文件中保存的键进行检 查,未过期的键会被载入到数据库中,而过期键则会被忽略,所以过期键对载入RDB文件的 主服务器不会造成影响。
如果服务器以从服务器模式运行,那么在载入RDB文件时,文件中保存的所有键,不乱是否 过期,都会被载入到数据库中,不过因为主从服务器在同步数据的时候,从服务器的数据库 就会被清空。所以一般来讲,过期键对载入RDB文件的从服务器也不会造成影响。
2.AOF文件写入
当服务器以AOF持久化模式运行时,如果数据库中的某个键已经过期,但它还没有被惰性删 除或者定期删除,那么AOF文件不会因为这个过期键而产生任何影响。

当过期键被惰性删除或者定期删除之后,程序会向AOF文件追加一条DEL命令,来显式地记 录该键已被删除。
和生成RDB文件类似,在执行AOF重写的过程中,程序会对数据库中的键进行检查,已过 期的键不会被保存到重写的AOF文件中。

十、Redis内存满了的几种解决方法(内存淘汰策略与Redis集群)

1,增加内存;2,使用内存淘汰策略。3,Redis集群。
重点介绍下23;
第2点: 我们知道,redis设置配置文件的maxmemory参数,可以控制其最大可用内存大小(字 节)。
那么当所需内存,超过maxmemory怎么办?
这个时候就该配置文件中的maxmemory-policy出场了。
其默认值是noeviction。
下面我将列出当可用内存不足时,删除redis键具有的淘汰规则。
在这里插入图片描述
LRU算法,least RecentlyUsed,最近最少使用算法。也就是说默认删除最近最少使用的 键。
但是一定要注意一点!redis中并不会准确的删除所有键中最近最少使用的键,而是随机抽 取3个键,删除这三个键中最近最少使用的键。
那么3这个数字也是可以设置的,对应位置是配置文件中的maxmeory-samples.
第3点:
3.集群怎么做 Redis仅支持单实例,内存一般最多1020GB。对于内存动辄100200GB的系统,就需要 通过集群来支持了。
Redis集群有三种方式:客户端分片、代理分片、RedisCluster(在之后一篇文章详细说一 下。)
∙ 客户端分片
通过业务代码自己实现路由
优势:可以自己控制分片算法、性能比代理的好
劣势:维护成本高、扩容/缩容等运维操作都需要自己研发
∙ 代理分片
代理程序接收到来自业务程序的数据请求,根据路由规则,将这些请求分发给正确的Redis
实例并返回给业务程序。使用类似Twemproxy、Codis等中间件实现。
优势:运维方便、程序不用关心如何链接Redis实例
劣势:会带来性能消耗(大概20%)、无法平滑扩容/缩容,需要执行脚本迁移数据,不方便(Codis在Twemproxy基础上优化并实现了预分片来达到Auto Rebalance)。
∙ Redis Cluster
优势:官方集群解决方案、无中心节点,和客户端直连,性能较好
劣势:方案太重、无法平滑扩容/缩容,需要执行相应的脚本,不方便、太新,没有相应成 熟的解决案例

十一、redis的过期策略以及内存淘汰机制

分析:这个问题其实相当重要,到底redis有没用到家,这个问题就可以看出来。 比如你redis只能存5G数据,可是你写了10G,那会删5G的数据。怎么删的,这 个问题思考过么?还有,你的数据已经设置了过期时间,但是时间到了,内存占 用率还是比较高,有思考过原因么?

回答:
redis采用的是定期删除+惰性删除策略。

为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放, 但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不 是删除key,因此没有采用这一策略.

定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需 要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行 检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用 定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一 下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。

采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性 删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。

在redis.conf中有一行配置

# maxmemory-policy volatile-lru

该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己)
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。应该没 人用吧。
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少 使用的key。推荐使用,目前项目在用这种。
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除 某个key。应该也没人用吧,你不删最少使用Key,去随机删。
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间 中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存 储的时候才用。不推荐
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的 键空间中,随机移除某个key。依然不推荐
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间 中,有更早过期时间的key优先移除。不推荐 ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删 除) 基本上一致。

十二、redis和数据库双写一致性问题

分析:一致性问题是分布式常见问题,还可以再分为最终一致性和强一致性。数 据库和缓存双写,就必然会存在不一致的问题。答这个问题,先明白一个前提。 就是如果对数据有强一致性要求,不能放缓存。我们所做的一切,只能保证最终 一致性。另外,我们所做的方案其实从根本上来说,只能说降低不一致发生的概 率,无法完全避免。因此,有强一致性要求的数据,不能放缓存。

首先,采取正确更新策略,先更新数据库,再删缓存。其次,因为可能存在删除 缓存失败的问题,提供一个补偿措施即可,例如利用消息队列

十三、如何解决redis的并发竞争key问题

分析:这个问题大致就是,同时有多个子系统去set一个key。这个时候要注意什 么呢?大家思考过么。需要说明一下,博主提前百度了一下,发现答案基本都是 推荐用redis事务机制。博主不推荐使用redis的事务机制。因为我们的生产环 境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个 key操作的时候,这多个key不一定都存储在同一个redis-server上。因此, redis的事务机制,十分鸡肋。

回答:如下所示
(1)如果对这个key操作,不要求顺序 这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较 简单。
(2)如果对这个key操作,要求顺序 假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为 valueB,系统C需要将key1设置为valueC. 期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种时 候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下

系统A key 1 {valueA 3:00}
系统B key 1 {valueB 3:05}
系统C key 1 {valueC 3:10}
那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢 到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。 以此类推。
其他方法,比如利用队列,将set方法变成串行访问也可以。总之,灵活变通。

十四、如何应对缓存穿透和缓存雪崩问题

回答:如下所示
缓存穿透,即黑客故意去请求缓存中不存在的数据,导致所有的请求都怼到数据 库上,从而数据库连接异常。 解决方案:
(一)利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库。 没得到锁,则休眠一段时间重试
(二)采用异步更新策略,无论key是否取到值,都直接返回。value值中维护一个 缓存失效时间,缓存如果过期,异步起一个线程去读数据库,更新缓存。需要做 缓存预热(项目启动前,先加载缓存)操作。
(三)提供一个能迅速判断请求是否有效的拦截机制,比如,利用布隆过滤器,内 部维护一系列合法有效的key。迅速判断出,请求所携带的Key是否合法有效。 如果不合法,则直接返回。 缓存雪崩,即缓存同一时间大面积的失效,这个时候又来了一波请求,结果请求 都怼到数据库上,从而导致数据库连接异常。
解决方案:
(一)给缓存的失效时间,加上一个随机值,避免集体失效。
(二)使用互斥锁,但是该方案吞吐量明显下降了。
(三)双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓 存B不设失效时间。自己做缓存预热操作。然后细分以下几个小点

  • I 从缓存A读数据库,有则直接返回
  • II A没有数据,直接从B读数据,直接返回,并且异步启动一个更新线
    程。
  • III 更新线程同时更新缓存A和缓存B。

十五、扩展

以下也是你应该知道的几种模型,祝你的面试一臂之力!
1、单进程多线程模型:MySQL、Memcached、Oracle(Windows版本);
2、多进程模型:Oracle(Linux版本);
3、Nginx有两类进程,一类称为Master进程(相当于管理进程),另一类称为Worker进程 (实际工作进程)。启动方式有两种:
(1)单进程启动:此时系统中仅有一个进程,该进程既充当Master进程的角色,也充当 Worker进程的角色。
(2)多进程启动:此时系统有且仅有一个Master进程,至少有一个Worker进程工作。
(3)Master进程主要进行一些全局性的初始化工作和管理Worker的工作;事件处理是在 Worker中进行的。
在这里插入图片描述

十二、总结

这些都是实际项目中,可能碰到的一些问题,也是面试的时候经常会被问到的知识点,实际 上还有很多很多各种各样的问题,文中的解决方案,也不可能满足所有的场景,相对来说只 是对该问题的入门解决方法。一般正式的业务场景往往要复杂的多,应用场景不同,方法和 解决方案也不同,由于上述方案,考虑的问题并不是很全面,因此并不适用于正式的项目开 发,但是可以作为概念理解入门,具体解决方案要根据实际情况来确定!

参考文章:
1、http://www.cnblogs.com/zhangweizhong/p/6258797.html
2、http://www.cnblogs.com/zhangweizhong/p/5884761.html
3、http://blog.csdn.net/zeb_perfect/article/details/54135506
4、https://blog.csdn.net/chenyao1994/article/details/79491337
5、https://blog.csdn.net/qq_24210767/article/details/80431394

在 Python 中使用 Redis 数据库时,可以使用 `hgetall` 命令获取指定哈希表中的所有字段和值。具体讲解如下: 1. 连接 Redis 数据库 首先需要使用 Redis 的 Python 客户端连接 Redis 数据库。可以使用 `redis` 模块中的 `Redis` 类来实现: ```python import redis # 创建 Redis 客户端对象 r = redis.Redis(host='localhost', port=6379, db=0) ``` 2. 添加哈希表数据 在获取哈希表数据之前,需要先向 Redis 数据库中添加一些数据。可以使用 `hmset` 命令添加哈希表数据: ```python # 添加哈希表数据 r.hmset('myhash', {'field1': 'value1', 'field2': 'value2', 'field3': 'value3'}) ``` 上述代码中,首先指定哈希表名称为 `myhash`,然后添加了三个字段和对应的值。 3. 获取哈希表数据 使用 `hgetall` 命令可以获取指定哈希表中的所有字段和值。具体使用方法如下: ```python # 获取指定哈希表中的所有字段和值 result = r.hgetall('myhash') print(result) ``` 输出结果为: ``` {b'field1': b'value1', b'field2': b'value2', b'field3': b'value3'} ``` 上述结果中,键名和键值都以字节字符串的形式输出,需要使用 `decode()` 方法将其转换为字符串: ```python # 将字节字符串转换为字符串 result = {key.decode(): value.decode() for key, value in result.items()} print(result) ``` 输出结果为: ``` {'field1': 'value1', 'field2': 'value2', 'field3': 'value3'} ``` 上述代码中,使用字典生成式将字节字符串转换为字符串,并将其存储在 `result` 变量中。 总结: `hgetall` 命令可以获取指定哈希表中的所有字段和值,使用 Redis 的 Python 客户端可以轻松实现该功能。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值