Redis 核心技术与实战
蒋德钧
中科院计算所副研究员
25202 人已学习
立即订阅
登录后,你可以任选4讲全文学习
推荐试读
换一换
02 | 数据结构:快速的Redis有哪些慢操作?
04 | AOF日志:宕机了,Redis如何避免数据丢失?
17 | 为什么CPU结构也会影响Redis的性能?
课程目录
已完结/共 53 讲
开篇词 (1讲)
开篇词 | 这样学Redis,才能技高一筹
基础篇 (10讲)
01 | 基本架构:一个键值数据库包含什么?
02 | 数据结构:快速的Redis有哪些慢操作?
03 | 高性能IO模型:为什么单线程Redis能那么快?
04 | AOF日志:宕机了,Redis如何避免数据丢失?
05 | 内存快照:宕机后,Redis如何实现快速恢复?
06 | 数据同步:主从库如何实现数据一致?
07 | 哨兵机制:主库挂了,如何不间断服务?
08 | 哨兵集群:哨兵挂了,主从库还能切换吗?
09 | 切片集群:数据增多了,是该加内存还是加实例?
10 | 第1~9讲课后思考题答案及常见问题答疑
实践篇 (28讲)
11 | “万金油”的String,为什么不好用了?
12 | 有一亿个keys要统计,应该用哪种集合?
13 | GEO是什么?还可以定义新的数据类型吗?
14 | 如何在Redis中保存时间序列数据?
15 | 消息队列的考验:Redis有哪些解决方案?
16 | 异步机制:如何避免单线程模型的阻塞?
17 | 为什么CPU结构也会影响Redis的性能?
18 | 波动的响应延迟:如何应对变慢的Redis?(上)
19 | 波动的响应延迟:如何应对变慢的Redis?(下)
20 | 删除数据后,为什么内存占用率还是很高?
21 | 缓冲区:一个可能引发“惨案”的地方
22 | 第11~21讲课后思考题答案及常见问题答疑
23 | 旁路缓存:Redis是如何工作的?
24 | 替换策略:缓存满了怎么办?
25 | 缓存异常(上):如何解决缓存和数据库的数据不一致问题?
26 | 缓存异常(下):如何解决缓存雪崩、击穿、穿透难题?
27 | 缓存被污染了,该怎么办?
28 | Pika:如何基于SSD实现大容量Redis?
29 | 无锁的原子操作:Redis如何应对并发访问?
30 | 如何使用Redis实现分布式锁?
31 | 事务机制:Redis能实现ACID属性吗?
32 | Redis主从同步与故障切换,有哪些坑?
33 | 脑裂:一次奇怪的数据丢失
34 | 第23~33讲课后思考题答案及常见问题答疑
35 | Codis VS Redis Cluster:我该选择哪一个集群方案?
36 | Redis支撑秒杀场景的关键技术和实践都有哪些?
37 | 数据分布优化:如何应对数据倾斜?
38 | 通信开销:限制Redis Cluster规模的关键因素
期中测试 (2讲)
期中测试题 | 一套习题,测出你的掌握程度
期中测试题答案 | 这些问题,你都答对了吗?
未来篇 (3讲)
39 | Redis 6.0的新特性:多线程、客户端缓存与安全
40 | Redis的下一步:基于NVM内存的实践
41 | 第35~40讲课后思考题答案及常见问题答疑
加餐篇 (7讲)
加餐(一)| 经典的Redis学习资料有哪些?
加餐(二)| 用户Kaito:我是如何学习Redis的?
加餐(三)| 用户Kaito:我希望成为在压力中成长的人
加餐(四) | Redis客户端如何与服务器端交换命令和数据?
加餐(五) | Redis有哪些好用的运维工具?
加餐(六)| Redis的使用规范小建议
加餐(七) | 从微博的Redis实践中,我们可以学到哪些经验?
结束语 (2讲)
期末测试 | 这些Redis核心知识,你都掌握了吗?
结束语 | 从学习Redis到向Redis学习
Redis 核心技术与实战
15
15
1.0x
00:00/00:00
登录|注册
开通超级会员可免费学习本课程,还可解锁海量内容免费学特权。

25 | 缓存异常(上):如何解决缓存和数据库的数据不一致问题?

