缓存
为什么使用缓存
我们如果要优化某个接口的延迟,脑海中第一个想到的办法就是加缓存。缓存通过减少数据传输时间,减少了单个请求的响应延迟,提高了单位时间内能处理的请求数量。假设单线程进程中,某个接口的延迟为 10 ms,那么 TPS 为 100 次/秒。其中,读数据库花费了 3 ms。如果我们采用 Redis 缓存数据,将读取时间降到 1 ms,那么接口延迟为 8ms,TPS 为 125 次/秒。通过使用缓存,接口的吞吐量提高了 25%。
为什么缓存能减少数据传输时间呢?我们常用的缓存有两种:进程内缓存(例如 Map)和进程外缓存(例如 Redis)。
-
当使用进程内缓存时,时间消耗在内存 IO;
-
当使用进程外缓存时,时间消耗在网络 IO + 内存 IO;
-
当使用数据库时,如果数据不在内存中,还得去磁盘加载。内存 IO、网络IO 和 磁盘 IO 的延迟为:
| 操作 | 延迟 |
|---|---|
| 主存访问 | 100 ns |
| 同一个数据中心往返 | 500,000 ns |
| 磁盘寻址 | 2,000,000 ns (2 ms) |
从传输速度来说,内存 IO > 网络 IO + 内存 IO > 磁盘 IO,即进程内缓存 > 进程外缓存 > 数据库。
缓存的类型
有了缓存,读请求一般是这样执行的:
- 先查看缓存中是否有数据
- 如果有,则直接返回数据
- 否则,从后端数据库读取数据,并把数据写到缓存中。
根据缓存中的数据是否接受写请求,我们可以把缓存分为只读缓存和读写缓存。
只读缓存
只读缓存:所有的写请求,包括增、删、改,直接发往后端的数据库,同时删除缓存中的数据。
- 优点
- 最新的数据都在数据库中
- 数据已持久化,不会丢失
- 缺点
- 一个数据修改后就被读取,会产生缓存缺失,需要先去后端数据库读取数据,并写到缓冲中。也就是说,只读缓存不适合读写频繁的数据。
读写缓存
读写缓存:除了读请求,写请求也会发送到缓存中,直接修改缓存中的数据。
- 优点
- 写入速度非常快
- 缺点
- 最新数据在缓存中,一旦出现掉电或宕机,内存中的数据就会丢失。
最新新数据在缓存中,需要将其同步到数据库中,有两种写回策略:同步直写和异步写回。
- 同步直写:写请求同时发给缓存和数据库。只要两者都返回成功,才给客户端返回成功。
- 优点
- 即使缓存宕机,数据库中仍有最新的数据,保障了数据的可靠性。
- 缺点
- 增加了响应延迟。需要等到写入数据库成功,才能给客户端返回成功。
- 多线程中,容易引起缓存和数据库数据不一致的情况,并且保证一致性需要花费很大的代价。
- 优点
- 异步写回:写请求只需要发给缓存。在数据被淘汰时,将最新数据写到数据中。
- 优点
- 写入速度非常快。
- 缺点
- 宕机时很可能丢失很久之前的数据。一种折中的方法是,定期将脏数据写到数据库,即使宕机,也只是丢失一部分数据(取决于写入间隔)。
- 优点
同步直写优先考虑数据的可靠性,异步写回优先考虑写入性能。
适用场景
实际中,我们可以根据数据的特点,选择不同的缓存类型:
- 如果你的数据读多写少,那么选择只读缓存更合适。例如,某个活动的排行榜数据、直播列表。
- 如果你的数据读写频繁,或者对响应延迟要求高,那么选择读写缓存更合适。
- 如果对数据的可靠性要求高,写回策略可以采用同步直写。例如,玩家的金币数据。
- 如果对数据的读写性能要求高,写回策略可以采用异步写回。例如,某个活动的数据。