【Redis】什么是缓存与数据库双写不一致?怎么解决?

news2024/11/23 16:39:48

1. 热点缓存重建

我们以热点缓存 key 重建来一步步引出什么是缓存与数据库双写不一致,及其解决办法。

1.1 什么是热点缓存重建

在实际开发中,开发人员使用 “缓存 + 过期时间” 的策略来实现加速数据读写和内存使用率,这种策略能满足大多数业务场景。但还是会有一些问题:

  1. 当前 key 是一个热点 key(某时间管理大师登顶微博热搜第一),并发量非常大;

  2. 在缓存失效瞬间,重建缓存不能在短时间完成(可能是一个负责业务场景,需要经过复杂的计算、多次IO、多次服务之间调用等等),有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

下面一起看一下热点缓存重建场景下的解决方案

1.2 基于 DCL(double check lock) 双重检测锁解决热点缓存并发重建问题

synchronized(this) {
    productStr = redisUtil.get(productCacheKey);
    if (!StringUtils.isEmpty(productStr)) {
        if (EMPTY_CACHE.equals(productStr)) {
            redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
            return new Product();
        }
        product = JSON.parseObject(productStr, Product.class);
        // 读延期
        redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS); 
        return product;
    }

    product = productDao.get(productId);
    if (product != null) {
        redisUtil.set(productCacheKey, JSON.toJSONString(product),
                genProductCacheTimeout(), TimeUnit.SECONDS);
        productMap.put(productCacheKey, product);
    } else {
        redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
    }
}

DCL 存在的问题:

  • synchronized 只在单节点内部有效,多节点会在每个web服务上缓存重建一次
  • this 是单例的,比如同时有 101、102 两个商品需要热点重建,101 先请求,synchronized(this) 会把 102 阻塞,可以 synchronized(每个商品)

解决办法:分布式锁解决热点缓存并发重建问题

1.3 分布式锁解决热点缓存并发重建问题

RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
hotCacheLock.lock();
try {
    productStr = redisUtil.get(productCacheKey);
    if (!StringUtils.isEmpty(productStr)) {
        if (EMPTY_CACHE.equals(productStr)) {
            redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
            return new Product();
        }
        product = JSON.parseObject(productStr, Product.class);
        // 读延期
        redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS); 
        return product;
    }

    product = productDao.get(productId);
    if (product != null) {
        redisUtil.set(productCacheKey, JSON.toJSONString(product),
                genProductCacheTimeout(), TimeUnit.SECONDS);
        productMap.put(productCacheKey, product);
    } else {
        redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
    }
} finally {
    hotCacheLock.unlock();
}

问题:缓存与数据库双写不一致

2. 缓存与数据库双写不一致

2.1 Cache Aside Pattern

Cache Aside Pattern 是最经典的 “缓存 + 数据库” 读写的模式。包括 Facebook 的论文《Scaling Memcache at Facebook》也使用了这个策略。

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

  • 命中:应用程序从cache中取数据,取到后返回。

  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

在这里插入图片描述
在这里插入图片描述

为什么不是写完数据库后更新缓存?而是删除缓存?

可以看一下Quora上的这个问答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是怕两个并发的写操作导致脏数据

是不是 Cache Aside 这个就不会有并发问题了?

不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。

这个问题理论上会出现,不过,实际上出现的概率可能非常低。

因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

所以,这也就是Quora上的那个答案里说的,要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook使用了这个降低概率的玩法,因为2PC太慢,而Paxos太复杂。当然,最好还是为缓存设置上过期时间。

2.2 缓存与数据库双写不一致

2.2.1 数据不一样场景

(1)双写不一致情况

线程1 在写数据库与更新缓存之间卡顿了一下,然后 线程2线程1 卡顿的这个空隙去写了数据库并刷新了缓存,然后 线程2 都已经执行完了,线程1 又把脏数据更新到了缓存,造成了数据库与缓存不一致。

在这里插入图片描述

(2)读写并发不一致

线程1 执行读操作,且没有命中缓存,然后就到数据库中取数据;此时来了一个 线程2 执行写操作,写完数据库后,让缓存失效,然后,之前的 线程1 再把老的数据放进去,会造成脏数据。

2.2.2 解决方案

(1)缓存数据加上过期时间

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

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

(2)消息队列串行化

