1. 为什么要用缓存?
用缓存,主要有两个用途:高性能、高并发。
高性能
- 假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作 MySQL,半天查出来一个结果,耗时 600ms。但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用户。那么此时咋办?
- 缓存啊,折腾 600ms 查出来的结果,扔缓存里,一个 Key 对应一个 Value,下次再有人查,别走 MySQL 折腾 600ms 了,直接从缓存里,通过一个 Key 查出来一个 Value,2ms 搞定。性能提升 300 倍。
- 就是说对于一些需要复杂操作耗时查出来的结果,且确定后面不怎么变化,但是有很多读请求,那么直接将查询出来的结果放在缓存中,后面直接读缓存就好。
高并发
- MySQL这么重的数据库,压根儿设计不是让你玩儿高并发的,虽然也可以玩儿,但是天然支持不好。MySQL 单机支撑到2000QPS(每秒查询率)也开始容易报警了。
- 所以要是你有个系统,高峰期一秒钟过来的请求有 1 万,那一个 mysql 单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放 mysql。缓存功能简单,说白了就是Key-Value式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发 so easy。单机承载并发量是 MySQL单机的几十倍。
缓存是走内存的,内存天然就支撑高并发。
2. 用了缓存之后会有什么不良后果?
常见的缓存问题有以下几个:
- 缓存与数据库双写不一致
- 缓存血崩、缓存穿透、缓存击穿
- 缓存并发竞争
缓存血崩、缓存穿透、缓存击穿知识铺垫:
缓存雪崩(Cache Avalanche)
- 对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。
这就是缓存雪崩。
缓存穿透(Cache Penetration)
- 对于系统 A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。
- 黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
- 举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
缓存击穿(Hotspot Invalid)
- 缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
- 解决方式也很简单,可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。
3. 为啥Redis单线程模型也能效率这么高?
- 纯内存操作
- 核心是基于非阻塞的IO多路复用机制
- C语言实现,一般来说,C语言实现的程序"距离"操作系统更新,执行速度相对会更快
- 单线程反而避免了多线程的频繁上下文切换的问题,预防了多线程可能产生的竞争问题。
4. redis 的并发竞争问题是什么?如何解决这个问题?了解 redis 事务的 CAS 方案吗?
- 某个时刻,多个系统实例都去更新某个 key。
- 可以基于 zookeeper 实现分布式锁。每个系统通过 zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 key,别人都不允许读和写。
你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。
每次要写之前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。