缓存机制程序员

常用缓存系统使用经验总结

2017-10-26  本文已影响332人  zqrferrari

缓存系统是提升系统性能和处理能力的利器,常用的缓存系统各自的特性和使用场景有所不同,这里总结下常用缓存系统时需要关注的点以及解决方案,以及业务中缓存系统的选型等。

本文内容主要包括以下:

1、常用缓存系统


在平常的业务开发过程中,一般会使用集团自己开发的tair分布式缓存系统,tair有三种存储引擎:mdb、ldb、rdb,从名字上就可以看出,分别对应memcache、leveldb、redis。在一些特定场景,还会使用到localcache,常见的会用到guava cache。

2、缓存使用中需要注意的点


2.1 热点

缓存中的热点key是指短时间大量访问同一个key,一般是高读低写。短时间频繁访问同一个key,请求会打到同一台缓存机器上,形成单点,无法发挥分布式缓存集群的能力。

案例:商品信息,更新很少,但是读取量很大,一般会以商品id为key,value为商品的基本信息。在大促期间有些热门商品会被频繁访问(小米新品首发、秒杀场景),形成热点商品。

解决方案:

2.2 惊群

缓存系统中的惊群效应是指大并发情况下某个key在失效瞬间,大量对这个key的请求会同时击穿缓存,请求落到后端存储(一般是db),导致db负载升高,rt升高。

案例:热点商品的过期,在缓存商品信息时一般会设置过期时间,在热点商品过期的瞬间,大量对这个商品信息的请求会直接落到db上。

分析:缓存失效瞬间,大量击穿的请求在从db获取数据之后,一般会再回写到缓存中,所以实际上只需要一个请求真正去db获取数据即可,其他请求等待它将数据回写到缓存中再从缓存中获取即可。

解决方案:

2.3 击穿

缓存击穿的场景有很多,如由缓存过期产生的惊群,数据冷热不均导致冷数据击穿到db,还有一种情况则是由空数据导致的缓存击穿。

案例:手淘包裹card提供用户最近30天的签收和未签收包裹列表,列表索引由redis zset构建,key为用户id,members为包裹id,score为包裹更新时间。查询时如果redis中查询不到用户相关的包裹列表索引,则去db中查询,查询完成之后再将db返回的结果回写到redis中,这是常规的处理方案。但是如果一个用户在最近30天都没有任何包裹,当他查询的时候则会每次都击穿缓存,落到db,而db中也没有该用户最近30天的包裹数据,缓存中依然为空。不幸的是这个接口的调用时机是手淘-“我的淘宝“tab,双十一调用峰值是8w qps,而大部分最近30天没有买过东西(大部分是男性)用户也会在大促的时候频繁使用手淘,这部分用户在每次查询的时候都会击穿缓存落到db,整个过程只能获取到一堆空数据。

解决方案:

2.4 并发

并发请求会带来很多问题,如之前讨论的热点key、惊群的并发读取,而并发写入也是一个需要考虑的点。

案例:商品的库存信息,大促期间有多个线程同时更新商品的库存数量,如:线程A获取库存数为10,做库存-2操作,并将结果8写入缓存;线程B在线程A写入前获取库存数为10,做库存-1操作,将结果9写入操作,这种情况下,缓存中保存的库存数量必定是有问题的。

解决方案:

2.5 一致性

使用缓存系统时,一致性是一个比较难解决的问题,需要在业务评估的时候就要考虑起来。一般业务对一致性的要求可以分为三档:强一致性、弱一致性、最终一致性。

如果业务对数据的一致性非常敏感,如电商的交易订单信息,其中涉及到交易的状态、付款信息等频繁变更的场景,而许多需要反查交易的系统对交易订单的状态的准确性要求非常高,即便是短暂的不一致也不能忍受。这种场景下,交易系统对数据的要求是强一致的,强一致场景下使用缓存系统则会极大的提高系统的复杂性,所以不建议使用独立的分布式缓存系统。使用mysql做后端存储时,强一致场景下,可以考虑mysql5.7 memcache plugin特性,即可以享受缓存带来的高性能又不用为数据一致性担心。

而大部分业务对数据的一致性要求不是很严格,如商品的名称、评价系统中的评论、点赞的个数、包裹的物流状态等,用户对这些信息是不是和后端存储中一样是不敏感的,短暂的不一致不会带来很严重的后果,这些场景下使用缓存系统比较合适。但是没有强一致性的要求不代表没有一致性的要求,一致性处理不好一样会带来用户的困惑或者系统的bug,比较常见的场景是列表页和详情页的不一致。