你好,我是蒋德钧。
在实际应用 Redis 缓存时,我们经常会遇到一些异常问题,概括来说有 4 个方面:缓存中的数据和数据库中的不一致;缓存雪崩;缓存击穿和缓存穿透。
只要我们使用 Redis 缓存,就必然会面对缓存和数据库间的一致性保证问题,这也算是 Redis 缓存应用中的“必答题”了。最重要的是,如果数据不一致,那么业务应用从缓存中读取的数据就不是最新数据,这会导致严重的错误。比如说,我们把电商商品的库存信息缓存在 Redis 中,如果库存信息不对,那么业务层下单操作就可能出错,这当然是不能接受的。所以,这节课我就重点和你聊聊这个问题。关于缓存雪崩、穿透和击穿等问题,我会在下一节课向你介绍。
接下来,我们就来看看,缓存和数据库之间的数据不一致是怎么引起的。

缓存和数据库的数据不一致是如何发生的?

首先,我们得清楚“数据的一致性”具体是啥意思。其实,这里的“一致性”包含了两种情况:
缓存中有数据,那么,缓存的数据值需要和数据库中的值相同;
缓存中本身没有数据,那么,数据库中的值必须是最新值。
不符合这两种情况的,就属于缓存和数据库的数据不一致问题了。不过,当缓存的读写模式不同时,缓存数据不一致的发生情况不一样,我们的应对方法也会有所不同,所以,我们先按照缓存读写模式,来分别了解下不同模式下的缓存不一致情况。我在第 23 讲中讲过,根据是否接收写请求,我们可以把缓存分成读写缓存和只读缓存。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/1000字
划线
笔记
复制
02 | 数据结构:快速的Redis有哪些慢操作?
04 | AOF日志:宕机了,Redis如何避免数据丢失?
17 | 为什么CPU结构也会影响Redis的性能?
24 | 替换策略:缓存满了怎么办?
40 | Redis的下一步:基于NVM内存的实践
41 | 第35~40讲课后思考题答案及常见问题答疑
开通超级会员免费畅看本课程
开通会员
该文章仅可免费阅读部分内容,如需阅读完整文章,请开通超级会员或单独购买本课程。
登录 后留言

