谈了千百遍的缓存数据的一致性问题

灵魂拷问

  • 保证缓存和数据库的一致性很简单吗?

  • 有哪些方式能保证缓存和数据库的一致性呢?

  • 如果发生了缓存和数据库数据不一致的情况怎么办呢?

在上篇文章我们介绍了缓存的定义分类以及优缺点等,如果还没看的同学可以移步这里

听说你会缓存?

当我们的系统引入缓存组件之后,性能得到了大幅度提升,但是随之而来的是代码需要引入一定的复杂度,比如缓存的更新策略,写入策略,过期策略等,而其中最可能导致程序员加班的莫过于缓存和数据库的一致性问题了,既:缓存中的数据和数据库中的数据不一致。

一致性问题

说到一致性问题,这算是分布式系统中不可避免的一个痛点,或者说分布式系统天然就自带了数据一致性问题,虽然可以利用很多分布式事务解决方案来做到一致性,但是实际的系统架构设计中,我还是推崇避免分布式事务。缓存和数据库数据的一致性在产生原理上和分布式类似,其实可以把他们两个的关系看做是分布式系统中的两个操作节点。

凡是处于不同物理位置的两个操作,如果操作的是相同数据,都会遇到一致性问题

产生数据一致性问题的根本原因是对一个数据的多个操作过程,缓存和数据库数据的一致性也是这个原理,系统中最常见的操作流程是这样的:

  • 数据的请求首先查询缓存中是否存在该数据

  • 如果数据命中缓存(在缓存中存在)则直接返回数据,如果数据没有命中缓存(缓存中不存在),则去数据库中取数据

  • 从数据库中取回数据,然后把数据写入缓存

好图

从图中可以清楚的看到,对数据库的操作和对缓存的操作是两个不同阶段的操作,在任何一个操作过程中都会发生线程安全问题。比如说:

  • 当两个线程同时查询缓存的时候,可能会发生两个线程都没有命中缓存的问题

  • 如果两个线程都没有命中缓存就会发生同时查询数据库的问题

  • 接着就会发生两个线程同时回写缓存的问题

而这还不是最致命的,毕竟两个线程同时查询数据库,同时回写缓存数据在多数情况下缓存数据和数据库数据还能保持一致。最要命的是如果是两个线程都进行更新操作,最常见的更新过程是先更新数据库,然后更新缓存。下面就以最常见的用户积分场景为例,每个用户都有自己的积分,假如发生以下过程:

  • 线程A根据业务会把用户id为1的积分更新成100

  • 线程B根据业务会把用户id为1的积分更新成200

  • 在数据库层面,线程A和线程B肯定不存在并发情况,因为数据库用锁来保证了ACID(假如是mysql等关系型数据库),无论数据库中最终的值是100还是200,我们都假设正确。

  • 假设线程B在A之后更新数据库,则数据库中的值为200

  • 线程A和线程B在回写缓存过程中,很可能会发生线程A在线程B之后操作缓存的情况(因为网络调用存在不确定性),这个时候缓存内的值会被更新成100,发生了缓存和数据库不一致的情况

通过以上案例可见,解决缓存和数据库数据不一致的根本解决方案是需要把两个操作合并成逻辑上能保证事务的一个操作

两个操作看做一个操作

分布式锁

在平时开发中,利用分布式锁可能算是比较常见的解决方案了。利用分布式锁把缓存操作和数据库操作封装为逻辑上的一个操作可以保证数据的一致性,具体流程为:

  • 每个想要操作缓存和数据库的线程都必须先申请分布式锁

  • 如果成功获得锁,则进行数据库和缓存操作,操作完毕释放锁

  • 如果没有获得锁,根据不同业务可以选择阻塞等待或者轮训,或者直接返回的策略

image

利用分布式锁是解决分布式事务的一种方案,但是在一定程度上会降低系统的性能,而且分布式锁的设计要考虑到down机和死锁的意外情况,而最常见的分布式锁就是利用redis,但是也会有不少坑,具体可以参考之前的文章

redis做分布式锁可能不那么简单

删除缓存

相对于分布式锁的方案,而程序员实际中最喜欢使用的还是删除缓存的方式,在一个可能会发生不一致的场景下,我们会以数据库为主,在操作完数据库之后,不去更新缓存,而是删除缓存。这在一定意义上相当于只操作数据库,把需要维护的两个数据源变成了一个数据源。

image

这种方式要求必须先操作数据库,后操作缓存,不然的话发生不一致的几率会大很多。为什么这么说呢?因为就算是先操作数据库也会有发生不一致的几率,但是毕竟在整个操作过程中,删除缓存的操作只占整个流程时间的一小部分而已,而且我们可以利用缓存的过期时间来保证数据的最终一致性,所以在一些可以容忍数据短暂不一致的场景下可以采用这种方案的。