(1)主要思路:在后台进程中我们可以创建多个队列,然后根据hash算法将写请求路由到不同的队列中,当来读请求的时候,就加入队列中,当写请求处理完毕后,再去处理读请求。
(2)分析:如果对于同一份数据有多个写请求同时在队列中,那么来一个读请求中加入队列中之后,一般写请求耗时比较久,那么读请求会需要很久才能返回,这样会特别影响性能,但能保证一致性(一般情况下建议不要用)。

(3)加分布式锁

通过加分布式读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。

    RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
    hotCacheLock.lock();
    try {
        productStr = redisUtil.get(productCacheKey);
        if (!StringUtils.isEmpty(productStr)) {
            if (EMPTY_CACHE.equals(productStr)) {
                redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);
                return new Product();
            }
            product = JSON.parseObject(productStr, Product.class);
            // 读延期
            redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS); 
            return product;
        }

        RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
        RLock readLock = readWriteLock.readLock();
        readLock.lock();
        try {
            product = productDao.get(productId);
            if (product != null) {
                redisUtil.set(productCacheKey, JSON.toJSONString(product),
                        genProductCacheTimeout(), TimeUnit.SECONDS);
                productMap.put(productCacheKey, product);
            } else {
                redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
            }
        } finally {
            readLock.unlock();
        }
    } finally {
        hotCacheLock.unlock();
    }


    @Transactional
    public Product update(Product product) {
        Product productResult = null;
        RReadWriteLock readWriteLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
        RLock writeLock = readWriteLock.writeLock();
        writeLock.lock();
        try {
            productResult = productDao.update(product);
            redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
                    genProductCacheTimeout(), TimeUnit.SECONDS);
            productMap.put(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), product);
        } finally {
            writeLock.unlock();
        }
        return productResult;
    }

(4)canal 监听 binlog日志

可以用阿里开源的 canal 通过监听数据库的 binlog日志 及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。

https://coolshell.cn/articles/17416.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/378224.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ECharts基础知识详细介绍

文章目录知识点ECharts 实例ECharts 基础配置组件seriesdataset用 option 描述图表titletooltiplegend 图例组件toolbox 工具栏坐标轴知识点 ECharts 实例series 与 datasetECharts 基础配置用 option 描述图表坐标系 ECharts 实例 一个网页中可以创建多个 ECharts 实例。每个…

JPA——Java.util.Date和Java.sql.Date

纵观整个项目的字段,最难搞的就是Date类型。今天我们就好好唠唠java中的两个Date类 一、Java.sql.Date 1. 构造方法 Date date new Date(System.currentTimeMillis());System.out.println(date);//2023-02-282. 常用方法: 方法作用void setTime(long…

【Java开发】JUC基础 04:Synchronized、死锁、Lock锁

1 概念介绍并发:同一个对象被多个线程同时操作📌 线程同步现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题,比如,食堂排队打饭,每个人都想吃饭,最天然的解决办法就是,排队…

强化学习RL 04: Actor-Critic Algorithm

actor: 是policy network,通过生成动作概率分布,用来控制agent运动,类似“运动员”。critic: 是value network,用来给动作进行打分,类似“裁判”。构造这两个网络,并通过environment奖励来学习这两个网络。…

AQS抽象队列同步器

aqs 抽象队列同步器,内部存储了一个valitail修饰的status 和内部类node ,来实现对共享变量并发同步队列机制,以reentrantLock为例,lock底层实际上调用的是sync的lock,会调用cas对status的状态进行修改,来确定是否获得锁…

学习 Python 之 Pygame 开发魂斗罗(七)

学习 Python 之 Pygame 开发魂斗罗(七)继续编写魂斗罗1. 载入水中图片并添加在水中的标志2. 修改玩家类函数3. 增加河的碰撞体4. 实现玩家在河中的样子继续编写魂斗罗 在上次的博客学习 Python 之 Pygame 开发魂斗罗(六)中&#…

csgo搬砖项目详细拆解,附选品+详细操作教程

项目实操 一:项目原理 csgo这款游戏不知道大家玩过没有,如果不了解的话,那你肯定知道穿越火线这款游戏吧,都是一个类型的射击游戏。 说到csgo,就得所以一下今天项目的平台steam,它是一个游戏平台&#x…

Redis面试问题总结

1. 什么是Redis?Redis 是一个使用 C 语言写成的,开源的高性能key-value非关系缓存数据库。它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。…

【Redis】Redis主从同步中数据同步原理

【Redis】Redis主从同步中数据同步原理 文章目录【Redis】Redis主从同步中数据同步原理1. 全量同步1.1 判断是否第一次数据同步2. 增量同步3. 优化Redis主从集群4. 总结1. 全量同步 主从第一次同步是全量同步。 数据同步包括以下三个阶段: 在从节点执行slaveof命令…

