缓存实战

缓存更新方式

不少研发同窗是这么用缓存的:在查询数据的时候,先去缓存中查询,若是命中缓存那就直接返回数据。若是没有命中,那就去数据库中查询,获得查询结果以后把数据写入缓存,而后返回。在更新数据的时候,先去更新数据库中的表,若是更新成功,再去更新缓存中的数据。流程以下图
image.png
这样使用缓存的方式有没有问题?绝大多数状况下都没问题。可是,在并发的状况下,有必定的几率会出现“脏数据”问题,缓存中的数据可能会被错误地更新成了旧数据。好比1,对同一条记录,同时产生了一个读请求和一个写请求,这两个请求被分配到两个不一样的线程并行执行,读线程尝试读缓存没命中,去数据库读到了数据,这时候可能另一个写线程抢先更新了缓存,在处理写请求的线程中,前后更新了数据和缓存,而后,拿着旧数据的第一个读线程又把缓存更新成了旧数据(几率低)。好比2两个线程对同一个条订单数据并发写,也有可能形成缓存中的“脏数据”(几率高)
一、故咱们常用Cache Aside 模式,它们处理读请求的逻辑是彻底同样的,惟一的一个小差异就是,Cache Aside 模式在更新数据的时候,并不去尝试更新缓存,而是去删除缓存。流程以下:
image.png
这种方式能够解决如上例子2中的脏数据的问题。在写策略中,可否先删除缓存,后更新数据库呢?答案是不行的,由于这样会大大提升如上事例1出现的几率。另外咱们通常会配合添加一个比较短的过时时间,即便示例1的状况出现了,也只有比较短期的脏数据。
但也要学会依状况而变。好比说新注册用户,按照这个更新策略,要写数据库,而后清理缓存。可当注册完用户后,当使用读写分离时,会出现由于主从延迟因此读不到用户信息的状况(一致性要求比较高的话,写后读在必定时间阈值里面通常去master读,此时就不会有这个问题,我会在一致性浅谈的文章里介绍)。而解决这个问题的办法偏偏是在插入新数据到数据库以后写入缓存,这样后续的读请求就会从缓存中读到数据了,由于是新注册的用户,因此不会出现并发更新状况。
二、另外一种常用的策略是模拟MySQL的从机,经过订阅binlog的方式更新缓存,此时MySQL必须设置为row格式。通常流程图以下:
image.png数据库

缓存穿透

若是咱们的缓存命中率比较低,就会出现大量“缓存穿透”的状况。缓存穿透指的是,在读数据的时候,没有命中缓存,请求“穿透”了缓存,直接访问后端数据库的状况。少许的缓存穿透是正常的,咱们须要预防的是,短期内大量的请求没法命中缓存,请求穿透到数据库,致使数据库繁忙,请求超时。大量的请求超时还会引起更多的重试请求,更多的重试请求让数据库更加繁忙,这样恶性循环最终致使系统雪崩。
一、当系统初始化的时候,好比说系统升级重启或者是缓存刚上线,这个时候缓存是空的,若是大量的请求直接打过来,很容易引起大量缓存穿透致使雪崩。为了不这种状况,能够采用灰度发布的方式,先接入少许请求,再逐步增长系统的请求数量,直到所有请求都切换完成。若是系统不能采用灰度发布的方式,那就须要在系统启动的时候对缓存进行预热:在系统初始化阶段,接收外部请求以前,先把最常常访问的数据填充到缓存里面,这样大量请求打过来的时候,就不会出现大量的缓存穿透了。
二、当有大量的请求访问不存在的数据时,好比在券商系统的用户表中,咱们须要经过用户 ID 查询用户的信息。若是要读取一个用户表中未注册的用户,按照这个策略,咱们会先读缓存再穿透读数据库。因为用户并不存在,因此缓存和数据库中都没有查询到数据,所以也就不会向缓存中回种数据,这样当再次请求这个用户数据的时候仍是会再次穿透到数据库。在这种场景下缓存并不能有效地阻挡请求穿透到数据库上,它的做用就微乎其微了。通常来讲咱们会有两种解决方案:回种空值以及使用布隆过滤器
第一种解决方案回种空值。当咱们从数据库中查询到空值或者发生异常时,咱们能够向缓存中回种一个空值。可是由于空值并非准确的业务数据,而且会占用缓存的空间,因此咱们会给这个空值加一个比较短的过时时间,让空值在短期以内可以快速过时淘汰。回种空值虽然可以阻挡大量穿透的请求,但若是有大量获取未注册用户信息的请求,缓存内就会有有大量的空值缓存,也就会浪费缓存的存储空间,若是缓存空间被占满了,还会剔除掉一些已经被缓存的用户信息反而会形成缓存命中率的降低。因此这个方案,在使用的时候应该评估一下缓存容量是否可以支撑。
第二种解决方案布隆过滤器。布隆过滤器有一个特色是:布隆过滤器若是返回不存在的那么必定是不存在的,可是若是返回存在,未必存在。若是布隆过滤器的屡次hash函数选择的比较合理,空间预估的比较合理,那边布隆过滤器返回存在,可是不存在的几率是很小的。故咱们可使用这一特性。如新注册的用户除了须要写入到数据库中以外,同时更新用户ID到布隆过滤器。那么当咱们须要查询某一个用户的信息时,先查询这个 ID 在布隆过滤器中是否存在,若是不存在就直接返回空值,而不须要继续查询数据库和缓存,这样就能够极大地减小异常查询带来的缓存穿透。
三、热点KEY问题,按照上文介绍的缓存更新方式(缓存+过时时间),当前KEY是一个热点KEY,有大量的并发请求而且重建缓存不能再很短期内完成。那么在缓存失效的瞬间,有大量请求来重建缓存,形成后端负载加大,甚至雪崩。这个问题的根本缘由是有大量的请求访问了后端存储,故咱们能够从减小访问后端请求的角度解决问题:
第一种方法是互斥锁方案:此方法只容许同一时刻只有一个线程更新缓存,具体的是在更新的时候申请互斥锁,获取到锁的线程更新缓存,其余线程等待更新完成。这种方法思路比较简单,可是可能存在死锁的风险,而且线程池可能会堵塞。
第二种方法是永远不过时:从缓存层面,不设置过时时间,从而不会出现热点KEY过时后产生的问题;从功能层面,为每一个value设置逻辑过时时间,当发现超过逻辑过时时间后使用单独的线程重建缓存。逻辑过时时间增长了代码复杂度和内存成本。
四、大量KEY同时访问的问题,按照上文介绍的缓存更新方式(缓存+过时时间),当有大量的KEY同时访问,那么他们的过时时间也是同样的,这个会致使不少缓存项同时过时,从而可能致使缓存的机器资源占用高(缓存在同一时间淘汰大量的缓存项),另外下次大量并发请求过来的时,就须要重建大量的缓存,从而致使缓存穿透甚至雪崩。解决办法也很简单,就是更新缓存的时候添加一个随机的过时时间(缓存+过时时间+随机时间)后端

相关文章
相关标签/搜索