删除缓存方案带来的另外一个劣势是:如果同样的数据会被频繁更新,缓存会被频繁删除,当有读请求的时候又会被频繁的从数据库加载,所以这种方案适用于那种对缓存命中率不敏感的系统中。

单线程

发生缓存和数据库不一致的原因在于多个线程的同时操作,如果相同的数据始终只会有一个线程去操作,不一致的情况就会避免了,比如nodejs,可以充分利用nodejs单线程的优势。提到单线程不能不提一下Actor模型,actor模型在对于同样的对象上可以看做是单线程模式,具体有兴趣的同学可以查看之前的推文

分布式高并发下Actor模型如此优秀

单线程的模式基本上和分布式锁的方案类似,只不过单线程不需要锁就可以实现操作的顺序化,这也是单线程的优势所在。

其他方案

如果是以缓存为主呢?假如我们的应用程序只和缓存组件通信,至于持久化数据库由专门的程序负责,这样行不行呢?在理论上是可以的

image

不过这种方案需要考虑几个方面:

  • 数据从缓存持久化到数据采用什么样的解决方案,是同步进行还是异步进行呢?

  • 在新数据请求的时候,如果缓存不存在,要采用什么样的方式来填充数据

  • 如果缓存模块挂掉了该怎么办?

以缓存为主的方案的优势是数据优先进入IO速度快的设备,对于那些请求量大,但是可以容忍一定数据丢失的应用非常合适,比如应用log数据的收集系统,这种系统其中一个最大的特点就是可以容忍一定数据的丢失,但是并发的请求数会非常大。所以我们就可以利用缓存设备前置的方案来应对这种应用场景

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

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

相关文章

BS作业 基于springboot + Thymeleaf +mybatis 实现的书城管理系统

一:项目背景 项目描述 一个基本功能较为完整的后台管理项目。项目主要功能有:登录验证,登录功能还加入了随机验证码的验证; 用户注册,注册中密码基于srping 安全框架提供的加密(自动加盐)的密码储存方式,对注册重名进…

Istio Pilot 源码分析(二)

张海东, ‍多点生活(成都)云原生开发工程师。本篇主要介绍 Pilot 源码中的 ServiceEntryStore 及其推送 xDS 的流程。本文为 Istio Pilot 源码分析系列的第二篇文章。Istio Pilot 源码分析(一)了解了 Pilot 源码的基本…

Pytorch中的 torch.Tensor() 和 torch.tensor() 的区别

直接在搜索引擎里进行搜索,可以看到官方文档中两者对应的页面: 分别点击进去,第一个链接解释了什么是 torch.Tensor: torch.Tensor 是一个包含单一数据类型元素的多维矩阵(数组)。 正因为 torch.Tensor 只包…

leetcote34. 在排序数组中查找元素的第一个和最后一个位置

一:题目 二&#xff1a;上码&#xff08;暴力二分&#xff09; // class Solution { // public: // /** // 思路:1.首先这是一个升序的 那么相同的一定是会相连的// */// vector<int> searchRange(vector<int>& nums, int target) {// …

Git 图形化操作之合并提交记录

Git 图形化操作之合并提交记录独立观察员 2020 年 9 月 24 日目录1、显示日志2、合并提交记录3、推送合并的提交前言&#xff1a;当我们使用 Git 时&#xff0c;有时会遇到刚提交推送完一次修改&#xff0c;发现漏了该某处&#xff0c;只好又提交推送一次&#xff0c;这样在提交…

Pytorch中的 torch.as_tensor() 和 torch.from_numpy() 的区别

之前我写过一篇文章&#xff0c;比较了 torch.Tensor() 和 torch.tensor() 的区别&#xff0c;而这两者都是深拷贝的方法&#xff0c;返回张量的同时&#xff0c;会在内存中创建一个额外的数据副本&#xff0c;与原数据不共享内存&#xff0c;所以不受原数据改变的影响。 这里…

chrome禁止三方cookie,网站登录不了怎么办

背景新版chrome(80)浏览器默认屏蔽所有三方cookie已经不是什么新闻了&#xff0c;具体原因这里不去深究&#xff0c;有大量相关文章介绍&#xff0c;由于目前许多网站都依赖三方cookie&#xff0c;因此该特性的推出还是造成了一些的影响&#xff0c;比如收集用户信息的广告商&a…

leetcode69. x 的平方根

