关于一致性,你该知道的事儿(下)

关于一致性,你该知道的事儿(下)

  • 前言
  • 一、并发修改单个对象
    • 1.1 原子写操作
    • 1.2 显示加锁
    • 1.3 原子的TestAndSet
    • 1.4 版本号机制
  • 二、 多个相关对象的一致性
    • 2.1 最大努力实现
    • 2.2 2PC && TCCC
    • 2.3.基于可靠消息的一致性方案
    • 2.4.Saga事务
  • 三、 多个副本的双写一致性
    • 3.1 写入时更新
    • 3.2 读取时更新
  • 四、后记
  • 参考

前言

上篇文章讲了一些关于较为底层的一致性内容,这篇文章上升一个层次,讨论讨论应用层面的一些一致性内容。

底层给我们提供了实现数据逻辑一致性的一些保证,但是由于上层应用多种多样,需求也各不相同,有些系统宁愿吞吐量降低也不允许不一致的情况发生,而有的系统则可以接受一定程度的不一致,但是需要保证一定的高可用和高并发(因此这些系统可能采没有实现事务功能的数据库系统)。 因此,要实现整个应用的一致性,上层系统还需要注意很多东西。下面是几点引起应用不一致的几个常见场景。


一、并发修改单个对象

和数据库一样,并发操作是引起不一致的一个重要场景,但是大多数web服务场景的应用不可避免的要支持并发(一个一个串行执行的请求任务在遇到io阻塞时效率会低到哭的)。

在这里插入图片描述

比如说,很多业务场景都会遇到read-modfy-write的逻辑,即先从数据库中读取要操作的数据D,然后根据用户请求对数据进行更新,成为D’, 然后将更新后的值写入到数据库中。

很常见的一个例子就是多个人更新同一个文档, 如果多个请求同时到来,需要进行这种“read-modify-write”的操作,设计不当有可能会造成后者的更新不包括前者修改后的数据,因此导致前者的修改丢失(更新丢失)的情形。

再比如说一种情况,一个群组,每加入一个人,就需要在群资料里的总人数中进行加1操作,如果使用普通的“read-modify-write”操作,并发场景下很有可能会出现更新数目不准确状况(两次+1操作被因为冲突变成了一次)。

有一些方式可以应用到上述场景来一定程度上的应对上述问题。

1.1 原子写操作

很多数据库提供了原子更新操作,将“read-modify-write”的逻辑下沉到数据库层面,可以解决某些场景的更新丢失问题。

比如说mongo提供单个document级别的原子操作。如果我们需要更新的数据是在同一个document中,那么使用mongo可以避免更新丢失(注意,这里是针对单个document情况,对于复杂的业务逻辑需要更新多个document,mongo只保证了每个document的原子性,而不保证整个update操作的原子性)。

redis提供的大部分单个命令操作时原子性的, 有些场景可以利用redis的原子操作实现一些防止并发冲突的功能,比如说原子自增,序列号分发等。

1.2 显示加锁

有些数据库(比如说mysql)提供了对返回的结果集加锁的功能。所以,应用程序可以根据请求对查询的结果集加锁,显示锁定待更新的对象。当其他的请求尝试读取对象的时候,必须等待当前请求的执行队列完成。

Select * from page 
where name = "modify_page"
for update;     //for update 指示数据库对返回的所有数据行进行加锁

锁是一把双刃剑,虽然好用易理解,但是用得不好往往会引起效率的降低,使用宜谨慎。

1.3 原子的TestAndSet

有些数据库支持原子性的testAndSet操作,即只有当前值没有被其他人修改时才执行更新写入操作。

比如说对于两个用户同时需要更新一篇文档,只有当前页面从上次读取出后没有发生变化,才会执行当前的更新操作;
如下:

update page 
set content = "new content"
where id = 1234 and content = "old content"

1.4 版本号机制

有这样一个场景,比如说要提供一个简单的kv存储系统给客户,客户通过调用接口来操作这些kv值。 但是有一个需求,客户端需要明确知道调用是否有并发冲突,即"我调用的时候要么成功,要么失败。但是不允许有人和我一起调用成功"。

这种kv存储怎么设计呢?直接使用已有的kv组件(如redis)肯定是不行的,因为它无法防止并发冲突,虽然redis可以使用单个线程执行客户端发来的请求(串行化请求),但是它会"悄咪咪"的把客户的请求都执行了,当返回结果给客户端时,客户端也不知道自己的请求是不是穿插了其他的请求。

