通常我们不希望所有数据的请求都去查询数据库,这一方面是慢,另一方面对数据库的压力也大.
因此,类似硬件层面的缓存,我们在应用层也会使用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攻击),
某个热点数据在某个时间点过期,但这时有大量的请求打过来,由于缓存缺失,全部穿透到数据库
为什么会过期? 这里先假设过期时间是不实时更新的,过期时间仅在第一次读时设置
SET if Not eXists
这就是类似一个cas(compare and swap
)原子指令,我们知道击穿的痛点在于突然大量对同一数据的请求达到数据库,那么我们可以不让这些请求同时达到数据库,由于是对同一数据的请求,我们可以在缓存层面设计,当第一个请求没查到数据时,用SETNX新建一个与key唯一相关的key_mutex键,置1,这样后续的请求如果未读到缓存,再去读这个key_mutex,读到了1,就会自旋或阻塞或睡眠,一般让其睡眠50ms就好,或者将key_mutex的value设成一个semaphore,让线程在这个信号量上阻塞,但很显然,调度的时间应该超过50ms,毕竟有大量的请求,没必要!
针对过期时间的一定优化,但无法解决对可写的热点数据的缓存穿透
一个数据被写后,通常是先写数据库,再delete缓存,这会导致缓存中数据不存在而击穿,用setnx可以很好解决
只要我们每读一次数据,就用原子指令atomic.Add()增加过期时间,那么缓存击穿的概率也大大降低
我们不再每次都更新缓存的过期时间,而是在value字段内置一个fake过期时间,这个fake过期时间比缓存的过期时间早,当我们每读一次数据,都要检查一下fake过期时间是否过期,如果过期,就更新缓存
如果给热点数据,设置一个较长的过期时间,就能防止其失效了
多个缓存在同一时间全部失效
给不同的资源设置不同的随机的过期时间,防止一起失效