缓存更新分析

问题来源

项目中为了加快响应时间以及减轻数据库压力,使用了redis缓存,同时缓存设置了过期时间(1天)

1
2
3
2个线程并发:一个读数据,一个写数据;
假使缓存过期,读缓存未命中,写数据还未开始,这个时候加载数据到缓存中(脏数据);然后,写数据开始。
结果导致,缓存失效前读到的数据都是脏数据,如何解决?

为什么要用缓存

一般数据都存放在关系型数据库中,以常用的MySQL数据库为例,正常情况下响应时间在10ms以内甚至更短;但是当数据上亿条,任何一款关系型数据库的响应时间都不可能控制在10ms以内

同时高并发情况下,比如同时来1万次请求,MySQL单库TPS(每秒事务量)大概只有1500左右,其它的请求只能处于等待状态,严重情况下数据库崩溃

使用缓存的场景

对于缓存来说,数据变更少且查询比较频繁是最好的场景,如果查询量不够大或者数据变动太频繁,缓存也就是失去了意义。

缓存类型

本地缓存

有些数据量不常变化,但是访问十分频繁,例如省、市地区数据。针对这种场景,可以将数据加载到应用的内存中,以提升系统的访问效率,减少数据库访问同时加快响应时间。

  • Guava Cache

比较常见的本地缓存有Guava Cache,使用方式如下:

1
2
3
4
5
6
7
8
9
10
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000) // 设置最大大小
.expireAfterWrite(10, TimeUnit.MINUTES) // 设置过期时间, 10分钟
.build(
new CacheLoader<String, String>() {
// 加载缓存内容
public String load(String key) throws Exception {
return getFromDB(key);
}
});
  • 本地缓存的缺点
    1. 数据保存在当前JVM中,无法共享
    2. 重启应用缓存丢失

分布式缓存

提到分布式缓存基本上都会说到 RedisRedis使用内存作为存储,所以性能上要比数据库要好很多,再加上Redis 还支持很多种数据结构,使用起来比较方便;但是,Redis需要通过网络来访问,所以网络的性能决定了 Reids 的瓶颈

缓存更新策略

更多策略见缓存更新的套路,这里具体分析喜马拉雅的缓存更新策略:先更新数据库,再失效缓存

Cache Aside Pattern

1
2
3
1. 失效:先从缓存中读取数据,没有得到,则从数据库中读取,成功后,放到缓存中。
2. 命中:从缓存中读取数据,取到后返回。
3. 更新:先把数据存到数据库中,成功后,再让缓存失效。

这种策略也会产生问题,比如:

1
2
3
4
两个并发操作,一个是更新操作,一个是查询操作;
读操作先到,没有命中缓存,然后就到数据库中读取。
这时来了一个写操作,写完数据库后,然缓存失效。
然后,之前的读操作再把旧数据写到缓存中,还是会造成脏数据。

但是,这种情况出现的概率非常低。这个场景需要发生在读缓存时缓存失效,并发着有一个写操作。而实际上数据库的写操作比读操作慢得多,而且还要锁表,而读操作需要在写操作之前进入数据库操作,又要在写操作完成后更新缓存,所有的这些条件都具备的概率并不大。所以,Cache Aside Pattern 还是相对靠谱的方式。

参考

缓存系统设计与更新机制

缓存更新的套路

热评文章