在这里插入图片描述
有其他的解决方案不?

可以参考版本控制的类似思想来解决这个问题。每个kv数据要保存一个版本号代表当前数据的版本 dataVersion, 每次操作完数据之后,dataVersion自增。同时当客户端请求的时候,让其带一个请求的版本号reqVersion(版本号可以是一个中心的版本分发器,也可以让app层或者redis返回时response携带,但必须是全局唯一的),只有reqVersion 和dataVersion 匹配时(比如说reqVersion=dataVersion+1),操作可以进行,否测返回客户端并发冲突。

这样当同时多个客户端请求时,如果携带相同的reqVersion来请求,只有一个可以成功,其他的将返回并发操作失败。如下图所示。

在这里插入图片描述
(上述请求假设req1先到达服务端)

这种方式从某种程度上和上面的TestAndSet有点像,都是先Test,符合某种条件时才进行Set; 不一样的一点是把判断的条件移到了客户端(让客户端请求时携带),这样让客户端能感知到并发的处理结果。其实如果不考虑判断的条件放在何处,TestAndSet和版本号机制本质上都是属于乐观锁的方式,只有在更新时才判断条件是否满足,是否有其他的线程更改了条件。

二、 多个相关对象的一致性

当现在很多的互联网应用开始划分业务为各个微服务的时候,各个微服务之间的联系就变得繁多了起来。很多时候,一个上游的请求会导致下游多个服务的调用;一个业务逻辑需要同时(这里的同时指的不是时间上的同时)修改多个对象,这就需要保证这多个数据对象的一致性。

一个很常见的业务场景就是订单支付,如下图所示。一个订单请求涉及到下游多个服务和对应数据对象的修改,。请求之后这些数据对象要保证一致性,不能订单数据为已支付,但是库存数据没修改。
在这里插入图片描述

我们要达成多个数据对象一致性(专业点叫做分布式事务),要么一起提交修改,要么都不提交修改的最终效果,这有哪些方式呢?

一般有常见的有几种方式(我用的还不多,在此简单介绍):

2.1 最大努力实现

最简单的一种方式就是重试策略。当要修改的其中某个数据对象不成功的时候(因为网络超时、或者机器宕机等原因),就重新发起请求,不断重试,直到重试成功或者达到最大的重试次数。

本质上来说,这种方式不一定能达到多个数据对象的一致性,因此只能算作最大努力实现。但对于一些一致性要求没那么高的场景,如果上层的应用设计的合理,还是可以使用这种方式的,毕竟执行失败是少数情况,很多时候retry几下就可以成功的。

2.2 2PC && TCCC

要么一起提交修改,要么都不提交修改。是不是和我们上面讨论的2PC协议有点类似?

没错,2PC也是实现这种分布式事务一种经典协议(只不过之前说它是在分布式数据库层面,现在讨论的是在业务应用层面),通过"Parepare”—>“Commit/Rollback"两个阶段来实现多个数据对象的一致性。

上文中有论述,这里就不重复了。 这里介绍一个在2PC在业务层面的一个变种,TCCC。

TCC是 Try-Confirm-Cancel 的简称,如其名字中所表述的,它的执行过程分为3个阶段:

  1. Try : 检测预留留资源, 对应于2PC的Prepare阶段
  2. Confirm: 真正的业务操作提交, 对应2PC的Commit阶段
  3. Cancel: 预留留资源释放, 对应2PC的Rollback阶段

如下图所示:
在这里插入图片描述
从某成程度上说,TCC是2PC在业务层间的套用,可以实现最终的一致性。但是2PC存在的问题,它也存在,而且这种方式对业务的侵入较强,会带来一定的开发量。

2.3.基于可靠消息的一致性方案

还有一种基于可靠消息的一致性方案,通过消息中间件自身提供的异步+持久化+重试的策略保证(当然也不是完全保证)消息一定会被消息的订阅方消费。 那么只需要保证业务操作(修改其中的某个数据对象)和消息的发送(传递修改其他对象的指令)是事务性的即可。

如下图所示为基于消息中间件的一致性方案,通过【消息发送方】执行本地事务(修改某个数据对象)和发送消息到服务端(也就是消息中间件服务)的一个类2PC过程来实现某种程度上的原子性。

