首页 > 数据库 >项目开发中 Redis 缓存和数据库一致性问题及解决方案

项目开发中 Redis 缓存和数据库一致性问题及解决方案

时间:2024-02-27 13:57:26浏览次数:20  
标签:缓存 删除 数据库 Redis 更新 一致性 数据

引入Redis缓存提高性能

如果公司的项目业务处于起步阶段,流量非常小,那无论是读请求还是写请求,直接操作数据库即可,这时架构模型是这样的:

但随着业务量的增长,你的项目业务请求量越来越大,这时如果每次都从数据库中读数据,那肯定会有性能问题。这个阶段通常的做法是,引入缓存来提高读性能,架构模型就变成了这样:

如何提高Redis缓存利用率?

想要缓存利用率最大化,我们很容易想到的方案是,缓存中只保留最近访问的热数据

具体步骤如下:

  • 写请求依旧只写数据库
  • 读请求先读缓存,如果缓存不存在,则从数据库读取,并重建缓存
  • 同时,写入缓存中的数据,都设置失效时间

缓存中不经常访问的数据,随着时间的推移,都会逐渐过期淘汰掉,最终缓存中保留的,都是经常被访问的热数据,缓存利用率得以最大化。

什么是Redis 缓存和数据库数据的一致性问题

Redis 缓存和数据库数据的一致性问题,就是 Redis 缓存的数据和数据库中保存的数据出现不相同的现象。导致这种现象的发生一般有两种情况:

  • 在 Redis 缓存和数据库进行数据同步时出现了异常,导致数据同步失败
  • 在高并发的情况下,有多个线程同时操作 Redis 缓存和数据库,导致数据不一致性

Redis 缓存和数据库数据的一致性问题解决方案

方案一:先更新Redis缓存,再更新数据库

这个方案一般不考虑。

原因是当数据同步时,更新 Redis 缓存成功,但更新数据库出现异常时,会导致 Redis 缓存数据与数据库数据完全不一致,而且这很难察觉,因为 Redis 缓存中的数据一直都存在。

方案二:先更新数据库,再更新Redis缓存

这种方案一般不考虑。

原因是当数据同步时,数据库更新成功,但 Redis 缓存更新失败,那么此时数据库中是最新值,Redis 缓存中是旧值。之后的应用系统的读请求读到的都是 Redis 缓存中旧数据。只有当 Redis 缓存数据失效后,才能从数据库中重新获得正确的值。

该方案还存在并发引发的一致性问题

假设同时有两个线程进行数据更新操作,如下:

流程如下:

  1. 线程1 更新了数据库
  2. 线程2 更新了数据库
  3. 线程2 更新了Redis缓存
  4. 线程1 更新了Redis缓存

线程1 虽然先于 线程2 发生,但 线程2 操作数据库和缓存的时间,却要比线程1 的时间短,执行时序发生错乱,最终这条数据结果是不符合预期的。如果是写多读少的场景,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。

方案三:先删除Redis缓存,后更新数据库

这种方案只是尽可能保证一致性而已,极端情况下,还是有可能发生数据不一致问题。

原因是当数据同步时,如果删除 Redis 缓存失败,更新数据库成功,那么此时数据库中是最新值,Redis 缓存中是旧值。之后的应用系统的读请求读到的都是 Redis 缓存中旧数据。只有当 Redis 缓存数据失效后,才能从数据库中重新获得正确的值。

该方案还存在并发引发的一致性问题

假设同时有两个线程进行数据更新操作,如下:

从上图可见,先删除 Redis 缓存,后更新数据库,当发生读/写并发时,还是存在数据不一致的情况。

如何解决呢?最简单的解决办法就是延时双删策略

步骤如下:

  1. 先淘汰 Redis 缓存

  2. 再写数据库

  3. 休眠 1 秒,再次淘汰 Redis 缓存 (这么做,可以将 1 秒内所造成的缓存脏数据,再次删除)

那么,这个 1 秒怎么确定的,具体该休眠多久呢?针对上面的情形,自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百 ms 即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的 Redis 缓存脏数据。

