Redis的缓存如何和数据库中的数据保持一致性?
我们都知道,Redis是一个基于内存的键值存储系统,数据完全存放在内存中,这使得它的读写速度远超传统的硬盘存储数据库。对于高访问频率、低修改率的数据,通过将它们缓存在Redis中,应用可以快速地从内存中获取数据,能够大量减少数据库的查询次数,特别是在高并发场景下,有效避免了数据库可能成为整个系统的瓶颈问题。
???? 但是在实际开发情景中,我们如何才能保证Redis缓存中的数据和数据库中数据的一致性呢?
什么情况会出现数据的不一致?
MySQL和Redis的操作包括读操作和写操作两种,那么有哪些情况是会导致二者数据不一致的?我们先从读和写的操作流程看起~
使用Redis读取数据的场景
????使用Redis读取数据的场景
- 客户端发起一个查询数据的接口;
- 向Redis中查询是否存在该条数据
- 如果存在则直接返回;
- 不存在则向数据库中查询,再将数据库中的数据保存在Redis中,然后设置一个过期时间,之后再返回客户端。
???? 设置过期时间的主要作用就是为了保证一些冷数据过期自动淘汰,不至于一直存在Redis中占用空间
使用Redis写数据的场景
????对数据进行修改是分为两个步骤:
- 修改数据库数据
- 修改/删除Redis缓存
为什么不推荐更新缓存而是直接删除?
删除逻辑非常简单,只需要在下一个线程的时候查询Redis,查询不到再从数据库中加载即可,其副作用只是增加了一次chache miss;而更新缓存的成本更高,因为我们写入数据库的值,很多情况下并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么每次写入数据库后,都要再次计算写入缓存的值,无疑是浪费性能,显然直接删除缓存更为合适,推荐直接删除。
????那么接下来的问题就是先操作缓存还是先操作数据库了
先操作缓存
????先操作缓存即先将缓存清空,再对数据库数据进行修改
- 线程一进行删除Redis缓存和修改数据库数据操作,线程二进行查询数据操作
- 线程一将Redis缓存清空之后发生了网络延迟,此时线程二进行缓存读取
- 线程二在Redis缓存中查询不到数据,向数据库中查询数据并将查询到的数据加载到Redis缓存中,此时Redis缓存中存储的还是原先的老数据
- 线程一完成数据的修改操作
- 此时数据库中的数据是修改后的新数据,而Redis缓存中的数据还是更新前的旧数据,此时就发生了Redis缓存和数据库中的数据不一致的情况
???? 如果先操作Redis缓存的话,就可能出现将Redis中的数据删除之后旧数据又被放入其中去,如果后期再有查询操作,那么在Redis缓存中查询到的数据就会全部是旧的数据,产生了缓存脏读,直到数据到了过期时间被清除掉,如果没有设置过期时间该数据就永远都是脏数据,不能保证Redis和数据库的一致性
先操作数据库
那我们先操作数据库呢?还会有这种问题的发生吗?
????先进行数据库操作流程
- 线程一进行删除Redis缓存和修改数据库数据操作,线程二进行查询数据操作
- 线程一首先对数据库中的数据进行修改
- 在此期间如果出现了另外一个线程来查询数据,就会直接从Redis中读取旧数据返回
- 线程一操作完数据库后对Redis缓存进行延迟删除
- 接下来的查询操作在Redis中查询不到数据,会向数据库中查询,并将查询到的数据放入Redis缓存中,此时Redis和数据库中的数据都是修改后的新数据
???? 先删数据库再删缓存,在多线程的情况下,当一个线程删除数据库,另一个线程读取缓存数据,读到的是未修改的Redis缓存中的旧数据,当先前一个线程删除完数据库后就会更新缓存,此时Redis和数据库中的数据就可以一致了,只会产生一次脏读
如何保证数据的一致性
????此时我们可以看到,在只读场景中是不会出现数据不一致的情况的,只有在并发的写操作中才会出现。问题已经很清楚了,那么如何解决呢?
强一致性和弱一致性
????首先我们先来了解两个概念:强一致性和弱一致性:
- 强一致性:任何一次读都能读到某个数据的最近一次写的数据;
- 弱一致性:数据更新后,如果能容忍后续的访问,只能访问到部分或全部访问不到;
延时双删策略
如果我们先操作Redis再操作数据库的话,就会出现我们上面所说到的脏读的问题,要解决这个问题,我们可以使用延迟双删的策略:在更新MySQL之后,等待一个短时间(例如几秒)再尝试第二次删除,防止在这段时间内有请求重建缓存而造成数据不一致。
基于消息队列
使用消息队列(如RabbitMQ、Kafka等)异步处理数据更新,更新MySQL的同时将更新事件发送到队列,消费者收到消息后负责更新Redis缓存。
订阅MySQL binlog同步:
Redis可以订阅MySQL的binlog日志,拿MySQL举例,当一条数据发生变更时,MySQL就会产生一条变更日志,我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据去删除对应的缓存。
删除重试机制
使用删除重试机制,保证删除缓存成功。比如重试三次,三次都失败则记录日志到数据库并发送警告让人工介入。在高并发的场景下,重试最好使用异步方式,比如发送消息到mq中间件,实现异步解耦。
????以上的这些方法最多只能实现弱一致性????,也就是不能保证更新后每次读取到的数据都是最新数据,如果我们想要保证每次查询到的数据都是新的数据的话,即保证数据的强一致性,那就必须要保证他们的操作是原子性的,那么只能加一把锁了。但是加锁的话是会影响到系统的吞吐量,而我们使用Redis就是为了提高性能,如果为了保证数据的强一致性而加一把锁,那么就得不偿失了。
总结
- 数据不一致的情况在只读场景中是不会出现的,只有在并发的写操作中才会出现;
- 相对于直接删除Redis缓存来讲,修改缓存的成本更高,因此更加推荐修改数据库后直接删除Redis缓存,在下一次查询操作时再向数据库中拿到数据保存在Redis中;
- 先操作Redis的话为了保证和数据库数据的最终一致性,需要进行延时双删的操作,即对Redis缓存进行两次删除,相比于先操作MySQL来说多进行一次删除Redis的操作;
- 无论是先操作数据库还是先操作缓存,都会出现脏读的情况,因为redis和数据库不是原子性的,要想保证其原子性那么只有加锁,但是加锁的话又会影响性能;
- Redis删除失败要进行重删操作