首先消息发送方会发送一个“半事务消息”,然后再执行本地事务,根据本地事务执行的结果,来给消息服务端再发送一个commit或rollback的确认消息。只有服务端收到commit消息后,才会真正的发送消息给订阅方。
在这里插入图片描述

这种方式使用消息中间件的方式解耦了修改两个对象数据的过程,对性能的损耗和业务的入侵更小。现在很消息中间件都实现了事务消息的功能,可以很好的帮上层业务实现多个对象的一致性问题。

2.4.Saga事务

还有一种应用于长事务的Saga方案,通过将长事务拆分为多个本地短事务来执行,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。(有点类似于数据库中的undo操作,出有问题了就逆向操作)。

如下图所示:
在这里插入图片描述
在这里插入图片描述
从上文介绍的几种应对多个相关联对象的一致性方案来看,很多方案或多或少都能看到重试的影子(第2、3哥方案中间过程也依赖于重试),或多或少也都有点加锁的味道。 一般来说,有锁就影响并发,影响性能。 在性能、可用性和一致性方面,具体采用哪种方案还是要看具体的业务场景和需求。

三、 多个副本的双写一致性

如前所述,除了数据库系统给我们提供的多副本机制,我们还会遇到不同异构层次涉及的多个副本,具体来说是缓存系统涉及到的多个副本。在应用中常见的就是类似 本地内存-> redis缓存->数据库系统这种,我们为了提供系统的读写性能,把一部分常用的数据缓存到更快访问的介质之上,对用上层应用来说这个过程也涉及到一致性的相关问题。 虽然一般来说这种方式不会要求多么强的一致性,但是不同的操作顺序也会对一致性有不同的影响。

这里简要讨论一下【Redis缓存—>数据库】这种缓存架构,应用层不同的操作顺序带来的不同结果。

对于【Redis缓存—>数据库】这种缓存架构方式,读取方式肯定是先从Redis中读取,如果Redis不存在,再从数据库中读取。

但是写入更新就有好几种方式了,按照缓存更新的时机,分为写入时更新,或者读取时更新

这么一说下来,就有如下几种操作方式了。

3.1 写入时更新

写入时更新分为【更新缓存–>更新数据库】 和 【更新数据库–>更新缓存】两种方式。这两种方式都会有并发冲突带来的不一致现象。比如说第一种方式吧,A、B两个进程并发来一套上述的流程。
在这里插入图片描述
【更新缓存–>更新数据库】

最终发生了Redis和数据库中数据不一致的情况。

第二种方式也是一样,都会产生这种A1->B1->B2->A2(A1:表示A进程执行第1个操作)的问题。
在这里插入图片描述
【更新数据库–>更新缓存】


我们这里没考虑执行失败,或者宕机的情况,如果考虑这种情况的话,第一种方式要比第二种方式影响更大些,因为在第一种方式里,如果Redis更新成功了,但是数据库失败了,数据就不仅仅是不一致的问题,而是产生了脏数据,缓存毕竟是缓存,我们最终要是要以数据库中的数据为准。

3.2 读取时更新

【删除缓存–>更新数据库】和【更新数据库–> 删除缓存】是两种读取时更新的方式,这两方式先删除缓存,然后下一次读取的时候就可以从数据库中读取更新的数据。这两种方式相当于是把写写冲突造成的不一致转移到了读写上。比如说下面的并发场景:

在这里插入图片描述
【删除缓存–>更新数据库】

在这里插入图片描述
【更新数据库–> 删除缓存】

这两种方式也会造成最终的缓存Dc和数据库Db不一致。相比来说,第四种方式要比第三种方式发生不一致的概率更小点,因为更新缓存的速度要远远大于更新数据库,第四种方式中ClientA 把数据库都更新了,缓存也删了,ClientB还没有更新缓存,这种情况不能说没有,但是概率上要少些。

但是采用删缓存有一个缓存穿透问题需要考虑:就是删除了缓存之后要防止突然大量的并发请求到数据库中。

上述只是简单讨论了一下,实际的现实的情况要更复杂(比如说哪个过程执行失败了),也更灵活(有些场景不需要太高的一致性),需要具体问题,具体分析。


四、后记