方案四:先更新数据库,后删除Redis缓存

实际使用中,建议采用这种方案。

这种方案其实一样也可能有失败的情况。

原因是当数据同步时,如果更新数据库成功,而删除 Redis 缓存失败,那么此时数据库中是最新值,Redis 缓存中是旧值。之后的应用系统的读请求读到的都是 Redis 缓存中旧数据。只有当 Redis 缓存数据失效后,才能从数据库中重新获得正确的值。

你有没有发现,这个问题其实在删除 Redis 缓存类的方案都是存在的,那么此时再读取缓存的时候每次都是错误的数据了。

此时解决方案有两个:

方案一:利用消息队列进行删除的补偿

步骤如下:

  1. 应用系统先对数据库进行更新操作
  2. 再对 Redis 进行删除操作的时候发现报错,删除失败
  3. 此时将 Redis 的 key 作为消息体发送到消息队列中
  4. 应用系统接收到消息队列发送的消息后
  5. 再次对 Redis 进行删除操作
方案二:订阅数据库变更日志,再操作缓存

具体来讲就是,我们的应用系统在修改数据时,只需修改数据库,无需操作 Redis 缓存。那什么时候操作缓存呢?这就和数据库的变更日志有关了。

拿 MySQL 举例,当一条数据发生修改时,MySQL 就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的 Redis 缓存。

该方案还存在并发引发的一致性问题

从上图可见,线程1 在做查询操作,刚好 Redis 缓存失效,然后从数据库获取数据,并写入 Redis 缓存中。这时,线程2 在做更新操作,先更新数据库,然后删除 Redis 缓存。由于线程1 查询数据库操作在线程2 更新数据库操作之前,所以导致获取的数据是数据库的旧值,而线程1 写入缓存操作由在线程2 删除缓存之后,导致写入到 Redis 缓存中的数据也是数据库的旧值。

其实出现以上情况的概率是非常低的,这是因为它必须满足 3 个条件,如下:

  1. Redis 缓存刚好已失效
  2. 读请求和写请求并发
  3. 更新数据库和删除 Redis 缓存的时间,要比读数据库和写 Redis 缓存时间短

因为写数据库一般会先加锁,所以写数据库,通常是要比读数据库的时间更长的。这么来看,先更新数据库,后删除 Redis 缓存的方案,是可以保证数据一致性的。

综上所述,想要保证数据库和 Redis 缓存一致性,推荐采用先更新数据库,再删除缓存方案,并配合消息队列或订阅变更日志的方式来做

总结

引入 Redis 缓存后,需要考虑 Redis 缓存和数据库一致性问题,可选的方案有:

  • 先更新数据库再更新 Redis 缓存

  • 先更新数据库在删除 Redis 缓存

先更新数据库再更新 Redis 缓存方案,在并发场景下无法保证缓存和数据一致性,且存在缓存资源浪费和系统性能浪费的情况发生。而在先更新数据库再删除 Redis 缓存的方案中,在并发场景下依旧有数据不一致问题,解决方案是延迟双删,但这个延迟时间很难评估,所以推荐用先更新数据库再删除 Redis 缓存的方案。

而在先更新数据库再删除 Redis 缓存方案下,为了保证两步都成功执行,需配合消息队列或订阅变更日志的方案来做,本质是通过重试的方式保证数据一致性。

在先更新数据库,再删除 Redis 缓存方案下,MySQL 的读写分离和主从库延迟也会导致缓存和数据库不一致,缓解此问题的方案是延迟双删,凭借经验发送延迟消息到队列中,延迟删除 Redis 缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率。

掌握缓存和数据库一致性问题,核心问题有 3 点:缓存利用率、并发、缓存和数据库同步问题

对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。

就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。

如果不能容忍缓存数据不一致,可以通过 Redission 实现 分布式读写锁保证并发读写或写写的时候按顺序排好队,而只读的时候相当于无锁。

标签:缓存,删除,数据库,Redis,更新,一致性,数据
From: https://www.cnblogs.com/binbingg/p/18026792

