1、为什么需要缓存
一般在项目中,最消耗性能的地方就是后端服务的数据库了。而数据库的读写频率常常都是不均匀分布的,大多情况是读多写少,并且读操作还会有一些复杂的判断条件,比如 like
、group
、join
等等,这些语法是非常消耗性能的,所有会出现很多的慢查询,因此数据库很容易在读操作的环节遇到瓶颈。那么通过在数据库前面,前置一个缓存服务,就可以有效的吸收不均匀的请求,抵挡流量波峰。
另外,如果应用与数据源不在同一个服务器的情况下,中间还会有很多的网络消耗,也会对应用的响应速度有很大影响,如果当前应用对数据实时性的要求不那么强的话,在应用侧加上缓存就能很快速的提升效率。
2、缓存的类别
缓存又分进程内缓存和分布式缓存两种,而本文介绍的 Redis 属于分布式缓存。
当你的应用需要考虑 Redis 服务器宕机或者更快的缓存服务,那么进程缓存就是很好的选择,多级缓存能带来更高的性能和更高的可用性,当然也带来更高的复杂性。
3、缓存更新策略
3.1 先更新数据库,再删除缓存(推荐)
3.2 先删除缓存,再更新数据库
该方案会导致数据不一致,流程如下:
- 请求 A 进行写操作,删除缓存
- 请求 B 查询发现缓存不存在
- 请求 B 去数据库查询得到旧值
- 请求 B 将旧值写入缓存
- 请求 A 将新值写入数据库
上述问题可以采用延时双删的策略解决:
- 先删除缓存
- 再写数据库
- 延时再次删除缓存
方案 1 和 2 都存在的一个问题,缓存删除失败:
可以提供一个保障的重试机制解决这个问题,如将未删除成功的缓存添加进消息队列。
4、高并发产生的缓存问题
4.1 缓存穿透
缓存穿透指的是使用在数据库不存在的信息进行大量的高并发查询,这导致缓存无法命中,每次请求都要穿透到后端数据库系统进行查询,使数据库压力过大,甚至使数据库服务宕机
解决方案:
- 对于返回为
NULL
的依然缓存,对于抛出异常的返回不进行缓存 - 提供一个能迅速判断请求是否有效的拦截机制,比如利用布隆过滤器,内部维护一系列合法有效的 key ,迅速判断出请求所携带的 Key 是否合法有效。如果不合法,则直接返回
4.2 缓存击穿
对于某些热点数据设置了过期时间,如果某个 key 失效,可能大量的请求打过来,缓存未命中,然后去数据库访问,此时数据库访问量会急剧增加
解决方案:
- 分布式锁:加载数据的时候可以利用分布式锁锁住这个数据的 Key,在 Redis 中直接使用上文的操作。对于获取到这个锁的线程,查询数据库更新缓存,其他线程采取重试策略,这样数据库不会同时受到很多线程访问同一条数据
- 异步加载:由于缓存击穿是热点数据才会出现的问题,可以对这部分热点数据采取到期自动刷新的策略,而不是到期自动淘汰
4.3 缓存雪崩
缓存雪崩是指缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,大量请求直接访问数据库,数据库压力过大导致系统雪崩
解决方案:
- 对不同的数据使用不同的失效时间
- 多级缓存