一致性是个大问题,这两篇文章从单机和分布式的角度,从数据库和应用层面,大概梳理了一致性的相关内容。内容有点多, 因此很多内容只是简单过了个囫囵吞枣。这两篇文章主要是想通过梳理一下一致性的相关内容,来对编程过程中涉及到的一致性有个大概的认识(知道是怎么回事儿,算是属于哪个分类,该往哪个方向考虑问题),以后遇到一致性问题不至于 卖虾米不拿秤-抓瞎。

但是如果从更高的层次来看,这两篇文章的很多内容其实非常相似,抽象的看,研究的可能就是一个东西,只是在实际中被用到了不同的场景,因而有些变化。因此如果能从宏观上来看这些内容,会对一致性有更深的理解(当然,我现在还没到这个程度)。

最后总结以一张“一致性全家图”来结束这两篇关于一致性的文章。
在这里插入图片描述


参考

【1】《DDIA》
【2】 挑战大型系统的缓存设计——应对一致性问题
【3】 不就是分布式事务,这下彻底清楚了😎
【4】 一致性问题与分布式事务
【5】 TCC分布式事务,最终一致性分布式事务
【6】Seata-go: Simple Extensible Autonomous Transaction Architecture(Go version)
【7】关于一致性,你该知道的事儿(上)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Flink container exit 143 问题排查

你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益: 了解大厂经验拥有和大厂相匹配的技术等 希望看什么,评论或者私信告诉我! 文章目录 一…

centos7.9系统安全加固

1、限制用户登陆 vim /etc/hosts.deny,若禁止192.168.0.158对服务器进行ssh的登陆,添加如下内容 sshd : 192.168.0.158 添加完毕后就生效了,直接用192.168.0.158访问主机,就无法连接了,显示 Connection closing...Soc…

【密评】 | 商用密码应用安全性评估从业人员考核题库(9/58)

Hill密码是重要古典密码之一,其加密的核心思想的是()。 A.线性变换 B.非线性变换 C.循环移位 D.移位 著名的Kerckhoff原则是指()。 A.系统的保密性不但依赖于对加密体制或算法的保密,而且依赖于密钥 B.系统…

【JUC】并发编程 Synchronized 锁升级原理

Synchronized如何实现同步/互斥的效果? monitorenter: 将锁对象对象头中Mark Word的前30bit替换成指向操作系统中与其关联的monitor对象,将锁记录位状态改为10 monitorexit: 将锁对象对象头中Mark Word进行重置,重新恢…

Open CASCADE 教程 – AIS:自定义呈现

