缓存使用总结

迷彩 11天前 ⋅ 44 阅读

缓存使用总结

缓存类型

  • 1、localcache
  • 2、memcache
  • 3、redis

区别对比

缓存类型使用场景使用示例优点缺点
localcache少量数据,对应用程序只读或读多写少后台配置,分区信息无需网络开销,访问速度最快集群机器数据不同步
memcache海量数据,高并发读写评论内容,账号信息内存占用相对redis少,适合大键存储数据结构单一,不支持备份及持久化,只支持客户端操作
redis海量数据,高并发读写评论id索引,收藏视频信息数据结构丰富,支持备份及持久化,支持服务器操作相对memcache内存效率低
  • localcache 适用于存储少量数据及对应用程序只读或读多写少的场景,例如后台黑白名单、推广信息等,因为应用程序对这些数据几乎只是只读的,数据的修改主要发生在后台管理员更新配置时,且这些数据量很少,完全可以存储在本地内存当中。应用程序只需要定期从数据库load数据进行更新即可。对于分布式集群的部署,每台机器独自维护一份localcache,单后台数据有变动时,不同机器不可能同时load更新,因此存在集群机器数据不一致的情况。 但是这种情况通常是在可接受范围内的。

  • memcache 适用于存储大量高并发读写的数据,减轻数据库访问压力。如果没有memcache缓存,所有的访问直接打到db,高并发情况下将立马把数据库打挂,由于是直接存储在内存当中,因此访问速度将大大降低,同时数据缓存在memcache集群当中,可以确保应用集群访问数据的一致性,而不会存在localcache当中的问题。由于memcache不支持持久化,一旦集群机器出现宕机,将导致所有数据丢失,但是memcache本身就不是为了持久化数据而存在的,所以这也不是一个问题,需要注意的是,一旦memcache出现宕机等情况需要服务重启时,需要对缓存进行预热,不然大量miss同样也会打挂数据库。

  • redis 同样也是为了应对高并发读写而存在的。和memcache一样也是k-v类型,但是redis支持更丰富的数据结构,list,set,sortset,hashes。由于redis数据不是完全存在内存当中,当redis内存耗尽时,长期不使用的value将被转移到磁盘,因此redis可以存储比自身内存大的数据。同时redis支持持久化及master-slave模式数据备份。重启时可以再次加载磁盘的数据到内存当中。redis还具有容灾模式,只需要开启aof,即使服务器宕机也可以通过aof文件进行数据恢复。是否使用持久化及开启aof要根据具体业务场景进行选择。

缓存更新逻辑

cache then db or db then cache ?

  • 对于cache和db的操作顺序,网上一直存在不同的观点,到底是先更新数据库再淘汰缓存,还是先淘汰缓存再更新数据库呢。我们的做法是先更新数据库在淘汰缓存。

  • 首先先分析一下两者可能导致的最差情况(不考虑db和cache其中一个操作失败的情况)。

  1. 先淘汰缓存在更新数据库。对于这种做法,如果淘汰了缓存,此时刚好来了一个请求,由于缓存已经被淘汰,新来的请求将从数据库读取信息重新load到缓存,如果先前的写操作还没完成,读操作读到的将是旧的数据,此时重新load到缓存的数据将是脏数据并且后续的请求将都读到脏数据。
  2. 先更新数据库在淘汰缓存。更新db的时候刚好有请求到达,此时请求读到的将是脏数据。db更新后将淘汰旧的缓存,后续的请求读到的将是新的数据。
  • 在不考虑cache和db操作失败的情况下,显然先更新db再更新cache更合适。那如果考虑其中一个更新失败了呢?
  1. 先更新(淘汰)cache,后续db操作失败。如果是更新缓存,db操作失败将导致缓存中的数据是脏数据。如果是淘汰缓存,db更新失败会导致一次cache miss。
  2. 先更新了db,后续cache操作失败,此时将会导致cache中的数据是脏数据。
  • 在考虑了更新失败的情况下,分布式服务中并没有完美的解决方案,方法需要自己根据业务进行权衡,除非你愿意牺牲性能使用事务强一致性

update cache or delete cache ?

  • 写操作发生时,是更新缓存还是删除缓存呢。facebook在 Scaling Memcache at Facebook中使用的策略是**更新数据库后删除memcache缓存。**为什么是删除缓存而不是更新缓存呢?假设并发写更新完数据库后同时去 更新缓存。此时两个写操作可能都从缓存中取到了数据A,此时将导致并发写导致脏数据。

  • 并发写操作导致脏数据的情况是因为必须先从memcache中取出数据修改完在写回。但是在redis中,由于redis支持服务器操作,INCE,HINCR等操作都可以直接在服务器当中操作完成,对于这类操作不存在并发写的问题,因此可以选择更新db后更新缓存。直接update将减少一次cache miss。

  • 在我们的大多数业务场景中,我们使用的是更新db后更新缓存。虽然更新缓存可能导致并发写脏数据。但是由于我们使用了kafka消息队列,并发写操作经过kafka后是可以转化为顺序写的。比如对于评论。同一个视频下面 可能有多条并发写评论,视频收到评论后需要更新视频的评论数信息,如果直接更新缓存有很大的几率导致并发写脏数据,但是我们使用视频aid作为key,通过kafka消息队列异步处理,这样对于同一个视频的写操作都会 发送到同一个kafka分区,同时对于consumer来说,同一个视频的写操作都会由同一个consumer消费,通过消息队列异步化处理,即可把并发写转化为顺序写,此时更新缓存就不会存在写竞争。

  • 到底delete还是update,还是得自己根据业务场景进行权衡。