精选留言(75)

  • Kaito
    数据在删改操作时,如果不是删除缓存值,而是直接更新缓存的值,你觉得和删除缓存值相比,有什么好处和不足?

    这种情况相当于把Redis当做读写缓存使用,删改操作同时操作数据库和缓存。

    1、先更新数据库,再更新缓存:如果更新数据库成功,但缓存更新失败,此时数据库中是最新值,但缓存中是旧值,后续的读请求会直接命中缓存,得到的是旧值。

    2、先更新缓存,再更新数据库:如果更新缓存成功,但数据库更新失败,此时缓存中是最新值,数据库中是旧值,后续读请求会直接命中缓存,但得到的是最新值,短期对业务影响不大。但是,一旦缓存过期或者满容后被淘汰,读请求就会从数据库中重新加载旧值到缓存中,之后的读请求会从缓存中得到旧值,对业务产生影响。

    同样地,针对这种其中一个操作可能失败的情况,也可以使用重试机制解决,把第二步操作放入到消息队列中,消费者从消息队列取出消息,再更新缓存或数据库,成功后把消息从消息队列删除,否则进行重试,以此达到数据库和缓存的最终一致。

    以上是没有并发请求的情况。如果存在并发读写,也会产生不一致,分为以下4种场景。

    1、先更新数据库,再更新缓存,写+读并发:线程A先更新数据库,之后线程B读取数据,此时线程B会命中缓存,读取到旧值,之后线程A更新缓存成功,后续的读请求会命中缓存得到最新值。这种场景下,线程A未更新完缓存之前,在这期间的读请求会短暂读到旧值,对业务短暂影响。

    2、先更新缓存,再更新数据库,写+读并发:线程A先更新缓存成功,之后线程B读取数据,此时线程B命中缓存,读取到最新值后返回,之后线程A更新数据库成功。这种场景下,虽然线程A还未更新完数据库,数据库会与缓存存在短暂不一致,但在这之前进来的读请求都能直接命中缓存,获取到最新值,所以对业务没影响。

    3、先更新数据库,再更新缓存,写+写并发:线程A和线程B同时更新同一条数据,更新数据库的顺序是先A后B,但更新缓存时顺序是先B后A,这会导致数据库和缓存的不一致。

    4、先更新缓存,再更新数据库,写+写并发:与场景3类似,线程A和线程B同时更新同一条数据,更新缓存的顺序是先A后B,但是更新数据库的顺序是先B后A,这也会导致数据库和缓存的不一致。

    场景1和2对业务影响较小,场景3和4会造成数据库和缓存不一致,影响较大。也就是说,在读写缓存模式下,写+读并发对业务的影响较小,而写+写并发时,会造成数据库和缓存的不一致。

    针对场景3和4的解决方案是,对于写请求,需要配合分布式锁使用。写请求进来时,针对同一个资源的修改操作,先加分布式锁,这样同一时间只允许一个线程去更新数据库和缓存,没有拿到锁的线程把操作放入到队列中,延时处理。用这种方式保证多个线程操作同一资源的顺序性,以此保证一致性。

    综上,使用读写缓存同时操作数据库和缓存时,因为其中一个操作失败导致不一致的问题,同样可以通过消息队列重试来解决。而在并发的场景下,读+写并发对业务没有影响或者影响较小,而写+写并发时需要配合分布式锁的使用,才能保证缓存和数据库的一致性。

    另外,读写缓存模式由于会同时更新数据库和缓存,优点是,缓存中一直会有数据,如果更新操作后会立即再次访问,可以直接命中缓存,能够降低读请求对于数据库的压力(没有了只读缓存的删除缓存导致缓存缺失和再加载的过程)。缺点是,如果更新后的数据,之后很少再被访问到,会导致缓存中保留的不是最热的数据,缓存利用率不高(只读缓存中保留的都是热数据),所以读写缓存比较适合用于读写相当的业务场景。
    2020-10-14
    36
    234
  • Alex_QY
    延时双删根本解决不了一致性问题,高迸发场景线程A根本不知道线程B,线程C的执行开始和结束的时间。
    所以sleep跟没sleep没啥区别。。。
    感觉唯一的办法就是要让整个事务方法由并行变成串行。
    感觉串行呢?要么借助分布式锁,要么借助MYSQL本身的update独占锁
    2020-10-29
    23
    76
  • ctang
    先删除缓存后更新数据库,数据库更新失败了,何来的旧值。只读缓存不是以数据库为准吗?
    2020-10-17
    16
    31
  • 约书亚
    完全没必要双删,双删比起先DB后删Redis,无非就是防止删除Redis操作失败。但高并发下依旧可能在第一次删期间混进来读操作。
    还有基于消息队列的方案,凭啥Redis操作能会失败,DB操作会失败,消息队列就不会了呢?就算能用事务消息,难道不怕旧值覆盖新值的情况发生么?
    对于课后题,这种被称为直接缓存,除了大家说的分布式锁方案保证并发下的正确,还可以考虑基于lua实现cas,有一定性能下降但大多数场景都还能接受。
    2021-02-01
    6
    20
  • Reborn 2.0
    不用第二种延迟双删, 直接用先DB后删redis不就好了, 延迟双删还要麻烦一点, 感觉没有什么优点啊
    2020-11-16
    3
    7
  • 不诉离殇
    老师分析问题的思路很赞,但是我对于结论有一个很大的疑问,按照分析思路 ,既然对于读写缓存同步写回策略可以采用(分布式)事务来保证原子性,那么只读缓存的更新数据库和删除缓存的操作为什么不能采用事务来保证原子性?二者的思路差异在哪里?
    2020-10-19
    3
    6
  • williamcai
    老师你好,用事务保证数据库和redis一致,不可行呀
    2020-10-16
    6
    6
  • 与路同飞
    如果业务层要求必须读取一致的数据,那么,我们就需要在更新数据库时,先在 Redis 缓存客户端暂存并发读请求,等数据库更新完,再读取数据,从而保证数据一致性。这个redis客户端暂存并发读请求咋弄
    2020-10-14
    10
    6
  • 范闲
    更新步骤:
    1.先更新缓存,再更新数据库
    2.先更新数据库,再更新缓存

    并发:
    1.读+写
    2.写+写
    3.读+读
    在并发情况下,3是肯定没有影响的。1和2肯定会对数据一致性有影响。这个时候可以利用分布式锁来处理。同一时刻一个key只有一把锁持有。
    2020-10-20
    4
  • liudu_ec
    蒋老师,问个问题,我现在是用缓存失效+ binlog 订阅更新缓存的,日常使用场景中,查询请求先查缓存,如果没有 就查数据库,数据库没有就会构建一条空缓存来保护数据库,
    还有一种场景是使用pipeline批量查询缓存,如果存在某个key不存在的情况,应该怎么优雅处理?
    2020-10-18
    1
    3
  • 石小
    老师好,给缓存做持久化,更新数据时只更新到缓存,之后由专门的进城进行刷到数据库;读取数据时也是从缓存读,读不到再从数据库读。这种方式有什么问题吗?
    2020-11-17
    2
  • 吃饭睡觉打酱油
    先更新数据库,再删除缓存这种,如果是缓存正好过期,来了一个读请求已经读到数据了,但是还没写入缓存,此时再来个并发更新数据库的操作,我们的写入缓存操作又在删除缓存之后,那最终,缓存的数据不就是旧数据了么?
    2021-04-27
    2
    1
  • escray
    我觉得专栏先定义了缓存数据一致性,这个是很有必要的,一种是缓存中有数据,且和数据库中一样;一种是缓存中没数据,数据库中是最新值。

    在学习专栏之前,感觉只读缓存的数据一致性要好于读写缓存,而两种缓存模式如果想要保持一致性的话,都需要采用原子性的事务处理。

    但是按照专栏的说法,其实是说读写缓存的同步写回策略可以保证缓存和数据库中的数据一致。

    其实缓存一致性问题和其他的分布式一致性问题从本质上来看似乎是一样的。

    对于课后题,只读缓存数据删改操作,如果在缓存中不是删除,而是更新,那么首先更新应该比删除慢,而且更新之后的数据其实不一定会在短时间内用得到。可能有个别的业务场景,比较适合缓存更新,但是更普遍的应该还是缓存删除。
    2021-03-29
    1
  • Mine
    给缓存设置过期时间,即使更新完数据库数据之后,删除缓存失败,缓存在过期时间到了失效之后重新拉取最新的数据保证最终的一致性。
    2021-02-04
    1
  • Geek_1e8830
    先删除缓存,在更新数据库失败,然后读取到旧值这不应该是正常的吗?既然数据库都更新失败了,那理所当然接下来获取到的肯定是旧值赛,没问题个人觉得这里。
    2021-02-02
    1
  • Henry
    好处:查询能立马命中缓存,请求不会打到数据库。
    不足:假设请求1,2 分别更新,请求1 数据库将a改为b,请求2数据库将b改为c,请求2更新缓存,请求1更新缓存,造成数据不一致。
    2020-11-24
    1
  • 曾轼麟
    老是Redis的LFU是做了优化的,访问次数会随着时间而递减,在updateLFU() ->LFUDecrAndReturn()里面

    作者回复: 是的,如果短时间内有频繁访问造成了过高的访问次数,这些数据也容易被滞留在缓存中,LFU会衰减访问次数,避免这些数据造成缓存污染。

    2020-10-30
    1
  • 慎独明强
    总结一下:1.先删除缓存再更新或删除数据库,如果删除数据库失败,其他线程读取到数据库的旧值,我觉得不应该叫旧值,都没有更新成功,数据库事务应该要回滚,这次业务应该算失败。对业务没有影响。 2.先删除缓存再更新数据库的并发场景,需要延迟双删。延迟这个时间不好控制,也会出现早删除,也会造成数据不一致。 3.先更新数据库,再删除缓存,需要保持原子性,可以采用队列来保持最终一致性。 4.先更新数据库,后删除缓存,会出现并发读,数据库已经更新但缓存更新,可以采用将读写操作写到队列,队列来保证操作的有序性,需要考虑性能。 5.对于有并发读写请求,有两个思路,分布式锁来限制并发。或者采用redis读写,mysql只做备份,保持最终一致性。这需要考虑redis的高可用。
    2020-10-17
    1
  • Dovelol
    “应用要把数据 X 的值从 10 更新为 3,先在 Redis 缓存中删除了 X 的缓存值,但是更新数据库却失败了。如果此时有其他并发的请求访问 X,会发现 Redis 中缓存缺失,紧接着,请求就会访问数据库,读到的却是旧值 10”。老师好,这一段更新数据库失败说明数据库的值就是10,缓存删除了再从数据库读到的值就是10,这个怎么能说是旧值呢?这个流程就相当于是缓存被删了,数据库没修改那对数据一致性没有印象吧?
    2020-10-14
    1
  • 那时刻
    请问老师,在采用基于消息队列的重试机制来解决数据不一致问题时,在数据删除或更新,我们就要把这些值从消息队列中去除。如果数据已经成功的删除或者更新,但是在从消息队列删除过程失败,导致已经处理的消息依然在消息队列中,这种情况怎么处理呢?

    我目前想到的是通过增加消息版本号来实现幂等操作。不知道有木有其它方法?
    2020-10-14
    2
    1
收起评论
75
返回
顶部