首页 > 数据库 >【Redis教程0x0C】数据库与缓存的一致性保证

【Redis教程0x0C】数据库与缓存的一致性保证

时间:2024-04-02 13:31:49浏览次数:29  
标签:缓存 请求 删除 数据库 Redis 更新 数据 0x0C

1. 引言

当我们在实现业务的过程中,如果发现服务器的性能瓶颈在数据库时,就要考虑加上Redis,让它作为数据库的缓存了。这样,客户端请求数据时,如果能在缓存命中,就不用去查数据库了,这大大减轻了数据库的压力,提高了服务器性能。
那么这里就产生了个问题,我们在数据更新的时候,既需要更新数据库,也需要更新缓存。那么怎么确定它们两者的先后顺序呢?

2. 先更新数据库,还是先更新缓存?

首先我们给出结论,无论是先更新哪个,后更新另一个,都会存在并发问题,导致缓存和数据库中数据不一致的现象。为什么呢?往下看!

2.1 先更新数据库,再更新缓存

举个例子,比如「请求 A 」和「请求 B 」两个请求,同时更新「同一条」数据,则可能出现这样的顺序:
image.png
A 请求先将数据库的数据更新为 1,然后在更新缓存前,请求 B 将数据库的数据更新为 2,紧接着也把缓存更新为 2,然后 A 请求更新缓存为 1。
此时,数据库中的数据是 2,而缓存中的数据却是 1,出现了缓存和数据库中的数据不一致的现象。

2.2 先更新缓存,再更新数据库

假设「请求 A 」和「请求 B 」两个请求,同时更新「同一条」数据,则可能出现这样的顺序:
image.png
A 请求先将缓存的数据更新为 1,然后在更新数据库前,B 请求来了, 将缓存的数据更新为 2,紧接着把数据库更新为 2,然后 A 请求将数据库的数据更新为 1。
此时,数据库中的数据是 1,而缓存中的数据却是 2,出现了缓存和数据库中的数据不一致的现象
既然两种更新方式都有问题,那也没有解决方法呢?
有是有,但势必影响了性能,我们后面再总结里会说。

3. 先更新数据库,还是先删除缓存?

既然无论先更新谁都会导致缓存和数据库的数据不一致,那么我们索性就不更新缓存了,直接把缓存中的旧数据删除,只更新数据库。这样一来,后续去读缓存发现没数据,再从数据库中读取数据,并更新到缓存中,保证数据一致性。

为什么选择删而不是更新呢?
删除一个数据,相比更新一个数据更加轻量级,出问题的概率更小。在实际业务中,缓存的数据可能不是直接来自数据库表,也许来自多张底层数据表的聚合。这时候去更新需要多表查询,开销太大了。
这也是一种Lazy Loading懒加载的思路,适用于加载代价大的操作。

这种策略叫做Cache Aside策略旁路缓存策略)。
该策略可以细分为读策略写策略
image.png
写策略

  1. 更新数据库中的数据;
  2. 删除缓存中的数据;

读策略

  1. 如果读取的数据命中缓存,则直接返回数据;
  2. 如果未命中缓存,则从数据库中读,然后回写到缓存,并且返回给用户;

对于写策略,我们不禁又有思考,是先更新后删,还是先删后更新呢?我们来分析一下!

3.1 先删除缓存,再更新数据库(写中读会导致不一致)

来看这样一个场景:
假设某个用户的年龄是 20,请求 A 要更新用户年龄为 21,所以它会删除缓存中的内容。这时,另一个请求 B 要读取这个用户的年龄,它查询缓存发现未命中后,会从数据库中读取到年龄为 20,并且写入到缓存中,然后请求 A 继续更改数据库,将用户的年龄更新为 21。
image.png
最终,该用户年龄在缓存中是 20(旧值),在数据库中是 21(新值),缓存和数据库的数据不一致。
可以看到,先删除缓存,再更新数据库,写中读(写操作的过程中有读操作)的时候,还是可能会出现缓存和数据库的数据不一致的问题。
那有没有解决方法呢?
有。叫做
延迟双删

