数据库和缓存(比如:redis)双写数据一致性问题,在高并发场景下,尤其值得注意。
下文主要讲述缓存Redis使用的常见方案,以及解决数据一致性的最优方案。
常见使用方案
通常情况下,我们使用缓存的主要目的是为了提升查询的性能。大多数情况下,我们是这样使用缓存的:
- 用户请求过来之后,先查缓存有没有数据,如果有则直接返回。
- 如果缓存没数据,再继续查数据库。
- 如果数据库有数据,则将查询出来的数据,放入缓存中,然后返回该数据。
- 如果数据库也没数据,则直接返回空。
这是缓存非常常见的用法。一眼看上去,好像没有啥问题。
但你忽略了一个非常重要的细节:如果数据库中的某条数据,放入缓存之后,又立马被更新了,那么该如何更新缓存呢?
不更新缓存行不行?
答:当然不行,如果不更新缓存,在很长的一段时间内(决定于缓存的过期时间),用户请求从缓存中获取到的都可能是旧值,而非数据库的最新值。这不是有数据不一致的问题?
保证数据一致性:先写数据库,再删缓存
在高并发的场景中,有一个读数据请求,有一个写数据请求,更新过程如下:
- 请求 e 先写数据库,由于网络原因卡顿了一下,没有来得及删除缓存。
- 请求 f 查询缓存,发现缓存中有数据,直接返回该数据。
- 请求 e 删除缓存。
在这个过程中,只有请求 f 读了一次旧数据,后来旧数据被请求 e 及时删除了,看起来问题不大。
但如果是读数据请求先过来呢?
- 请求 f 查询缓存,发现缓存中有数据,直接返回该数据。
- 请求 e 先写数据库。
- 请求 e 删除缓存。
这种情况看起来也没问题呀?
答:对的。
但就怕出现下面这种情况,即缓存自己失效了。如下图所示:
- 缓存过期时间到了,自动失效。
- 请求 f 查询缓存,发缓存中没有数据,查询数据库的旧值,但由于网络原因卡顿了,没有来得及更新缓存。
- 请求 e 先写数据库,接着删除了缓存。
- 请求 f 更新旧值到缓存中。
这时,缓存和数据库的数据同样出现不一致的情况了。
但这种情况还是比较少的,需要同时满足以下条件才可以:
- 缓存刚好自动失效。
- 请求 f 从数据库查出旧值,更新缓存的耗时,比请求 e 写数据库,并且删除缓存的还长。
我们都知道查询数据库的速度,一般比写数据库要快,更何况写完数据库,还要删除缓存。所以绝大多数情况下,写数据请求比读数据情况耗时更长。
由此可见,系统同时满足上述两个条件的概率非常小。
推荐使用先写数据库,再删缓存的方案,虽说不能 100% 避免数据不一致问题,但出现该问题的概率,相对于其他方案来说是最小的。
删除缓存失败重试机制
异步重试,避免使用同步重试
- 每次都单独起一个线程,该线程专门做重试的工作。但如果在高并发的场景下,可能会创建太多的线程,导致系统 OOM 问题,不太建议使用。
- 将重试的任务交给线程池处理,但如果服务器重启,部分数据可能会丢失。
- 将重试数据写表,然后使用 elastic-job 等定时任务进行重试。
- 将重试的请求写入 mq 等消息中间件中,在 mq 的 consumer 中处理。
- 订阅 mysql 的 binlog,在订阅者中,如果发现了更新数据请求,则删除相应的缓存。
推荐在MQ消费端实现重试。