一个缓存使用案例

背景

有2w+个商品需要在商品详情页展示定制的信息,我们将这部分数据落到一个单表中,然后提供一个RPC接口让商品详情系统获取定制信息。可想而知,这个接口的访问量是蛮高的,在日常情况下qps大概是6k左右,而在大促时候,预估会有5w+的qps。为了能够让接口稳定的运行,我们不得不采用缓存来提高性能,同时防止DB被打垮。

多级缓存

日常情况2w+的商品6k的qps,我们认为就算有热点也高不到哪里去,所以只用一级缓存服务器(用redis表示)即可。
《一个缓存使用案例》

大促的时候,就算5w+的qps只用redis缓存我认为也是够的,毕竟redis集群下key也是分散到各个服务器上,就算有一个极热的商品估计撑死也就5000。但是,从以为的经验来看,大促的时候这种非关键应用的资源有可能是会被超卖的,然后就偶尔来几下超时,这一超时吧,rpc的线程就会被阻塞了,qps一上来很快线程池就耗光了,然后其他服务也就挂了。所以,为了稳妥点就加了几级缓存
《一个缓存使用案例》

  1. warmup,是一个本地java堆缓存,用来放预热数据。这部分数据被认为是极热的,从业务要做的活动跟历史的top品信息是可以获取这些数据的。同时,在大促的时候,所有商品都是禁写的,所以这里也不会有超时时间。大促一过,预案一调,这批数据立马作废,不会再被访问
  2. jvmlocal,同样也是本地java堆缓存,用来存放黑马热点,就是warmup遗漏的热点商品。这个是一个可调的有容量限制的,使用LRU来发现热点数据。同时,在我们的设计中,这部分数据是否过期也是有开关进行控制
  3. redis,也就是远程缓存了,这部分是有过期时间的,所以是需要注意穿透的

总结下,在我们的case中,只用了warmup跟redis,我们只有2w+个商品,全部加载到内存也就100M不到,对GC也不会有太大的影响。

如果把问题再复杂化点,比如总共有1000w的商品,其中有大量的热点商品完全超过了java内存的大小,那么可以考虑在每个服务器上搭一个redis,减少网络传输的开销
《一个缓存使用案例》

预热

在大促开始前需要将warmup缓存预热起来,不然就会用大量的请求穿透。在设计预热方案时,我们需要考虑的是预热时间。我们有2w个品,60台机器,如果每台机器单独预热的话,那就需要2w*60=120w个db请求,假设每秒放200个请求访问db,那么就需要120w/200=6000s,大概2个小时。
上面的计算貌似很有道理,其实是有问题的。因为一次db请求怎么也得分页查个200条数据吧,所以实际时间10分钟就够了。我们这个case太简单了,完全反应不出预热的复杂性。换一个更复杂的case,假设有1000w个商品,1000台机器,同时需要调用下游的rpc接口获取数据,下游系统的限流qps是10w,那么需要的时间是: 1000w*1000/10w=10ws=27.7小时
所以你就需要提早2天进行预热,中间还不能有错。如果提早2天,那么这两天产生的热点数据就没有机会预热了,而这部分数据价值可能更高点。当时我们对这种case也提过几个方案
1.如果用一台服务器进行预热,假设每台能用1000qps,那么需要3.5个小时左右。如果用50台分批预热的话只需要5分钟左右,然后再把这些数据dump出来,分发给每台机器,每台机器用dump文件来预热,那么估计30分钟就可以预热完毕
2.引入一个调度系统,业务系统提供两个rpc:

1.  第一个rpc用来获取缓存数据
2.  第二个rpc把缓存放到预热存储中

方案1简单粗暴,能够快速实施,方案2更像是一个通用型的预热系统

更新策略

在我们的case中,大促的时候是禁写的,而在日常情况下,也没有localcache,所以通常只有redis跟db之间的一致性的问题。在redis跟db之间更新策略,通常有写db失效缓存写db同时写缓存写db写缓存再binlog同步补偿等等。所有的这些模式,都是无法保证完全一致的,以目前常用的写db失效缓存为例
t1: 线程1访问缓存,没有命中,从db获取数据A
t2: 线程2更新db,然后失效缓存
t2: 线程1将A put到缓存。 A为老数据,数据不一致
它的问题在于读写并发可能会引起不一致,而写db同时写缓存不仅在读写并发,同时在写写并发的时候也可能存在问题。
但是我们还是采用了写db同时写缓存的方式,因为写db失效缓存会立刻引起穿透,而不一致的问题,还可以通过过期时间来解决,我们的case是可以容忍一小会的数据不一致的,并且我们认为这个发生的概率非常低
对于本地缓存虽然我们没有启用,但也是做了更新策略的,这部分除了发消息,也没啥好的办法,收到消息后,会直接失效本地缓存,因为消息可能乱序。

超时机制

超时主要为了保证redis跟db的数据一致性。首先在预热redis的时候,会给过期时间加一个随机数,为了防止同时失效造成大量的穿透。
设置过期时间,一个典型的问题的高并发下可能会有大量请求穿透到db,从而击跨db。解决方法要么加锁,要么直接放弃。我们采用直接放弃,让一个请求穿透到db,其他请求直接返回让服务降级前台不展示这部分数据。
但是,我们需要降低这种场景出现的概率,通常有两种情况会出现该问题:
1 缓存被失效,我们的更新采用直接写缓存方式,所以不存在该问题
2 缓存过期,为了缓解这个问题,引入了逻辑过期
写成伪码逻辑大概如下:

get(k):
  r = redis.get(k)
  if(r.rc == TIME_OUT)
      return null
  if(r.v == NOT_EXIST)
      return null
  if(is_logic_timeout(r.v))
      if(cas(flag(key), 1))
         v = db.get(k)
         reset_time(v)
         redis.set(k,v)
         flag(key) = 0
         return v
   return r.v

防穿

通常来说,有3种情况会出现穿透
1 过期,上面用来直接失效加逻辑过期来解决
2 null穿透,会再redis中保存可配数量的null值
3 缓存限流
对于缓存限流,通常我们使用了本地缓存后,可以有效的避免这个问题。但是,有些写热点本地缓存就没啥办法了。举一个不太恰当的例子,比如说要对一个接口做每秒20w的限流,简单点话,这个流控写入缓存服务器,然后过期时间是1s。但是每秒20w的qps操作缓存服务器,本身就会被缓存服务限流。那么可以这样做:
1 将20w分成N份,对于每次请求,随机选一个进行decr,如果超过,直接报qps流控限制,对就是这么暴力,总共代码不超过10行,基本够用
2 改良一下就是将这N份的key保存到缓存中,用n-key表示,对于每个请求,从n-key随机取一个,如果满了,从n-key中删除这个key,之后你可以直接返回被流控,或者再循环试几次

    原文作者:冰冻爱心小烧烤
    原文地址: https://www.jianshu.com/p/fb7abadf54e3
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