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

news2024/9/20 14:31:58

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

  • 前言
  • 一、并发修改单个对象
    • 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.coloradmin.cn/o/1666518.html

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

相关文章

docker搭建redis6.0(docker rundocker compose演示)

文章讲了:docker下搭建redis6.0.20遇到一些问题,以及解决后的最佳实践方案 文章实现了: docker run搭建redisdocker compose搭建redis 搭建一个redis’的过程中遇到很多问题,先简单说一下搭建的顺序 找一个redis.conf文件&…

LaTeX多行公式中\split出现一长一短多行公式无法居中

最近在整理一篇论文时出现了一长一短多行公式的问题无法居中 类似下图的情况: 这部分的代码如下: \begin{equation} \begin{split} \scalebox{0.75}{$X_{n} C$}\\ \scalebox{0.75}{$X_{m} \biggl\{\begin{array}{ll} \sum\limits_{i1}^{n} [X_{i} …

Java语法学习九之内部类和异常

目录 内部类 实例内部类 静态内部类 局部内部类 匿名内部类 异常 异常的体系结构 异常的分类 编译时异常 运行时异常 异常的处理 防御型编程 异常的抛出 异常的捕获 异常声明throws ​编辑 try-catch捕获并处理 finally 自定义异常类 内部类 实例内部类 p…

二叉树的非递归遍历(c++)

前序 . - 力扣(LeetCode). - 备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/binary-tree-preorder-traversal/description/ 1---2---4---5--…

如何修复显示器或笔记本电脑屏幕的黄色色调?这里提供几种方法

序言 如果你的笔记本电脑屏幕呈淡黄色,则可以启用夜灯功能。该问题也可能源于连接松散的显示电缆、损坏的显卡驱动程序或错误配置的显示器设置。以下是一些故障排除步骤,你可以尝试解决此问题。 禁用夜间模式 夜间模式功能旨在减少显示器的蓝色色调,使屏幕看起来更温暖,…

解决axios发送post请求,springMVC接收不到数据问题

今天发现一个问题: vue组件中无法正确接收并处理axios请求 这个问题已经困扰我好久了,在电脑面前坐了两天只能确定前端应该是正确的发送了请求,但发送请求后无法正确接受后端返回的数据。 问题:vue组件无法接受后端数据 错误代码如…

【信号与槽机制】

信号与槽机制 🌟 信号函数🌟 槽函数🌟 连接函数🌸 QObejct::connect函数剖析🌟 官方文档中给出的定义🌟《Qt 5.9 C开发指南》中的定义 🌟 信号函数 信号是一种特殊的成员函数,用于在…

JVM 类加载机制

JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。 加载 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.class 对…

用户管理中心——登录功能

用户管理中心——登录功能 一、用户登录1. 登录设计2. 登录方法实现实现细节逻辑删除实现代码 3. 登录接口实现4. 测试 二、用户管理用户管理接口 一、用户登录 1. 登录设计 2. 登录方法实现 实现细节 首先在接口中com.example.demo.service.UserService定义一个用户登录doL…

HSB色彩模式计算详解

HSB色彩模式计算详解 前些天撰文几篇介绍了几种圆形和矩形的HSB绘制方法。后2篇介绍了HSB的计算方法。我感到不是很详细,今再补充说明计算方法。 圆形H调色板选色,计算 Rad, ang, L, return H 计算二点距离 L,取色点到圆心距离 x0250;…

https://是怎么实现的?

默认的网站建设好后都是http访问模式,这种模式对于纯内容类型的网站来说,没有什么问题,但如果受到中间网络劫持会让网站轻易的跳转钓鱼网站,为避免这种情况下发生,所以传统的网站改为https协议,这种协议自己…

【牛客】SQL201 查找薪水记录超过15条的员工号emp_no以及其对应的记录次数t

1、描述 有一个薪水表,salaries简况如下: 请你查找薪水记录超过15条的员工号emp_no以及其对应的记录次数t,以上例子输出如下: 2、题目建表 drop table if exists salaries ; CREATE TABLE salaries ( emp_no int(11) NOT N…

【深入理解MySQL的索引数据结构】

文章目录 🔊博主介绍🥤本文内容📕索引底层数据结构与算法📙索引数据结构📘二叉树📘红黑树📘Hash📘B-Tree📘BTree 📙表在不同存储引擎的存储结构📘…

react18【系列实用教程】useContext —— Context 机制实现越层组件传值 (2024最新版)

什么是 Context 机制? Context 机制是 react 实现外层组件向内层组件传值的一种方案,父组件可以向其内部的任一组件传值,无论是子组件还是孙组件或更深层次的组件。 实现步骤 1.使用createContext方法创建一个上下文对象 Ctx 2.在顶层组件中通…

基恩士PLC-KV5500基础入门

一、准备工作: 1.准备的东西:一个基恩士PLC-KV5500模块。两个自复位开关,24v LED灯一个,24v开关电源一个,KV5500端子台IO线缆;有编程软件的电脑一台。 编程软件: 基恩士PLC-KV5500接线图&…

妙笔生花,创作无限——WonderPen妙笔 for Mac

写作,是灵感的流淌,是心灵的独白。WonderPen妙笔 for Mac,为您的灵感插上翅膀,让您的创作更加流畅自如。它拥有简洁直观的界面设计,让您的思绪在纯净的写作环境中自由飞翔。多种写作模式,满足您不同的创作需…

vue2基础语法02——计算属性、方法、侦听器

vue2基础语法02——计算属性、方法、侦听器 1. 计算属性 computed1.1 为什么要用计算属性1.2 简单例子1.2.1 例子1.2.2 计算属性缓存 1.3 计算属性的 setter 2. 方法 methods2.1 例子2.2 说明2.3 简单方法替换实现 3. 侦听属性 watch3.1 介绍3.2 值的情况3.2.1 对应回调函数3.2…

halcon学习之一维测量基础

目录 创建测量矩形,获取测量句柄 gen_measure_rectangle2() 使用句柄进行测量 measure_pos() 修改参数Threshold 修改参数Transition 修改参数select 参数RowEdge,ColumnEdge,Distance …

KAN网络

目录 背景知识 什么是神经网络? 神经网络发展史 MP神经元模型 感知机模型 KAN 引言 MLP架构vsKAN架构 从数学定理方面来看: 从算法层面上看: 从实际应用过程看: KAN的架构细节 KAN的准确性 KAN的可解释性 监督学习…

验证搜索二叉树

目录 题目 方法一 思路 优化 方法二 思维误区 递归关系推导 代码实现 题目 98. 验证二叉搜索树 难度:中等 给你一个二叉树的根节点root ,判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下: 节点的左子树只包含…