文章目录 开始 (Getting Started)呈现构建器 (Presentation builders)基元数组 (Primitive arrays)基元外观 (Primitive aspects)二次构建器 (Quadric builders)计算选择 (Computing selection)突出显示选择所有者 (Highlighting selection owner)突出显示的方法 (Highlighting…

【网站项目】SpringBoot796水产养殖系统

🙊作者简介:拥有多年开发工作经验,分享技术代码帮助学生学习,独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。🌹赠送计算机毕业设计600个选题excel文件,帮助大学选题。赠送开题报告模板&#xff…

vi\vim编辑器

root用户(超级管理员) 无论是Windows、MacOS、Linux均采用多用户的管理模式进行权限管理。 在Linux系统中,拥有最大权限的账户名为:root(超级管理员) root用户拥有最大的系统操作权限,而普通…

改进YOLOv5,YOLOv5+CBAM注意力机制

目录 1. 目标检测模型 2. YOLOv5s 3. YOLOv5s融合注意力机制 4. 修改yolov5.yaml文件 5. ChannelAttentionModule.py 6. 修改yolo.py 1. 目标检测模型 目标检测算法现在已经在实际中广泛应用,其目的是找出图像中感兴趣的对象,并确定对象的类别和位…

牛客NC343 和大于等于K的最短子数组【困难 前缀和 Java/Go】

题目 题目链接: https://www.nowcoder.com/practice/3e1fd3d19fb0479d94652d49c7e1ead1 思路 本答案利用前缀和解答,Java,Go答案通过,但是同样的代码用PHP的话有一个测试用例超时 应该还有更优秀的答案,后面找到更优…

如何远程操作服务器中的Python编译器并将运行结果返回到Pycharm

文章目录 一、前期准备1. 检查IDE版本是否支持2. 服务器需要开通SSH服务 二、Pycharm本地链接服务器测试1. 配置服务器python解释器 三、使用内网穿透实现异地链接服务器开发1. 服务器安装Cpolar2. 创建远程连接公网地址 四、使用固定TCP地址远程开发 本文主要介绍如何使用Pych…

分布式与一致性协议之PBFT算法(一)

PBFT算法 概述 前面提到了拜占庭将军问题之后,有人可能会感到困惑:口信消息型拜占庭问题直接在实际项目中是如何落地的呢?事实上,它很难在实际项目中落地,因为口信消息型拜占庭问题之解是一个非常理论化的算法,没有与…

C++类的概念以及用法

目录 面向过程和面向对象初步认识类的引入类的定义类的两种定义方式声明和定义全部放在类体中 声名定义分离 类的作用域成员变量命名规则建议访问限定符 类的封装类的实例化类对象模型类的对象大小的计算扩展 结构体内存对齐规则 感谢各位大佬对我的支持,如果我的文章对你有用,…

《Fundamentals of Power Electronics》——转换器的传递函数

转换器的工程设计过程主要由以下几个主要步骤组成: 1. 定义了规范和其他设计目标。 2. 提出了一种电路。这是一个创造性的过程,利用了工程师的物理洞察力和经验。 3. 对电路进行了建模。组件和系统的其他部分适当建模,通常使用供应商提供的…

祝天下母亲节快乐!虚无!——早读(逆天打工人爬取热门微信文章解读)

练功加精力哦 引言Python 代码第一篇 人民日报【夜读】人与人之间最好的关系:遇事靠谱,懂得感恩第二篇 冯站长之家 三分钟新闻早餐结尾 感恩与善行 是人生旅途中的灯塔 怀感恩之心 行小善之事 它们将指引我们走向光明 引言 今天是母亲节 祝天下的所有母…

三星硬盘格式化后怎么恢复数据

在数字化时代,硬盘作为数据存储的核心部件,承载着我们的重要文件、照片、视频等资料。然而,不慎的格式化操作可能使我们失去宝贵的数据。面对这样的困境,许多用户可能会感到无助和焦虑。本文旨在为三星硬盘用户提供格式化后的数据…

【CMU 15-445】Proj4 Concurrency Control

Concurrency Control 通关记录Task1 TimestampsTask2 Storage Format and Sequential ScanTask3 MVCC ExecutorsTask3.1 Insert ExecutorTask3.2 CommitTask3.3 Update and Delete ExecutorTask3.4 Stop-the-world Garbage Collection Task4 Primary Key IndexTask4.0 Index Sc…

vue3 element plus el-date-picker组件在日期上做标识

1.先看效果图,带红点的就是我要做标识的日期 2.直接把代码拿出来就可以用 (1)html部分 <el-date-pickerv-model"startTime"type"datetime"placeholder"选择开始日期"format"YYYY-MM-DD HH:mm"value-format"YYYY-MM-DD HH:mm…

基于ChatGLM+Langchain离线搭建本地知识库(免费)

目录 简介 服务部署 实现本地知识库 测试 番外 简介 ChatGLM-6B是清华大学发布的一个开源的中英双语对话机器人。基于 General Language Model (GLM) 架构&#xff0c;具有 62 亿参数。结合模型量化技术&#xff0c;用户可以在消费级的显卡上进行本地部署&#xff08;INT…

大模型微调之 在亚马逊AWS上实战LlaMA案例(八)

大模型微调之 在亚马逊AWS上实战LlaMA案例&#xff08;八&#xff09; 微调技术 Llama 等语言模型的大小超过 10 GB 甚至 100 GB。微调如此大的模型需要具有非常高的 CUDA 内存的实例。此外&#xff0c;由于模型的大小&#xff0c;训练这些模型可能会非常慢。因此&#xff0c…

计算机网络(网络原理与应用)之高级交换实验------冗余环路与生成树协议

一、实验目的 (1)了解生成树协议的作用&#xff1b; (2)熟悉生成树协议的配置。 二、应用环境 采用生成树协议可以避免环路。 生成树协议的根本目的是将一个存在物理环路的交换网络变成一个没有环路的逻辑树形网络。IEEE802.ID协议通过在交换机上运行一套复杂的算法STA(sp…