缓存常见问题

缓存穿透

  • 缓存穿透指的是访问一个不存在的数据一定会发生miss穿透到数据库,但是从db中查询也查找不到数据不写入缓存,导致下一次查询还是会继续cache miss。解决方法是从db中查询不到数据时,在缓存中设置一个空对象 或者不存在的标志。这个方法可以解决穿透的问题,但是代价就是牺牲加大的缓存在保存空对象数据。

  • 在评论系统中,用户获取评论列表的时候需要查询用户对这些评论是否点赞。但是点赞是相对冷门的操作,对于绝大多数用户根本不存在点赞相关数据,因此每次用户访问都会导致请求穿透到DB,针对这种情况,我们进行 标志存储。原先是采用set来保存用户点赞过的评论的id,当不存在点赞信息时,我们就初始化一个set,并在set中设置唯一member-1.这样通过牺牲少量的内存就可解决穿透问题。对于置顶评论也是同样的问题。

雪崩问题

  • 雪崩问题指的是缓存服务挂掉了导致所有请求全部到达db,瞬间打挂服务。

  • 针对雪崩问题,解决方法就是部署高可用集群,对服务进行降级及限流。

热点数据过期

  • 当热点数据过期时,可能所有的请求会同时cache miss然后同时去请求数据库导致数据库压力骤增。

  • 可以使用分布式锁来解决,当第一个请求miss获取锁后,其他请求全部阻塞直到第一个请求重新从db load数据后才解除,可以防止大量请求直接穿透到db。

redis使用

  • 业务中,redis的使用占绝大部分,且相对于memcache,由于丰富的数据结构,redis的使用也相对比较复杂。因此着重讲讲redis使用的一些注意事项

redis数据类型选择

Use hashes when possible

  • redis的内存效率是比较低的,尤其是使用k-v的时候,在http://redis.io/topics/memory-optimization这这篇博客中明确说明。能使用hashes的地方尽量使用hashes,在使用k-v结构的时候,每一个value都是一个 是一个redisObject,当使用hashes的时候,redis使用的压缩列表和字典两种存储方式,使用压缩列表的时候,由于压缩列表内存的优化,将大大节省内存空间。 redisObject typedef struct redisObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; int refcount; void *ptr; } robj;

  • 例如视频下评论信息,可以使用 aid-rpid =》 replyinfo,即aid-rpid作为key,replyinfo作为value,也可以使用aid =》rpid-replyifo,即aid作为key,value是hashes,hashes的key是rpid,value是replyinfo。经过测试,在同样100W个key的情况下使用后一种方法可以比第一种节约大量的内存。同时,第二种方法还可以通过HGET aid 获取指定评论,也可以HGETALL aid获取所有评论。第一种则只能先 KEYS aid*获取rpid在去查询info。

sortset

  • sortset的实现采用了跳表和字典两种数据结构,跳表保证rank及范围查询时O(logN)的复杂度,字典则使得score查询时能达到O(1)的时间复杂度. 使用sortset时需要注意的一点是sortset的大小,由于sortset是插入排序,如果sortset里数据量太大,可能导致插入排序速度太慢。

set

  • set底层使用的其实是dict,其实就是一个value为null的hashes。需要注意的是,在某些场景下,可以使用二进制数组来替代set节省内存空间,通过GETBIT,SETBIT设置member判断member是否存在。

redis底层图解

  • 下图大致概括了redis常用的数据类型的底层实现。 缓存使用总结 01.png

redis expire

  • 不同于memcache必须在set key的时候指定expire time,redis可以在set的时候指定expiretime,也可以在使用途中在设置expiretime。
  • 那么何时设置expire呢,是在set数据前之前还是set数据之后呢。在业务中曾经犯过这样一个错误。用户查询收藏夹信息的时候会先查询缓存,如果缓存miss则从db中加载。收藏夹信息在redis中是以sortset方式存储的。score表示收藏时间。当用户添加收藏的时候,就把新的数据ZADD进去,然后expire 增加过期时间。粗看是没什么问题,但是这个时候忽略了一个问题,就是zadd前缓存过期了,下次查询的时候由于zadd数据存在将不会miss导致读到的只是最新zadd的数据!
  • 因此在执行类似zadd sadd hset等操作的时候,一定要先进行expire,如果miss则不执行,等到查询miss的时候在从db中load

hashes 拆分

  • 是否有必要对hashes进行拆分?答案是肯定的。原因如下:
  1. 减小hashes粒度,增加查询效率。
  2. 分散存储空间,redis当中,hashes的k-v只会保存在一个节点,如果所有的数据全部存在一个hashes,将导致节点数据不均衡。相反,把hashes进行拆分,每个hashes保存的只是部分数据,不同的hashes也会被分配到集群的不同节点。均衡集群的内存负载。

全部评论: 0

    我有话说: