暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

项目中缓存的那些事

Sumslack团队 2018-09-27
203

缓存在项目中的位置实在太重要了,有超过90%以上的项目都是读远大于写的,合理的使用缓存,可有效提升系统性能,减缓IO数据库压力,同时我们也应该防范缓存穿透,缓存并发,缓存集中失效等问题,如果连缓存在项目中都没能被合理使用,那就甭提分库分表,全文搜索,分布式,微服务等,可以这样说,缓存是每个程序员必备的最重要技能之一,任何优秀的项目都离不开缓存的合理使用


本文将为您呈现缓存的方方面面,从你开始一个项目时,在用户看来它们的访问路径应该是这样的:

你的缓存可能被使用包括所有层次中,从前端(Vue缓存,路由缓存等),到nginx代理层(静态资源缓存等),再到服务层(方法缓存),数据库层(一二级缓存使用),所以,不管你是前端程序员,还是后台架构师亦或是中间件工程师,缓存,可以说,无处不在!

缓存穿透

我们在获取数据时,需要判断数据在缓存中是否存在,如无,则进行数据库操作,如存在,直接返回缓存内容,那么试想,如果用户以一个不存在的key
来频繁攻击我们的应用,并发量很大时,所有的请求全部会进行数据库查询,缓存完全失效,甚至引发DB Crash
,怎么解决呢?可以对不存在的Key值预设某个特殊值,当大量不存在的Key
攻击我们系统时,如果返回这个特殊值,我们认为这个Key
对应的值不存在,由应用决定是否等待某个时间后继续请求这个Key
,这样就避免大量的请求穿透到数据库进行查询,而屏蔽在了缓存之中;

缓存并发

有时候如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题。
我们可以对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。
这种情况和刚才说的预先设定值问题有些类似,只不过利用锁的方式,会造成部分请求等待。

缓存失效

这个和高并发类似,缓存一般都有失效时间,这样就会出现某个时间点缓存失效,引发请求全部转发到DB,造成DB压力,所以一般我们建议缓存失效时间分散开,对多个缓存的CacheName设置的失效时间错峰,这样缓存同时失效的概率将大大降低。

本地缓存和分布式缓存

在我们项目中,经常会采用本地缓存和分布式缓存混用的情况,如数据库查询可以采用本地缓存,token存储可采用分布式缓存等;

  • 本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费,创建的本地缓存框架是ehcache
    ,也可以是自己写的一个static map,如需实时更新本地缓存,可结合zk的自动发现机制;

  • 分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存,常见的分布式缓存是redis
    memcache
    等;

目前各种类型的缓存都活跃在成千上万的应用服务中,还没有一种缓存方案可以解决一切的业务场景或数据类型,我们需要根据自身的特殊场景和背景,选择最适合的缓存方案。

一个应用中的那些缓存

首先,前端的静态页面我们可以使用nginx
缓存并在nginx
中设置过期时间,请求转发到后台服务时,一般可以使用ehcache
redis
,memcache
方法级
的缓存,缓存可以被应用在涉及到token
之类的有时效性的缓存,也可以用在诸如数据库之类的查询中,可以将缓存内容写入磁盘或分布式缓存服务器中,如下是Spring
的方法级缓存代码:

 1  //根据入参缓存
2  @Cacheable(value="apiAppInstalledCache",key="methodName+#Id")
3  public ApiSettingsArgs getSettingsArg(Long id) {
4      //coding
5  }
6  //如更新动作,则刷新指定缓存
7  @CacheEvict(value="apiAppInstalledCache",allEntries=true)
8  public boolean removeSettingsArg(Long id) {
9      //coding
10  }

复制

Spring的缓存注解能满足大部分需求,也可以在Spring注解缓存基础上,根据自身业务进行定制;

最常用的本地缓存框架ehcache
常用属性的说明

 1name:缓存名称
2maxElementsInMemory:内存中最大缓存对象数
3maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大
4eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false
5overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。
6diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。
7diskPersistent:是否缓存虚拟机重启期数据
8diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒
9timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地处于空闲状态
10timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后,如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,Ehcache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义
11memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。

复制

后台服务再往后,就是数据库DAO
的二级缓存,如下:

1    @Query("from App")
2    @QueryHints({ @QueryHint(name = "org.hibernate.cacheable", value ="true") })
3    Page<App> findAll(Pageable pageable);

复制

数据库的一级缓存存在于同一个Session
会话中,如果您采用JPA
,一般实用性不高。

在一个项目中,用的最多的就是方法级缓存,合理有效的使用缓存,能让系统的性能有质的提升。

感谢阅读!


你可以继续阅读:云服务推送API中消息中间件的使用 | 云服务平台中推送服务的设计与实现 | 对微服务的理解以及实现一套微服务对外发布API管理平台 | 项目开发中常用的设计模式整理 | 异构语言调用平台的设计与实现 | 大话正则表达式 | 云API平台的设计与实现 | 个税改了,工资少了,不要慌!文末附计算器



关注我们的公众号 

长按识别二维码关注我们



文章转载自Sumslack团队,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论