GO学习记录(4)
一、缓存一致性解决方案:删除缓存
严格来说,这个方案并没有解决缓存一致性的问题。
目前,高并发、高性能和强一致性解决方案,只有花钱买Oracle数据库。
删除缓存是指,先更新数据库,再删除缓存。
这种策略能够有效缓解一致性的问题。
如图,就是在极端情况下,它还是有一致性的问题。
但是这种极端情况比较少见,因为正常来说读 DB 到回写缓存是很快的,明显比写 DB 到删除 Key 快。
二、分页接口缓存第一页
分页接口是很不好设计缓存的。
但是有一种策略还比较好用:缓存第一页的结果。
这种缓存策略是建立在第一页会被频繁访问,因此缓存第一页效果会很明显。
这个策略可以叠加业务相关预加载,也就是将第一页的前几条数据的详情也放到缓存里面。
三、业务相关预加载
业务相关的预加载也就是在分页查询的时候,可以尝试把第一页的前N条的详细内容提前缓存起来。
这种是基于用户行为的业务相关的预加载。
简单来说,就是如果你可以在一个接口里面预期用户很快会访问下一个接口,那么你就可以提前将下一个接口的数据加载出来。
四、应用启动预加载
业务相关的预加载算是一种很巧妙的做法。在实践中更加常用的是应用启动的时候预加载缓存。
例如标签服务,正常启动时候预加载都是预加载本地缓存,如果是 Redis 缓存,那么不需要每个节点启动的时候都加载,只需要预加载一次就可以。
func (repo *CachedTagRepository) PreloadUerTags(ctx context.Context) error{
offset := 0
const batch = 100
for(...)
return nil
}
五、本地缓存 - Redis - DB 三级结构
在一些对性能要求比较苛刻的应用中,会考虑使用本地缓存 - Redis - DB 的三级结构。
要注意读写顺序:
- 读:先读本地缓存,再读 Redis,最后读 DB。
- 写:先写 DB,再写本地缓存,最后写 Redis。
注意写顺序,先写本地缓存,后写 Redis 是因为本地缓存操作几乎不可能失败。
六、本地缓存作为 Redis 缓存的备份
本地缓存的缺陷是命中率比较低,以及占用内存比较多,所以轻易不要使用本地缓存。
另外一种策略就是将本地缓存用作 Redis 缓存的备份。
也就是正常是走 Redis,再走 DB。
但是在 Redis 崩溃之后,可以先走本地缓存,再走 DB。
等 Redis 恢复了再切换回去。
七、优化策略——不缓存大对象
在使用缓存的时候,大对象往往会带来一些性能问题:
- 占用内存多
- 查询和写入的性能都很差
在使用 Redis 缓存的时候,这两个问题会更加严重,也就是会遇到所谓的大 key 问题。
因此一种策略就是不缓存大对象,如图所示,在写入缓存之前,检测一下对象的大小。对象大小超过阈值,则不会缓存。
如果没有统一缓存接口,那么这种检查基本上是在 Cache 这一层检查的。
而计算对象大小,一般是将对象序列化为 []byte 之后计算的。
若必要缓存大对象,可将大对象拆分为几部分。
八、优化策略——只缓存大对象
与前一个策略刚好相反的是,只缓存大对象。这种策略是基于这么一个假设
大对象的计算非常耗费资源,所以我们需要缓存下来。而小对象计算起来非常快,所以缓存与否都可以。
可以注意到,这个策略和前一个策略是完全相反的,这也就是体现了要具体问题具体分析。
九、缓存时间设置
有一个经典的问题就是:缓存的过期时间究竟应该是多长?
政治正确的说法就是:你预测接下来多久这个数据会被访问,就缓存多久。
举例来说:
- 在业务相关的预加载缓存里面,预加载的缓存的过期时间是很短的,因为如果预测成功,那么在秒级就会有人访问;如果预测失败,那么就要快速释放内存;
- 在榜单之类的数据里面,缓存时间可以设置非常长,甚至永不过期,即每次都用新榜单数据来覆盖了老榜单的数据;
最为极端的就是如果我们预期这个数据只会被读一次,那么就没必要缓存。