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

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

  • 前言
  • 一、并发修改单个对象
    • 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】关于一致性,你该知道的事儿(上)

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

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

相关文章

HNCTF-PWN

1.ez_pwn 直接看危险函数,不能溢出,只能覆盖ebp。 后面紧接的又是leave,ret 很明显是栈迁移,通过printf打印出ebp,通过偏移计算出栈地址。 通过gdb调试,偏移是0x38 以下是payload: from pwn import * #i…

【自然语言处理】【大模型】DeepSeek-V2论文解析

论文地址:https://arxiv.org/pdf/2405.04434 相关博客 【自然语言处理】【大模型】DeepSeek-V2论文解析 【自然语言处理】【大模型】BitNet:用1-bit Transformer训练LLM 【自然语言处理】BitNet b1.58:1bit LLM时代 【自然语言处理】【长文本…

11.偏向锁原理及其实战

文章目录 偏向锁原理及其实战1.偏向锁原理2.偏向锁案例代码演示2.1.偏向锁案例代码2.2.1.无锁情况下状态2.1.2.偏向锁状态2.1.3.释放锁后的状态 2.2.偏向锁的膨胀和撤销2.2.1.偏向锁撤销的条件2.2.2.偏向锁的撤销 2.2.3.偏向锁的膨胀 2.3.全局安全点原理和偏向锁撤销性能问题2.…

EPAI手绘建模APP工程图顶部工具栏

7、工程图 图 302 工程图 工程图包括顶部常用工具栏、右侧工程图工具栏、左侧模型列表栏、中间的工程图。 (1) 常用工具栏 ① 删除,选中场景中工程图元素后,删除。可以选择多个工程图元素同时删除。 ② 设置,打开工程图设置页面&#xff0…

2024 年最新本地、云服务器安装部署 miniconda 环境详细教程(更新中)

Anaconda 概述 Anaconda 是专门为了方便使用 Python 进行数据科学研究而建立的一组软件包,涵盖了数据科学领域常见的 Python 库,并且自带了专门用来解决软件环境依赖问题的 conda 包管理系统。主要是提供了包管理与环境管理的功能,可以很方便…

picoCTF-Web Exploitation-More SQLi

Description Can you find the flag on this website. Additional details will be available after launching your challenge instance. Hints SQLiLite 先随便输入个账号密码登录一下,得到查询SQL,接下来应该对SQL进行某些攻击来绕过密码登录成功 -- …

微信小程序踩坑,skyline模式下,简易双向绑定无效

工具版本 基础库版本 Skline模式 页面json设置 问题描述 skyline模式下,textarea,input标签设置简易双向绑定 model:value是无效的,关闭skyline模式就正常使用了 截图展示 这里只展示了textarea标签,input标签的简易双向绑定也是无效的 总结 我在文档里面是没找到skyline里面不…

动态规划----股票买卖问题(详解)

目录 一.买卖股票的最佳时机: 二.买卖股票的最佳时机含冷冻期: 三.买卖股票的最佳时期含⼿续费: 四.买卖股票的最佳时机III: 五.买卖股票的最佳时机IV: 买卖股票的最佳时机问题介绍:动态规划买卖股票的最佳时机是一个经典的…

windows使用Docker-Desktop部署lobe-chat

文章目录 window安装docker-desktop下载和启动lobe-chatAI大语言模型的选择lobe-chat设置大模型连接 window安装docker-desktop docker-desktop下载地址 正常安装应用,然后启动应用,注意启动docker引擎 打开右上角的设置,进入Docker Engine设…

算法学习系列(六十):区间DP

目录 引言区间合并模板一、石子合并二、环形石子合并三、能量项链 引言 关于这个区间 D P DP DP ,其实是有套路和模板的,题型的话也是变化不多,感觉就那几种,只不过有些题会用到高精度或者是要记录方案,所以整体来说…

Unity编辑器如何多开同一个项目?

在联网游戏的开发过程中,多开客户端进行联调是再常见不过的需求。但是Unity并不支持编辑器多开同一个项目,每次都得项目打个包(耗时2分钟以上),然后编辑器开一个进程,exe 再开一个,真的有够XX的。o(╥﹏╥)o没错&#…

Rust学习笔记(上)

前言 笔记的内容主要参考与《Rust 程序设计语言》,一些也参考了《通过例子学 Rust》和《Rust语言圣经》。 Rust学习笔记分为上中下,其它两个地址在Rust学习笔记(中)和Rust学习笔记(下)。 编译与运行 Ru…

python使用yaml文件以及元组样式字符串使用eval的类型转换

编程中,对于可变内容,最好是将其放入配置文件中,经过这段时间的学习,感觉使用yaml文件很方便。我的环境:win10,python3.8.10。 python使用yaml文件,首先要安装库。 pip38 install pyyaml 安装…

AWTK 开源串口屏开发(18) - 用 C 语言自定义命令

AWTK-HMI 内置了不少模型,利用这些模型开发应用程序,不需要编写代码即可实现常见的应用。但是,有时候我们需要自定义一些命令,以实现一些特殊的功能。 本文档介绍如何使用 C 语言自定义命令。 1. 实现 hmi_model_cmd_t 接口 1.1…

实现二叉树的基本操作

博主主页: 码农派大星. 关注博主带你了解更多数据结构知识 1我们先来模拟创建一个二叉树 public class TestBinaryTreee {static class TreeNode{public char val;public TreeNode left;public TreeNode right;public TreeNode(char val) {this.val val;}}public TreeNode …

linux 安装 mangodb 并设置服务开机自启

1、下载 wget http://mosquitto.org/files/source/mosquitto-1.6.8.tar.gz 2、解压 tar -zxvf mosquitto-1.6.8.tar.gz 3、编译安装cd mosquitto-1.6.8 make sudo make install4、在当前目录。进入mosquitto服务文件存放的文件夹 cd service/systemd可以看到3个文件 点击read…

【C/C++】设计模式——工厂模式:简单工厂、工厂方法、抽象工厂

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c系列专栏&#xff1a;C/C零基础到精通 &#x1f525; 给大…

二.基础篇: 面向对象进阶

1. 基础篇语法篇&#xff1a;一.基础篇&#xff1a;基础语法-CSDN博客 面向对象进阶 本章主要学习内容&#xff1a; static继承包&#xff0c;final&#xff0c;权限修饰符&#xff0c;代码块抽象类接口多态内部类 1. static static翻译过来就是静态的意思static表示静态&am…

AI语音模型PaddleSpeech踩坑(安装)指南

PaddleSpeech简介 PaddleSpeech 是基于飞桨 PaddlePaddle 的语音方向的开源模型库&#xff0c;用于语音和音频中的各种关键任务的开发&#xff0c;包含大量基于深度学习前沿和有影响力的模型。 PaddleSpeech安装步骤 提示&#xff1a;要找到一个合适的PaddleSpeech版本与pad…

java项目之相亲网站的设计与实现源码(springboot+mysql+vue)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的相亲网站的设计与实现。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 相亲网站的设计与实…