zoukankan      html  css  js  c++  java
  • 缓存总结2

    分类

    本地缓存(HashMap/ConcurrentHashMap、Ehcache、Guava Cache等),

    缓存服务(Redis/Tair/Memcache等)。

    使用场景

    什么情况适合用缓存?考虑以下两种场景:

    1、短时间内相同数据重复查询多次且数据更新不频繁,这个时候可以选择先从缓存查询,查询不到再从数据库加载并回设到缓存的方式。此种场景较适合用单机缓存。

    2、高并发查询热点数据,后端数据库不堪重负,可以用缓存来扛。

    选型考虑

    如果数据量小,并且不会频繁地增长又清空(这会导致频繁地垃圾回收),那么可以选择本地缓存。具体的话,如果需要一些策略的支持(比如缓存满的逐出策略),可以考虑Ehcache;如不需要,可以考虑HashMap;如需要考虑多线程并发的场景,可以考虑ConcurentHashMap。

    其他情况,可以考虑缓存服务。目前从资源的投入度、可运维性、是否能动态扩容以及配套设施来考虑,我们优先考虑Tair。除非目前Tair还不能支持的场合(比如分布式锁、Hash类型的value),我们考虑用Redis。

    设计关键点

    什么时候更新缓存?如何保障更新的可靠性和实时性?

    更新缓存的策略,需要具体问题具体分析。这里以门店POI的缓存数据为例,来说明一下缓存服务型的缓存更新策略是怎样的?目前约10万个POI数据采用了Tair作为缓存服务,具体更新的策略有两个:

    1、接收门店变更的消息,准实时更新。

    2、给每一个POI缓存数据设置5分钟的过期时间,过期后从DB加载再回设到DB。这个策略是对第一个策略的有力补充,解决了手动变更DB不发消息、接消息更新程序临时出错等问题导致的第一个策略失效的问题。通过这种双保险机制,有效地保证了POI缓存数据的可靠性和实时性。

    缓存是否会满,缓存满了怎么办?

    对于一个缓存服务,理论上来说,随着缓存数据的日益增多,在容量有限的情况下,缓存肯定有一天会满的。如何应对?

    ① 给缓存服务,选择合适的缓存逐出算法,比如最常见的LRU。

    ② 针对当前设置的容量,设置适当的警戒值,比如10G的缓存,当缓存数据达到8G的时候,就开始发出报警,提前排查问题或者扩容。

    ③ 给一些没有必要长期保存的key,尽量设置过期时间

    缓存是否允许丢失?丢失了怎么办?

    根据业务场景判断,是否允许丢失。如果不允许,就需要带持久化功能的缓存服务来支持,比如Redis或者Tair。更细节的话,可以根据业务对丢失时间的容忍度,还可以选择更具体的持久化策略,比如Redis的RDB或者AOF。

    缓存被“击穿”问题

    对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑另外一个问题:缓存被“击穿”的问题。

    概念:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

    如何解决:业界比较常用的做法,是使用mutex(互斥)。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

    类似下面的代码:

    Java代码  收藏代码
    1. public String get(key) {  
    2.     String value = redis.get(key);        
    3.     if (value == null) { //代表缓存值过期  
    4.           //设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db  
    5.             if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功  
    6.                 //从数据库获取  
    7.                 value = db.get(key);  
    8.                 redis.set(key, value, expire_secs);  
    9.                 redis.del(key_mutex);  
    10.             } else {    
    11.                 //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可  
    12.                 sleep(50);  
    13.                 get(key);  //重试  
    14.             }  
    15.     }else {  
    16.         return value;        
    17.     }  
    18. }  

    setnx 赋值判断原值是否存在,存在不赋值,返回0;不存在才赋值,返回1

    setnx name Tom  ---返回值:0,因为name的原有value为zlh,存在值则不赋值。

    get name  ---返回值:zlh,因为有值,故上面赋值为tom失败,返回0。

    setnx phone 18501733702   ---返回值:1,赋值成功,因为原来不存在phone的key与value。

    get phone   ---返回值:18501733702,说明上面的setnx赋值成功。

    。。。

    ==================================================================

    说说通用的缓存策略,有两种,下面来点图



     


     

     第一种方案,客户端使用的比较多,缓存和 DB(或者文件)同步更新,服务端一般都是用第二种方案。

    =====================================================================

    那些年使用缓存踩过的坑--缓存更新策略

    https://my.oschina.net/percylee/blog/903295

    今天讲的这个话题,我相信是众多工程师和团队的痛。从我刚开始工作,那时候构建本地缓存,到后续memcache, Redis的出现,到现在各种分布式集群的缓存,例如redis Cluster等产品的出现,缓存越来越发达和复杂了,缓存对我们的系统也越发重要,现在很难相信一个后端服务里没有缓存的存在。在这篇文章里,我会和大家分享一下过去踩到的缓存坑,然后试图给出一些解决方案,大家可以一起讨论,最终拿出更好的方法。由于篇幅有限,所以这里的缓存讨论,只局限于后端服务的缓存,并且不涉及具体的框架,对于H5,iOS和Android等前端缓存的讨论,会在以后的文章里呈现出来。

    案例1,缓存和DB的同步更新不在同一个事务里并且没有重试补偿机制

    为了减少系统间的依赖,不同系统的数据更新往往不放在同一个事务里,采用MQ来进行通信。大家可以看下图,后台系统CRM更新产品数据到DB,Product系统收到异步消息通知后,更新最新数据到缓存。这是一个最常见的缓存应用场景,我相信很多团队都是这样用的。在这个Case里容易出现的问题在于,如果批处理任务收到消息后服务crash掉了,缓存没有正常更新,就出现了与DB的数据不同步,前端系统一直不能读到最新数据,导致业务异常

      

    解决方案:

    1.  失败消息一定要建立一定时间间隔的重试机制

    2.  系统要有缓存更新的报警机制,方便更新失败或者重试超时后,可以人工介入进行补偿。

    案例2, 同一数据被1个以上的服务执行写操作,其中一个服务的缓存数据没有版本控制

    这也是两个不同服务更新数据过程中很常见的情况,见下图,CRM系统更新了某个用户的Profile, 保存更新数据库后,通过MQ通知用户系统更新缓存,由于是异步更新延迟,在缓存更新前,用户系统收到前端的指令,读取了当前缓存里的用户数据,做了修改,并更新到DB中。出现的结果就是数据库里的CRM的更新被错误覆盖。

      

    解决方案:

    缓存里的数据有一个标志位可以作为更新数据库数据的依据(Update_time or Version), 如果缓存里数据时间与数据库时间不能匹配,意味着另外一个服务更新了该数据,那么就先从DB里读取最新数据版本,然后在新版本上提交数据。

      

    案例3, 并发查询缓存中同一数据,如果缓存没命中,导致DB瞬时被打爆做促销活动的时候,存在大量用户的并发访问某一个特定商品,该商品数据缓存失效,或者做了数据更改,但是对应缓存还没有更新,那么所有这些访问将同时直接被作用到DB上。

      

    解决方案:

    做一个计数器或者锁(没有特别复杂逻辑的话,可以直接用HashMap),如果发现某个KEY缓存没有命中,那么在计数器+1, 然后访问数据库,拿到结果更新缓存,清理掉计数器中的key。 在这个过程中,如果有第二个线程或者更多的线程需要访问这个KEY时,发现计数器的值>1 或者被加锁, 那么wait, 直到计数器清理掉,当然,这个技术器阈值是可以在配置文件里配置的,不一定是1。

      

    案例4, 缓存没有设置默认值,被攻击,缓存一直保持在被“穿透”状态

    这个情况,和案例3比较类似,都是缓存无法命中,但不一样的地方在于,数据的KEY值是无法控制的,所以没法简单的用计数器和锁来处理, 比方,被人为攻击,制造的大量的无效userID访问。

    解决方案:

    所有没有在缓存的KEY,全部分配一个默认VALUE “UNKOWN-KEY” ,具体是什么情况下,将默认值分配给没有命中的KEY, 这个可以根据自己的业务系统来定,比方说,可以根据特定的IP段,或者没有命中的总次数等,然后我们就可以决定是否继续访问DB还是直接返回默认值给前端,拒绝本次数据访问。这种做法的核心在于,每次数据访问,都会有缓存结果返回,根据系统的情况来决定是否要进一步访问DB。

      

    总结,今天列举的这几个案例,归纳起来,可以总结为以下几点:

    1. 保证缓存同步

    2. 减少缓存并发

    3. 杜绝缓存穿透

      

    缓存与背后的DB是相互依存的关系,缓存系统的设计原则,就是将访问的异常处理或者压力尽可能的前置处理掉,将DB还原成它最初本来的存储功能

    转自:https://uule.iteye.com/blog/2427505

    寻找撬动地球的支点(解决问题的方案),杠杆(Java等编程语言)已经有了。xkzhangsan
  • 相关阅读:
    UVA 1386 Cellular Automaton
    ZOJ 3331 Process the Tasks
    CodeForces 650B Image Preview
    CodeForces 650A Watchmen
    CodeForces 651B Beautiful Paintings
    CodeForces 651A Joysticks
    HUST 1601 Shepherd
    HUST 1602 Substring
    HUST 1600 Lucky Numbers
    POJ 3991 Seinfeld
  • 原文地址:https://www.cnblogs.com/xkzhangsanx/p/10891384.html
Copyright © 2011-2022 走看看