为什么要做缓存
- 加速访问
大部分应用核心业务是围绕着数据来的,如果出现数据库访问速度的瓶颈,通常依靠数据库本身的做法是增加索引、分表。
如果选择增加索引,会带来写入速度慢、增加死锁风险的问题;如果选择分表,则会增加业务系统的复杂性,而且对于跨表统计查询等操作来说是雪上加霜;
除了上面的两种方式,访问数据前如果可以在内存中得到结果,那么就不去访问数据库,毫无疑问,这快得多!
- 保护数据库
传统数据库承压能力是非常差的,这是因为访问数据库中的数据会带来磁盘io(除非你使用索引覆盖查询),如果大量的请求堆积在数据库访问上,数据库或者我们的应用可能直接无法提供服务。
缓存的出现可以让我们避开磁盘操作,直接在内存中得到数据,数据库的功能就变成了持久化和为缓存提供数据。
缓存存在的问题
- 需要占用内存
并非所有的数据都适合缓存,即使它是数据库中的。
数据库中的大字段,如果数据库中的某张表存在一个较大的列,那就得考虑是不是要缓存它了,或者缓存时额外的过滤掉这些列,大字段往往不需要频繁的查询。
访问不那么频繁的数据,如果数据的访问数据库足以支撑,没必要加一层缓存,如果统计压力非常大,应该做的是预统计或者分表。
- 数据一致性
保证缓存中的数据和数据库中的一致是缓存设计最优先考虑的内容。相对于正确性速度往往没有那么重要,因为访问缓存的数据是非常快的,额外的做一些工作保证一致性是非常必要的。
- 雪崩问题
如果使用懒加载的缓存,在某一个时刻大量的请求用到了未加载的数据,会导致压力给到数据库;
如果为了保险起见,我们给缓存增加一个过期时间,过期时间过于密集的话,大量缓存同时过期的时刻就会去访问数据库刷新这些缓存,压力又打到了数据库;
- 穿透问题
访问缓存中不存在的数据将会访问数据库加载缓存,但是如果数据库中依然不存在呢?那就会每次都去数据库中确认这条数据不存在,这样的请求如果是密集的,也会把压力给到数据库;
缓存的设计
- 一个小数据库表
直接整张表都放到缓存中即可,这种表往往是不怎么更新的,做好缓存一致性每次写操作刷新全部缓存即可。
写操作增加事务,事务开启前把缓存标记为过时。
设计队列(可以直接使用公平锁来实现),先入先出,避免数据库读的同时写操作事务未提交造成过期数据加载到缓存;同时也保证多个读写请求的顺序正确;
- 写不频繁,读频繁
最经典的缓存使用场景。
同样的使用事务、队列来保证数据的正确性;只不过这种场景往往本身应用是分布式的,要把队列替换成分布式队列或者分布式锁(redisson是个不错的选择);如果采用队列要充分考虑队列的性能是否足够,容量是否足够,比如说来自同一个服务的大量的请求堆积在队列中出现了请求倾斜,就得考虑是不是负债均衡有问题,考虑负债均衡算法的合理性;还可以通过其他方式优化,比如读请求入队前检查队列中如果不存在写请求,直接查就可以了不用入队;
解决雪崩问题,把缓存的有效期加盐,避免大量缓存同一时间过期;还可以通过热数据预热来避免该场景的出现;
解决穿透问题,如果数据不存在,也给它缓存下来,但是过期时间设置的要小一些,避免被攻击爆内存;或者是前置检查,比如缓存中存储最大的id和最小的id,如果要查询的数据不在这个范围内,显然是没有的;
- 写频繁,读不频繁
不建议使用缓存,代价太大,大量的cpu、内存资源消耗在维护缓存上。
- 写频繁、读频繁
这种数据真的存在吗,如果真的存在,考虑不使用传统数据库,直接上内存数据库是不是更好些,如果实时性要求不高,大数据量的情况下考虑使用hbase这种库。