在处理缓存和后端存储数据一致性的时候,需要考虑以下几点:

2.6 预热

使用分布式缓存的目的是为了替后端存储挡下绝大部分的请求,但是在实际的业务场景中,数据的时候用频率是不一样的,有的数据请求高,有的数据请求低,这样就造成数据的冷热不均,而且这样的冷热数据往往也是跟实际的业务场景变化而变化,在电商场景中则更加明显。

案例:家居大促、暑期电脑家电大促、秋冬服装大促等。每次电商节,行业大促其侧重点都有所不同,反应在应用系统的数据的缓存上,则是不同商品在缓存系统中的冷热交替。如平常家居类商品访问会很少,所以在缓存系统中由于请求较少,一段时间后会被逐出或者过期掉,甚至在db中也是冷数据,在大促开始的时候则会由于流量的涌入,导致缓存被击穿,请求到达后端存储,造成存储系统压力过大。

解决方案:

2.7 限流

缓存系统虽然性能很高,单机几万到几十万qps也没有问题,但是毕竟是有处理极限,对请求还是需要有基本的限流措施,而应用也需要时刻关注是否触发了缓存系统的限流,如果触发需要立即停止调用并进行review,否则会拖垮缓存系统或者影响其他使用同个缓存系统的业务。

2.8 序列化&压缩

大并发下对缓存系统的请求qps一般都非常高,一个系统几十万甚至上百万的请求也有可能的,序列化的性能以及序列化后的空间消耗则变得比较重要,所以需要选择合适的序列化的方式。

案例:商品信息中包含了商品的名称、商品图片地址、商品类目、商品描述、商品视频地址、商品属性等,这些信息很少更新,但是会造成商品的size会很大,一个商品信息的DO在使用java原生序列化之后会有几十K,如果一次批量获取则有可能超过1M。

解决方案:

2.9 容灾

使用缓存系统的时候一定要明确一个思想,缓存不是存储,它不能用来代替持久化的存储方案,如db、hbase。即便是redis已经宣称实现了持续久化的方案RDB和AOF,缓存系统后端还是需要有一套持久的存储。

如果数据是不可丢失的,那么在使用缓存系统的时候,一定需要考虑当缓存系统崩溃或者网络抖动时,缓存中数据丢失和不一致的容灾方案,还有缓存恢复之后数据重建方案。

案例:手淘包裹列表的redis方案,使用redis的zset来实现包裹按时间的排序,查询时先查redis拿到排好序的包裹id列表,再用id列表回表查询具体数据。这样做的好处是复杂的排序操作由原先db移到redis,db只需要完成简单的主键id查询即可,提升查询的性能。但是需要考虑的是如果redis不可用,那么还是需要到db中完成复杂的查询,只是这个时候需要对查询的接口进行限流,防止压垮db。而redis恢复之后数据恢复方案有两种,一是直接清空掉redis中所有数据,一段时间内由db查询支撑并缓慢重建用户在redis中的包裹数据,二是清空redis数据并遍历db重建所有数据。

2.10 统计&监控

主要是统计缓存的命中率、错误数、错误类型等指标。

缓存命中率直接反应了缓存的效果,如果命中率过低(30%以下)则加缓存带来的受益不大,这个时候付出的缓存容量、代码复杂度都得不偿失,所以需要及时review使用缓存的场景、key的设计、冷热数据、代码的使用,逐步调优提升命中率(70%以上)。

缓存的错误数、错误类型则用于统计和监控分布式缓存应用的健康状态,在缓存崩溃或者网络抖动的时候,错误数或者错误持续时长达到阈值则需要切换到容灾方案。

3、其他


3.1 spring cache

缓存系统的引入必然会对原有的代码结构带来一定的冲击,特别是在复杂场景下往往不只会使用一套缓存系统,mdb、ldb、redis、localcache全上也有可能,还涉及到一致性、并发、击穿等处理,代码的复杂度会大大增加。

spring cache是一套基于注释的缓存技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。

通过使用spring cache的注解可以在DO层进行横切,让缓存和DO操作隔离开,关注于各自的业务逻辑,从而实现对外高内聚,对内松耦合。spring cache的说明和各个注解的作用不做多的介绍,主要介绍下使用经验。

3.2 分布式锁

分布式锁是分布式场景下一个典型的应用,其实现方式多种多样,也有很多基于缓存系统的实现方式。

上一篇 下一篇

猜你喜欢

热点阅读