技术分担产品之忧(上):挑选有业务专家潜力的人

你好,我是王植萌,去哪儿网的高级技术总监、TC主席。从2014年起,担任一个部门的技术负责人,有8年技术总监经验、5年TC主席的经验。这节课我会从去哪儿网产研融合的经验出发,和你聊一聊怎么让技术分担产品之忧。 技术分…

SSL证书与我们普通人之间有什么关系

对于很多普通人来说,SSL证书似乎会感到很陌生,总觉得离自己很遥远,从而并没有引起察觉。要是这么想的话那么就真的大错特错了,其实SSL证书与我们普通人之间还是很密密相关的,是我们应该都需要关注的,下面就…

cesium学习记录02-vue项目中cesium的配置与使用

1,下载cesium包 (当然,使用npm install cesium安装也是可以的,不过在这里选择下载包放到本地) 官方下载地址 笔者的cesium版本为1.101 2,将下载的Cesium文件夹放到项目里某个位置 这里,笔者将…

又一个国内类ChatGPT模型?【秘塔科技上线自研LLM大模型「对话写作猫」】

又一个国内类ChatGPT模型?【秘塔科技上线自研LLM大模型「对话写作猫」】 (马上被打脸 ~ ~) 一直期待中国有没有类ChatGPT产品可以出现。 昨天,2023年2月27日,秘塔科技上线了自研LLM大模型「对话写作猫」,…

全屋Wi-Fi领域「兵戎相见」,鸿雁这一局赢面大不大?

作者 | 牧之 编辑 | 小沐 出品 | 智哪儿 zhinaer.cn相比全屋智能,另一个刚需属性更强,消费规模更大的细分市场,便是全屋Wi-Fi。在这个板块,当鸿雁入局的时候,笔者还是有些许的「诧异」。毕竟,鸿雁给大众的印…

【Vue3】vue3 + ts 封装城市选择组件

城市选择-基本功能 能够封装城市选择组件&#xff0c;并且完成基础的显示隐藏的交互功能 &#xff08;1&#xff09;封装通用组件src/components/city/index.vue <script lang"ts" setup name"City"></script> <template><div class…

【PyTorch】教程:torch.nn.Conv2d

Conv2d CLASS torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride1, padding0, dilation1, groups1, biasTrue, padding_modezeros, deviceNone, dtypeNone) 2D 卷积 out(Ni,Coutj)bias(Coutj)∑k0Cin−1Weight(Coutj,k)∗input(Ni,k)out(N_i, C_{out_j})bias(C_…

k8s环境jenkins发布vue项目指定nodejs版本

k8s环境jenkins发布vue项目指定nodejs版本1、背景2、分析3、解决方法3.1、 找到配置镜像位置3.2、 制作新镜像3.3、 推送镜像到私有仓库3.4、 修改配置文件1、背景 发布一个前端项目&#xff0c;它需要nodejs 16.9.0版本支持&#xff0c;而kubesphere 3.2.0集成的jenkins 的镜…

Hbase2.4.11简单了解_搭建Hbase集群_配置Hbase高可用---大数据之Hbase工作笔记0034

然后我们看一下hbase的集群架构,可以看到跟其他Hadoop系列的架构一样 都是有个master对吧,然后 还有3个region server,然后所有的机器,都连接到zookeeper 然后这里还要注意有个:backup-master103 ,这个是个备用的master 看看master 和 regionserver的作用. master部署到namen…

深度学习之神经网络的优化器篇

神经网络的优化器 文章目录神经网络的优化器GD 梯度下降算法重球法SGD随机梯度下降Momentum动量梯度NAG(Nesterov accelerated gradient)AdaGrad(Adaptive gradient)RMSProp(Root mean square prop)Adam(Adaptive Moment Estimation)AdamWAdan(Adaptive Nesterov Momentum)本片…

cesium学习记录03-QGis数据生产=>Postgis存储=>Geoserver发布=>Cesium调用

说明&#xff1a; 参照文章 1&#xff0c;安装 QGIS 下载安装 &#xff08;前四步就可以了&#xff09; 2&#xff0c;下载安装postgresql 3&#xff0c;下载安装PostGis 4&#xff0c;QGIS连接PostGis 5&#xff0c;QGIS 上传到Postgis 1&#xff0c;QGIS图的图 &…