相关文章

  • 解析Spring中的循环依赖问题:初探三级缓存
    什么是循环依赖?这个情况很简单,即A对象依赖B对象,同时B对象也依赖A对象,让我们来简单看一下。//A依赖了BclassA{publicBb;}//B依赖了AclassB{publicAa;}这种循环依赖可能会引发问题吗?在没有考虑Spring框架的情况下,循环依赖并不会带来问题,因为对象之间相互依赖......
  • 使用python批量删除redis key
     比如我的业务。刚上线默认为超级管理员新增权限--请导出id用于清缓存svc格式请注意分页需要导出全部selectCONCAT('@rbac/ent/aclgr/',e.id)as需要清理缓存的rediskeyfroment_rbac_groupewherenotexists(selectp.`groupid`froment_rbac_group_permissionp......
  • Redis扩展功能
    Redis事务一次操作执行多条命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入、不许加塞。由于redis只能在执行前检查一组命令的语法错误,在命令执行时出现异常没法全体回滚,所以是弱一致性。multi+exec组合正常执行执行前队......
  • 将 Redis 数据放置在 Controller 层还是 Service 层
    在三层架构中,将Redis数据放置在Controller层还是Service层,同样需要根据具体的业务需求和设计原则来决定。以下是一些常见的考虑因素:数据访问频率:如果某个数据在多个请求之间频繁被读取或写入,可以考虑将其放置在Service层的缓存中,以减少对Redis的频繁操作。这样可以提高......
  • 【性能测试】Redis中的缓存雪崩、缓存击穿、缓存穿透问题详解
    一.什么是缓存雪崩当我们提到缓存系统中的问题,缓存雪崩是一个经常被讨论的话题。缓存雪崩是指在某一时刻发生大量的缓存失效,导致瞬间大量的请求直接打到了数据库,可能会导致数据库瞬间压力过大甚至宕机。尤其在高并发的系统中,这种情况会导致连锁反应,整个系统可能会崩溃。1.......
  • CentOS下安装redis 详细步骤
    1、前言Redis版本6.2.5服务器版本LinuxCentOS7.664位2、下载Redis下载网址:https://redis.io/download/笔者使用wget在线下载wgethttp://download.redis.io/releases/redis-6.2.5.tar.gz3、解压、安装3.1使用tar解压压缩包tar-xzvfredis-6.2.5.tar.gz3.2......
  • ELKF日志系统搭建(二)进阶——使用 Kafka 作为日志消息缓存
    说明:在一些比较大的业务使用场景中,因为应用繁多,需要收集的日志也很多,通过filebeat或者logstash收集上来的日志如果全都直接发送给ES,那么就会对ES集群产生一定的压力,为了避免出现日志接收不过来的问题,于是引入了消息队列作为缓存,比如常见的使用Redis或Kafka作为消息缓存......
  • redis自学(5)QuickList
    问题1:ZipList虽然节省内存,但申请内存必须是连续空间,如果内存占用较多,申请内存效率很低。怎么办?为了缓解这个问题,我们必须限制ZipList的长度和entry大小。问题2:但是我们要存储大量数据,超出了ZipList最佳的上限怎么办?我们可以创建多个ZipList来分片存储数据。问题3:数据拆分后比......
  • redis常见的五种类型
    https://www.cnblogs.com/xkqwy/p/16353029.html 总结1.string类型写命令通过set关键字实现,set[key][value]读命令通过get关键字实现,get[key]2.list列表类型通过rpush、lpush,将一个或多个值向右或向左推入。rpush[key][value1][value2],将value值推入到列表的右端......
  • Redis扩展数据类型&命令
    StreamRedisStream是Redis5.0版本引入的一种新的数据类型,它是一个持久化的、可查询的、可扩展的消息队列服务。Stream类型的数据结构类似于一个日志系统,数据被添加到Stream的末尾,并且每个数据都会被分配一个唯一的序列号,这个序列号是按照时间顺序递增的。主体队列:Stre......