缓存的使用并不是零成本的,任何系统使用缓存,都会遇到两大问题:
第一,数据不一致问题;
第二,系统复杂度大幅度增加;
如何设计缓存:
缓存哪些情况用:
1.读多写少的场景:Tag分类统计,网站右侧栏,RSS。
2.缓存给系统带来巨大瓶颈的IO操作,在普通应用里尤其指由top SQL 或者慢 SQL 所带来的DAO查询。
3.记录文章点击数,秒杀系统;利用对象缓存不一致性,每次页面展示时,只更新缓存对象,页面优先读取缓存但不更新数据库,让缓存不一致,积累n次后,直接更新一次数据库,但绕过缓存过期操作。
数据发生变化,怎么保证数据一致性:
1.(淘汰缓存)数据同步数据库的时候,将缓存失效即可,如果业务对数据实时性要求不高,可以直接设置缓存失效时间,而不需要去手动失效它,这可以让代码达到最简的地步;坏处是当缓存失效瞬间,所有的请求都会经过数据库,可能导致数据库压力过大,导致缓存一直加不上,可能会引发DB故障。
2.(更新缓存)在缓存的数据更新的同时也触发程序更新缓存。可以在很大程度上避免上述所说的缓存失效雪崩效应;坏处是由于并发的原因,存在极小几率你更新的缓存会导致脏数据进入缓存中(就是线程1读取后还没来得及更新,线程2给更新了,这时候,后写入的将会覆盖前面的,从而导致数据丢失)三种解决办法:
2.1.锁控制;这种方式一般在客户端实现(在服务端加锁是另外一种情况),其基本原理就是使用读写锁,即任何进程要调用写方法时,先要获取一个排他锁,阻塞住所有的其他访问,等自己完全修改完后才能释放;如果遇到其他进程也正在修改或读取数据,那么则需要等待;锁控制虽然是一种方案,但是很少有真的这样去做的,其缺点显而易见,其并发性只存在于读操作之间,只要有写操作存在,就只能串行。
2.2.版本控制;这种方式也有两种实现,一种是单版本机制,即为每份数据保存一个版本号,当缓存数据写入时,需要传入这个版本号,然后服务端将传入的版本号和数据当前的版本号进行比对,如果大于当前版本,则成功写入,否则返回失败;这样解决方式比较简单;但是增加了高并发下客户端的写失败概率;
2.3.就是多版本机制,即存储系统为每个数据保存多份,每份都有自己的版本号,互不冲突,然后通过一定的策略来定期合并,再或者就是交由客户端自己去选择读取哪个版本的数据。很多分布式缓存一般会使用单版本机制,而很多NoSQL则使用后者。
3.有些时候缓存的更新不一定能够成功,也有可能会有脏数据进入缓存,如果要确保数据‘绝对’一致性,我们可以采取适当的补偿机制,如 定时从数据库的值更新到缓存,或者在更新缓存失败时,插入失败日志,定时重新执行缓存更新等
4.有些查询对象的写远大于读,采用定时程序 Reload或Rebuilt所有的缓存;
更新数据时,是先操作数据库还是先操作缓存?
1.先更新数据库,再淘汰缓存
2.先淘汰缓存,再更新数据库
原则是谁先做对业务的影响最小,就采用谁。
淘汰缓存后,写db前,在主从的数据库结构中可能有别的请求进来把缓存又个更新了,这怎么办呢?
使用双刷缓存策略
缓存穿透:(查询一个一定不存在的数据)一般网站经常会缓存用户搜索的结果,如果数据库查询不到,是不会做缓存的。但如果频繁查这个空关键字,会导致每次请求都直接查询数据库了,就失去了缓存的意义,在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决办法1:如果把查询不到的空结果,也给缓存起来,这样下次同样的请求就可以直接返回null了,即可以避免当查询的值为空时引起的缓存穿透,过期时间会很短,最长不超过五分钟。
解决办法2:可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放行给后面的正常缓存处理逻辑。
布隆过滤器拦截器
缓存雪崩:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,,就出现了缓存失效(过期)到新缓存未到这个期间。这个时间段,所有请求都走数据库,而对数据库CPU和内存造成巨大压力,前端连接数不够、查询阻塞,DB瞬时压力过重雪崩。
解决方法:大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存击穿:一种热点问题(比如某一并发很大的热点新闻),就是对某一有过期时间的key,缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决办法1:互斥锁:此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
解决办法2:永远不过期。 “永远不过期”包含两层意思:从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期;从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
缓存淘汰算法:LRU算法