伪代码:

#删除缓存
redis.delKey(X)
#更新数据库
db.update(X)
#睡眠
Thread.sleep(N)
#再删除缓存
redis.delKey(X)

加了个睡眠时间,主要是为了确保请求 A 在睡眠的时候,请求 B 能够在这这一段时间完成「从数据库读取数据,再把缺失的缓存写入缓存」的操作,然后请求 A 睡眠完,再删除缓存。
所以,请求 A 的睡眠时间就需要大于请求 B 「从数据库读取数据 + 写入缓存」的时间。
但是具体睡眠多久其实是个玄学,很难评估出来,所以这个方案也只是尽可能保证一致性而已,极端情况下,依然也会出现缓存不一致的现象。

3.2 先更新数据库,再删除缓存(读中写会导致不一致,但实际上很难发生!)

继续用「读 + 写」请求的并发的场景来分析。
假如某个用户数据在缓存中不存在,请求 A 读取数据时从数据库中查询到年龄为 20,在未写入缓存中时另一个请求 B 更新数据。它更新数据库中的年龄为 21,并且清空缓存。这时请求 A 把从数据库中读到的年龄为 20 的数据写入到缓存中。
image.png
最终,该用户年龄在缓存中是 20(旧值),在数据库中是 21(新值),缓存和数据库数据不一致。
从上面的理论上分析,先更新数据库,再删除缓存,在读中写(读操作的过程中写)也是会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高
为缓存的写入通常要远远快于数据库的写入
,所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况
而一旦请求 A 早于请求 B 删除缓存之前更新了缓存,那么接下来的请求就会因为缓存不命中而从数据库中重新读取数据,所以不会出现这种不一致的情况。
所以,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的
为了保证万无一失,我们还可以给缓存数据加上过期时间,这样就算在这期间存在缓存数据不一致,有过期时间兜底,最终也能达到一致。

4. 如何保证两个操作都能成功?

之前我们说到,要想保证数据一致性,最建议的方案就是先更新数据库,再删除缓存,并且给缓存数据加上过期时间。
但这仍可能存在这样一个问题:明明更新了数据,但是要过一段时间才生效。
为什么呢?
这是因为更新数据库和删除缓存是两个操作,如果删除缓存(第二步操作)的时候失败了,导致缓存中的数据是旧值,而数据库是新值。不过好在加了过期时间,这样在旧值过期后,还是能去数据库读到新值。
image.png
那我们应该怎样保证两步操作都成功呢?
有两种方法:

  1. 重试机制;
  2. 订阅MySQL binlog,再操作缓存。

4.1 重试机制

我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。

  • 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
  • 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。

举个例子,来说明重试机制的过程。
image.png

4.2 订阅MySQL binlog,再操作缓存

先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。
Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
下图是 Canal 的工作原理:
image.png
所以,如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。

5. 总结

首先明确一点,在一般业务场景中,最推荐的保证数据库和缓存一致性的方案还是先更新数据库,再删除缓存。并且给缓存设置一个过期时间。
当然,上面的方案在每次更新数据的时候,都会删缓存,这势必会影响缓存的命中率。所以如果在对缓存命中率很高的场景下,可以采用更新数据库+更新缓存的方案。当然这个方案会导致在两个线程并发更新时数据不一致的问题,为了避免,可以有以下两种方案:(提高了缓存命中率,但降低了性能)

  1. 在更新缓存前加个分布式锁,保证同一时间只运行一个请求更新缓存,会对性能造成影响;
  2. 在更新缓存时,设置一个较短的过期时间,这样即使缓存不一致,数据也会很快过期,对业务还是能接收的。

标签:缓存,请求,删除,数据库,Redis,更新,数据,0x0C
From: https://blog.csdn.net/weixin_70757494/article/details/137236268