一:题目 二:上码 class Solution { public:/**思路:1.因为我们的 ans的平方 < x 那么我们就可以用二分法来做 不断缩小左右范围来确定 ans**/int mySqrt(int x) {int left 0; int right x;int ans 0;while (left < right) {long mid (right-left)/2 left;if (mid*…

初识ABP vNext(11):聚合根、仓储、领域服务、应用服务、Blob储存

点击上方蓝字"小黑在哪里"关注我吧聚合根仓储领域服务BLOB储存应用服务单元测试模块引用前言在前两节中介绍了ABP模块开发的基本步骤&#xff0c;试着实现了一个简单的文件管理模块&#xff1b;功能很简单&#xff0c;就是基于本地文件系统来完成文件的读写操作&…

leetcode367. 有效的完全平方数

一:题目 二:上码 class Solution { public:/**完全平方数:若一个数能表示成某个整数的平方的形式&#xff0c;则称这个数为完全平方数思路:1.我们将num先折半,因为它是某个整数的平方&#xff0c;而这个数的范围肯定不会超过num的一半2.那么这就相当于在[left,num/2]中查找某个…

跟我一起学.NetCore之文件系统应用及核心浅析

前言在开发过程中&#xff0c;肯定避免不了读取文件操作&#xff0c;比如读取配置文件、上传和下载文件、Web中html、js、css、图片等静态资源的访问&#xff1b;在配置文件读取章节中有说到&#xff0c;针对不同配置源数据读取由对应的IConfigurationProvider进行读取&#xf…

深度学习入门笔记(1)——导论部分

此笔记来源于 Sebastian Raschka 的 Introduction to Deep Learning 系列课程。 首先介绍的是传统的编程范式&#xff0c;假设我们想实现垃圾邮件识别的功能&#xff0c;传统的方法就是由程序员来找出垃圾邮件的规则并对其进行编程&#xff0c;得到一个垃圾邮件识别的程序。 机…

深度学习入门笔记(2)—— 感知器

最经典的神经元模型&#xff0c;从左到右依次是&#xff1a;输入、权重、加权和、阈值、输出。加权和又叫做 Net Input&#xff0c;符号为 z&#xff0c;当 z 的值大于阈值时输出 1&#xff0c;小于阈值时输出 0。 实现与门和或门&#xff0c;权重为 1&#xff0c;阈值分别为 1…

创建一个对象时,在一个类当中 静态代码块 和普通代码块构造方法 的顺序?

一:前言须知 普通代码块&#xff0c;在创建对象实例的时候&#xff0c;会被调用&#xff0c;每创建一次&#xff0c;就调用一次静态代码块&#xff0c;在类加载的时候执行&#xff0c;并且只会执行一次类加载的时机: 创建对象实例的时候&#xff08;new&#xff09;创建子类实…

ASP.NET Core 基于声明的访问控制到底是什么鬼?

从ASP.NET 4.x到ASP.NET Core&#xff0c;内置身份验证已从基于角色的访问控制(RBAC)转变为基于声明的访问控制(CBAC)。我们常用的HttpContext.User属性ASP.NET 4.0时代是IPrincipal类型&#xff0c;ASP.NETCore现在强化为ClaimsPrincipal类型。本文就一起来看看这难缠的、晦涩…

回溯的问题合集(Leetcode题解-Python语言)

78. 子集 class Solution:def subsets(self, nums: List[int]) -> List[List[int]]:ans []cur []def dfs(i):if i len(nums):ans.append(cur.copy())return# 包括 nums[i]cur.append(nums[i])dfs(i1)# 不包括 nums[i]cur.pop()dfs(i1)dfs(0)return ans要找出所有子集&a…

一个对象的创建流程

一:流程 加载Person类的信息,(也就是加载Person.class文件 只加载一次) 这个就是类加载的几个过程加载 ,将.class文件转化成二进制流加载到JVM的内存的方法区中&#xff0c;并在堆中生成一个Class对象验证准备解析初始化 该实例堆当中开辟空间 每个类的实例都会记得自己是由哪…

RabbitMq如何确保消息不丢失

上篇写了掌握Rabbitmq几个重要概念&#xff0c;从一条消息说起&#xff0c;这篇来总结关于消息丢失让人头痛的事情。网络故障、服务器重启、硬盘损坏等都会导致消息的丢失。消息从生产到消费主要结果以下几个阶段如下图。①生产阶段&#xff0c;生产者创建消息&#xff0c;经过…

LEETCODE PATTERNS Neetcode 刷题记录(Leetcode题解-Python语言)

LEETCODE PATTERNS 官网在这个链接&#xff0c;Neetcode 官网在这个链接 If input array is sorted then 遇到有序数组用二分或双指针 Binary searchTwo pointers If asked for all permutations/subsets then 求排列或子集用回溯 Backtracking If given a tree then 遇到树就用…