Skip to content

Latest commit

 

History

History
84 lines (44 loc) · 4.36 KB

[cache] base concepts.md

File metadata and controls

84 lines (44 loc) · 4.36 KB

应用层缓存

通常我们不希望所有数据的请求都去查询数据库,这一方面是慢,另一方面对数据库的压力也大.

因此,类似硬件层面的缓存,我们在应用层也会使用in-memory cache

通常,我们使用redis,或mongoDB,memcached

缓存虽好,但也面临着一些问题,比如缓存穿透,缓存击穿,缓存雪崩

缓存穿透

如果某些请求一直查询一些不存在的数据,那么将会大幅提高缓存缺失率,这些请求将会去数据库查询,增加数据库压力,但是由于这些数据是不存在的,所以这是一次无用功,平白无故的增加了数据库压力,我们需要对其优化

一般的, cpu缓存命中率可达90%以上

所谓穿透,就是指缓存缺失了,请求穿透了缓存达到数据库

白名单: 布隆过滤器

布隆过滤器可以用于检索一个元素是否在一个集合中。布隆过滤器存储空间和插入/查询时间都是常数 O(K)

布隆过滤器是基础结构是一个较大的bitmap

  • 插入: 对输入,通过K个散列函数,映射到k个bit上,置为1
  • 查询: 对输入,通过k个散列函数,映射到k个bit上,若这k个bit均为1,则判定为存在,否则判定为不存在

它使用了多个hash函数,这是与普通的哈希表的差别,目的是为了降低hash冲突,如果一个hash函数的冲突率是0.5,那k个hash函数的冲突率就是(0.5)^k,很可观

但显然,这无法保证不发生冲突. 而且,其他键的hash值和自己的的hash值是存在交集的,所以这导致了布隆过滤器的特性: 存在误判率(只会误判为存在)

一般的缓存就是一个动态hash表

如果误判,则必定是认为数据存在,然后去查找缓存,查找数据库,这是可以忍受的,因为这种误判不会使得服务提供差错,而只是增加延时

如果是将存在的误判为不存在,则千万不可使用,否则就相当于拒绝服务了

我们倾向于有损服务,而不是不服务

黑名单: 缓存空结果

除了在缓存前,再前置一个过滤器,还可以当在数据库中查询到空结果时,重新设置缓存(read allocate).

但是一般的,这个缓存的过期时间要比较短,毕竟你无法知道这个数据是否真的马上被创建了

但显然,这种方法有着很大的缺陷,它相当于每查询一次设置一个黑名单,我们无法阻止恶意的用户持续的进行查询不存在数据的攻击(这也算一种dos攻击),

缓存击穿

某个热点数据在某个时间点过期,但这时有大量的请求打过来,由于缓存缺失,全部穿透到数据库

为什么会过期? 这里先假设过期时间是不实时更新的,过期时间仅在第一次读时设置

SETNX

SET if Not eXists

这就是类似一个cas(compare and swap)原子指令,我们知道击穿的痛点在于突然大量对同一数据的请求达到数据库,那么我们可以不让这些请求同时达到数据库,由于是对同一数据的请求,我们可以在缓存层面设计,当第一个请求没查到数据时,用SETNX新建一个与key唯一相关的key_mutex键,置1,这样后续的请求如果未读到缓存,再去读这个key_mutex,读到了1,就会自旋或阻塞或睡眠,一般让其睡眠50ms就好,或者将key_mutex的value设成一个semaphore,让线程在这个信号量上阻塞,但很显然,调度的时间应该超过50ms,毕竟有大量的请求,没必要!

过期时间策略

针对过期时间的一定优化,但无法解决对可写的热点数据的缓存穿透

一个数据被写后,通常是先写数据库,再delete缓存,这会导致缓存中数据不存在而击穿,用setnx可以很好解决

更新过期时间1,每次

只要我们每读一次数据,就用原子指令atomic.Add()增加过期时间,那么缓存击穿的概率也大大降低

更新过期时间2,阈值

我们不再每次都更新缓存的过期时间,而是在value字段内置一个fake过期时间,这个fake过期时间比缓存的过期时间早,当我们每读一次数据,都要检查一下fake过期时间是否过期,如果过期,就更新缓存

热点不过期

如果给热点数据,设置一个较长的过期时间,就能防止其失效了

缓存雪崩

多个缓存在同一时间全部失效

随机过期时间

给不同的资源设置不同的随机的过期时间,防止一起失效