集中讨论关于缓存 Cache 的问题
Posted sp42a
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了集中讨论关于缓存 Cache 的问题相关的知识,希望对你有一定的参考价值。
缓存四大问题及解决方案
https://blog.csdn.net/zyhlwzy/article/details/110420098
问题一:缓存穿透,
指缓存中没有数据,数据库中也没有数据。在进行数据的访问时,通过数据的 key 读取数据,但是该 key 对应数据在数据库中没有,在缓存中也没有,造成每次通过该 key 读取数据都会进行数据库操作,且每次读取都为 null 的情况。在大型项目中,这种无效的数据库操作会增加数据库的读压力。
示例代码:
public Object getData(String key)
if(redisTemplate.hasKey(key))
return redisTemplate.opsForValue().get(key);
Object object = dataDao.selectSysCompanyById(key);
if(object != null)
redisTemplate.opsForValue().set(key,object);
return object;
如上代码,在项目当中读取数据时,首先都会先读取缓存,如果缓存中不存在数据,则从数据库读取数据,如果从数据库读取到数据,那么将数据缓存并返回。
当请求的 key 对应的数据,在缓存中不存在,同时在数据库中也不存在时,就会出现每次都会执行数据库操作,但是每次都是返回 null 的无效数据库操作,这种情况就是典型的缓存穿透。
解决方案一:缓存空对象,即对于在数据库中和缓存中都不存在的数据,第一次读取到为 null 时,在缓存中就存一个空值。读取缓存的时候,如果缓存中存在,且为空对象,直接返回即可。
public Object getData(String key)
if(redisTemplate.hasKey(key))
if(redisTemplate.opsForValue().get(key) == null)
//存在缓存且为空,直接返回
return null;
return redisTemplate.opsForValue().get(key);
Object object = dataDao.selectSysCompanyById(key);
if(object != null)
redisTemplate.opsForValue().set(key,object);
else
//缓存空对象
redisTemplate.opsForValue().set(key, null);
return object;
解决方案二:使用布隆过滤器,所谓布隆过滤器,我们可以理解其为一个集合,这个集合只存储数据的 key 而不存储数据值,主要用来判断一个 key 存在还是不存在。布隆过滤器的介绍见 https://zhuanlan.zhihu.com/p/72378274。
google 的 guava 实现了单机版的(即基于JVM)的布隆过滤器,我们的示例使用 guava 来呈现,在实际的项目中,如果已经达到了要使用布隆过滤器,那么基本上是要自己实现一个分布式的布隆过滤器,可以先研究 google 的 guava 的实现,来自行尝试实现一个自己的布隆过滤器。
使用布隆过滤器解决缓存穿透,其主要思想是,将数据的key全部存入布隆过滤器,在进行数据读取时,先判断key在布隆过滤器中是否存在,如果存在则执行读取操作,如果不存在,直接返回。
也就是说,在创建数据或者将数据同步到缓存服务时,需要将数据的 key 存储到布隆过滤器的集合中,用以作为读取数据时的判断依据。
public Object getDataWithBloom(String key)
//initBloomFilter : 初始化布隆过滤器,将数据库中所有的key都写入到布隆过滤器
BloomFilter<String> bloomFilter = initBloomFilter();
if(!bloomFilter.mightContain(key))
//存在缓存且为空,直接返回
return null;
if(redisTemplate.hasKey(key))
return redisTemplate.opsForValue().get(key);
Object object = dbMapper.selectSysCompanyById(key);
if(object != null)
redisTemplate.opsForValue().set(key,object);
return object;
问题二:缓存击穿
所谓缓存击穿,是指指定 key 的数据在数据库中存在,但是缓存中还没有写入该 key 对应的数据,在高并发场景下,多个线程同时通过该 key 读取数据时,会因为高并发访问的原因导致同一个 key 对应的数据会从数据库被多次读取。正常情况下是同一个 key 的数据,只能允许一个线程从数据库读取一次并缓存到缓存服务器 Redis 之后,其他的线程读取数据时直接从缓存服务读取。
示例代码:
public Object getData(String key)
if(redisTemplate.hasKey(key))
return redisTemplate.opsForValue().get(key);
Object object = dataDao.selectSysCompanyById(key);
if(object != null)
redisTemplate.opsForValue().set(key,object);
return object;
比如上述代码实现,就存在缓存击穿的风险,在高并发场景下,如果同时有100个线程请求同一个 key 的数据,且该数据在数据库中存在,但是在缓存中不存在。那么在某一个线程从数据库读取数据并写入到缓存之前,所有执行了到2行代码的线程都会执行一次数据库的 select 操作和缓存的写入操作。如第一个线程还未读取数据写入缓存,就有50个线程执行完了第2行代码,那么这50个线程都会执行一次数据库的 select 操作,同时也都会做一次缓存写入操作,这就是典型的缓存击穿现象。
解决方案:互斥锁,单机情况下,使用 JVM 中的同步代码块 synchronized
或者 ReentrantLock
,分布式情况下,使用分布式锁。
比如:
public Object getData(String key)
//initBloomFilter : 初始化布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),10000,0.0001);
if(!bloomFilter.mightContain(key))
//存在缓存且为空,直接返回
return null;
if(redisTemplate.hasKey(key))
return redisTemplate.opsForValue().get(key);
//加锁
RLock lock = redissonClient.getLock(key);
lock.lock();
Object object = null;
try
if(redisTemplate.hasKey(key))
//阻塞的线程获得锁后,直接从缓存读取返回
return redisTemplate.opsForValue().get(key);
//第一个线程读库
object = dbMapper.selectSysCompanyById(key);
if(object != null)
//第一个线程写缓存
redisTemplate.opsForValue().set(key,object);
finally
//释放锁
lock.unlock();
return object;
问题三:缓存雪崩
所谓缓存雪崩,就是指缓存服务不具备高可用性导致大面积缓存失效的情况。
解决方案:缓存集群,数据量不大,小公司,采用Redis主从+哨兵模式的解决方案,海量数据,大公司,采用Redis Cluster模式搭建缓存集群。如果发生了雪崩,应急方案为进行服务熔断,通过限流慢慢对缓存进行预热。
问题四:缓存与数据库数据一致性问题
所谓数据一致性,就是指在解决数据进行更新时,如何对数据进行操作才能保证数据库中的数据和缓存中的数据一致的问题。
发生数据一致性问题的场景一:
发生数据一致性问题的场景二:
解决方案:先删除缓存,再更新数据库。但是单纯的先删除缓存,再更新数据库,高并发场景下依然解决不了问题。如下图:
解决方案一:延迟双删:
解决方案二:串行化:
基于 cache 的过载问题解决模式
https://blog.csdn.net/bjo2008cn/article/details/53305739
假设系统 A 依赖于系统 B,同时为了提高访问效率,A 系统在本地设置系统 B 的c ache,其过期时间为 t。当 cache 失效时的策略如下:
基于超时的常规模式: 单统程请求,其他线程等待
在t到达后,Cache 中的 Key 和对应 Value 将被清除,get 操作将通过 RPC 获取 B 系统的 Key 对应的 Value,并更新本地 Cache。
此时,如果另一个线程发现 cache 过期,get 操作先判断有没有其他线程发起了远程调用, 如果有,那么自己就等待,直到那个线程远程获取操作成功,get 操作返回更新后的 cache 中的值。如果远程获取操作失败,则 get 操作抛出异常,不会返回任何 Value。
基于刷新的常规模式:
在t到达后,Cache 中的Key和相应Value都不会被清除,而是被标记为旧数据,如果有线程调用get操作,将触发 refresh 更新操作,根据get和refresh的同步关系,又分为两种模式:
- 同步模式:get操作等待refresh操作结束,refresh结束后,get 操作返回当前 Cache 中Key对应的Value(新值),注意:refresh操作结束并不意味着refresh成功,还可能抛了异常,这时 get操作返回的值可能是旧值。如果其他线程进行get操作,Key已经过期,并且发现有线程触发了refresh操作,则自己不等refresh完成直接返回旧值。
- 异步模式:get操作触发refresh操作,不等refresh完成,直接返回Cache中的旧值。如果其他线程进行get操作,发现Key已经过期,并且发现有线程触发了refresh操作,则自己不等refresh完成直接返回旧值。
基于刷新的续费模式
该模式和基于刷新的常规模式唯一的区别在于 refresh 操作超时或失败的处理上。在基于刷新的常规模式中,refresh 操作超时或失败时抛出异常,Cache 中的相应 Key-Value 还是旧值,这样下一个 get 操作到来时又会触发一次 refresh 操作。在基于刷新的续费模式中,如果 refresh 操作失败,那么 refresh 将把旧值当成新值返回,这样就相当于旧值又被续费了T时间,后续T时间内 get 操作将取到这个续费的旧值而不会触发 refresh 操作。
基于刷新的续费模式也像常规模式那样分为同步模式和异步模式,不再赘述。
缓存更新的套路
https://coolshell.cn/articles/17416.html
看到好些人在写更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。然而,这个是逻辑是错误的。试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
我不知道为什么这么多人用的都是这个逻辑,当我在微博上发了这个贴以后,我发现好些人给了好多非常复杂和诡异的方案,所以,我想写这篇文章说一下几个缓存更新的 Design Pattern(让我们多一些套路吧)。这里,我们先不讨论更新缓存和更新数据这两个事是一个事务的事,或是会有失败的可能,我们先假设更新数据库和更新缓存都可以成功的情况(我们先把成功的代码逻辑先写对)。
更新缓存的的 Design Pattern 有四种:Cache aside, Read through, Write through, Write behind caching,我们下面一一来看一下这四种 Pattern。
Cache Aside Pattern
这是最常用最常用的 pattern了。其具体逻辑如下:
- 失效:应用程序先从 cache取 数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
- 命中:应用程序从 cache 中取数据,取到后返回。
- 更新:先把数据存到数据库中,成功后,再让缓存失效。
Cache-Aside-Design-Pattern-Flow-Diagram
Updating-Data-using-the-Cache-Aside-Pattern-Flow-Diagram-1
注意,我们的更新是先更新数据库,成功后,让缓存失效。那么,这种方式是否可以没有文章前面提到过的那个问题呢?我们可以脑补一下。
一个是查询操作,一个是更新操作的并发,首先,没有了删除 cache 数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。而不会像文章开头的那个逻辑产生的问题,后续的查询操作一直都在取老的数据。
这是标准的 design pattern,包括 Facebook 的论文《Scaling Memcache at Facebook》也使用了这个策略。为什么不是写完数据库后更新缓存?你可以看一下 Quora 上的这个问答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是怕两个并发的写操作导致脏数据。
那么,是不是 Cache Aside 这个就不会有并发问题了?不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。但,这个 case 理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
所以,这也就是Quora上的那个答案里说的,要么通过 2PC 或是 Paxos 协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而 Facebook 使用了这个降低概率的玩法,因为 2PC 太慢,而 Paxos 太复杂。当然,最好还是为缓存设置上过期时间。
Read/Write Through Pattern
我们可以看到,在上面的 Cache Aside 套路中,我们的应用代码需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository)。所以,应用程序比较啰嗦。而 Read/Write Through 套路是把更新数据库(Repository)的操作由缓存自己代理了,所以,对于应用层来说,就简单很多了。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的 Cache。
Read Through
Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside 是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载,从而对应用方是透明的。
Write Through
Write Through 套路和 Read Through 相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由 Cache 自己更新数据库(这是一个同步操作)
下图自来 Wikipedia 的 Cache 词条。其中的 Memory 你可以理解为就是我们例子里的数据库。
Write-through_with_no-write-allocation
Write Behind Caching Pattern
Write Behind 又叫 Write Back。一些了解 Linux 操作系统内核的同学对 write back 应该非常熟悉,这不就是 Linux 文件系统的 Page Cache 的算法吗?是的,你看基础这玩意全都是相通的。所以基础很重要,我已经不是一次说过基础很重要这事了。
Write Back 套路,一句说就是,在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的 I/O 操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg 还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。但是,其带来的问题是,数据不是强一致性的,而且可能会丢失(我们知道 Unix/Linux 非正常关机会导致数据丢失,就是因为这个事)。在软件设计上,我们基本上不可能做出一个没有缺陷的设计,就像算法设计中的时间换空间,空间换时间一个道理,有时候,强一致性和高性能,高可用和高性性是有冲突的。软件设计从来都是取舍 Trade-Off。
另外,Write Back 实现逻辑比较复杂,因为他需要 track 有哪数据是被更新了的,需要刷到持久层上。操作系统的 write back 会在仅当这个 cache 需要失效的时候,才会被真正持久起来,比如,内存不够了,或是进程退出了等情况,这又叫 lazy write。
在 wikipedia 上有一张 write back 的流程图,基本逻辑如下:
Write-back_with_write-allocation
再多唠叨一些
- 上面讲的这些 Design Pattern,其实并不是软件架构里的 mysql 数据库和 memcache/redis 的更新策略,这些东西都是计算机体系结构里的设计,比如 CPU 的缓存,硬盘文件系统中的缓存,硬盘上的缓存,数据库中的缓存。基本上来说,这些缓存更新的设计模式都是非常老古董的,而且历经长时间考验的策略,所以这也就是,工程学上所谓的 Best Practice,遵从就好了。
- 有时候,我们觉得能做宏观的系统架构的人一定是很有经验的,其实,宏观系统架构中的很多设计都来源于这些微观的东西。比如,云计算中的很多虚拟化技术的原理,和传统的虚拟内存不是很像么?Unix 下的那些 I/O 模型,也放大到了架构里的同步异步的模型,还有 Unix 发明的管道不就是数据流式计算架构吗?TCP 的好些设计也用在不同系统间的通讯中,仔细看看这些微观层面,你会发现有很多设计都非常精妙……所以,请允许我在这里放句观点鲜明的话——如果你要做好架构,首先你得把计算机体系结构以及很多老古董的基础技术吃透了。
- 在软件开发或设计中,我非常建议在之前先去参考一下已有的设计和思路,看看相应的 guideline,best practice 或 design pattern,吃透了已有的这些东西,再决定是否要重新发明轮子。千万不要似是而非地,想当然的做软件设计。
- 上面,我们没有考虑缓存(Cache)和持久层(Repository)的整体事务的问题。比如,更新 Cache 成功,更新数据库失败了怎么吗?或是反过来。关于这个事,如果你需要强一致性,你需要使用“两阶段提交协议”——prepare, commit/rollback,比如 Java 7 的 XAResource,还有 MySQL 5.7 的 XA Transaction,有些 cache 也支持 XA,比如 EhCache。当然,XA 这样的强一致性的玩法会导致性能下降,关于分布式的事务的相关话题,你可以看看《分布式系统的事务处理》一文。
缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题
https://blog.csdn.net/xlgen157387/article/details/79530877
前面一节说到了《为什么说 Redis 是单线程的以及Redis为什么这么快!》,今天给大家整理一篇关于 Redis 经常被问到的问题:缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等概念的入门及简单解决方案。
缓存雪崩
缓存雪崩我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库 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;
解释说明:
- 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际 key 的缓存;
- 缓存数据:它的过期时间比缓存标记的时间延长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进行预先校验,然后再放行给后面的正常缓存处理逻辑。
缓存预热
缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
解决思路:
- 直接写个缓存刷新页面,上线时手工操作下;
- 数据量不大,可以在项目启动的时候自动进行加载;
- 定时刷新缓存;
缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis 默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
- 定时去清理过期的缓存;
- 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的 key 是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
- 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
- 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
- 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
- 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
总结
这些都是实际项目中,可能碰到的一些问题,也是面试的时候经常会被问到的知识点,实际上还有很多很多各种各样的问题,文中的解决方案,也不可能满足所有的场景,相对来说只是对该问题的入门解决方法。一般正式的业务场景往往要复杂的多,应用场景不同,方法和解决方案也不同,由于上述方案,考虑的问题并不是很全面,因此并不适用于正式的项目开发,但是可以作为概念理解入门,具体解决方案要根据实际情况来确定!
参考文章:
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
以上是关于集中讨论关于缓存 Cache 的问题的主要内容,如果未能解决你的问题,请参考以下文章
十一:Centralized Cache Management in HDFS 集中缓存管理
如何在spring cache java中配置多个缓存管理器