相关文章

  • redis数据库
    1、redis数据库是什么?redis数据库是一个持久化缓存数据库,是一个高性能分布式的内存数据库,可以支持大量没有固定模式存储结构的数据。2、redis的特点?(1)开源免费(2)支持数据的持久化,将数据保存在磁盘当中,要使用的时候加载即可(3)redis支持key-value,以及set  zset  hash ......
  • Redis开源协议调整,我们怎么办?
    本文分享自华为云社区《Redis开源协议调整,我们怎么办?》,作者:华为云PaaS服务小智。2024年3月20日,Redis官方宣布,从Redis7.4版本开始,Redis将获得源可用许可证( RSALv2 )和服务器端公共许可证( SSPLv1 )的双重许可,时间点恰逢刚刚完成最新一轮融资,宣布的时机耐人寻味。 ......
  • 【Redisson】源码预读准备工作
    1 前言微服务常见的就是服务和服务之间的协同了,那么Redisson就是我们常用的一种协同工具了,所以想看看它的源码,只有了解它的原理,才能更好的正确使用它。2 准备工作既然要看是不是得先知道的它的源码地址呢?地址:Redisson源码有了源码,是不是还需要一份文档呢?没文档的话怎么......
  • 【Cache】将常用的“小表”缓存到Buffer Cache
    对于那些被经常以全表扫描访问获取数据的“小表”来说,为了提升性能可以考虑将这些表cache在BufferCache中。什么样的表可以称其为“小表”呢?例如经常被访问的参数表,此类表通常包含的数据量并不大,经常以全表扫描的访问形式对其进行访问。如果不强制将这些表cache在BufferCache中,......
  • 数据库:Redis数据库
    一、非关系型数据库1.什么是非关系型数据库非关系型数据库(Non-relationalDatabase)又称NoSQL数据库是一种不同于传统关系型数据库管理系统(RDBMS)的数据存储解决方案。NoSQL这个术语最初意味着"NotOnlySQL",强调的是这类数据库不完全依赖于SQL作为查询语言,并且通常不遵循关系......
  • Linux 安装 Redis (Docker)
    Linux安装Redisdockerpullredis由于容器内目录下没有redis.conf,导致/mydata/redis/conf/redis.conf认为是目录所以先创建配置文件mkdir-p/mydata/redis/conftouch/mydata/redis/conf/redis.confmkdir-p/mydata/redis/conf:创建一个目录/mydata/redis/conf,-p如......
  • 内存,寄存器,缓存,cache
    对上面这几个名词认识,但要说对他们的理解,不知道。在项目开发过程中,经常会遇到说什么从缓存读取数据,拷贝到内存,什么值存在寄存器中等等,但都是傻傻分不清。没有自己的理解。下面从volatile关键字引入本节的学习原文链接:https://blog.csdn.net/Goforyouqp/article/details/1313099......
  • 【NoSQL】SpringBoot+Redis简单使用
    【NoSQL】SpringBoot+Redis简单使用Redis是一款key-value存储结构的内存级NoSQL数据库;支持多种数据存储格式、支持持久化、支持集群windows下载:https://github.com/tporadowski/redis/releases<dependency><groupId>org.springframework.boot</groupId><artifactId......
  • 在.Net中操作redis
    在.Net中操作redis一、环境.Net7redis7.2.4二、所需类包StackExchange.Redis三、连接redis信息appsettings.json配置redis连接信息//Redis配置//"Redis":{//"Default":{//"Connection":"",//连接地址,端口号,密码//"Instance......
  • Higress 基于自定义插件访问 Redis
    作者:钰诚简介基于wasm机制,Higress提供了优秀的可扩展性,用户可以基于Go/C++/Rust编写wasm插件,自定义请求处理逻辑,满足用户的个性化需求,目前插件已经支持redis调用,使得用户能够编写有状态的插件,进一步提高了Higress的扩展能力。文档在插件中调用Redis[1]中提供了......