read-after-write consistency 写后读一致性的解决方法

news2024/10/3 0:32:30

问题定义

写后读一致即写完数据之后马上读,直接能读到新的数据,而不是老的数据。

导致这个问题主要是数据库之间的同步延时。这里只讨论一主多从的情况。
如下图:

  1. 用户添加新评论
  2. 用户刷新,读请求到从节点1,此时从节点还没有主从复制完成,用户看不到自己的评论
  3. 用户刷新,读请求到从节点1,此时从节点主从复制完成,用户可以看见自己的评论
  4. 用户刷新,读请求到从节点2,此时从节点还没有主从复制完成,用户看不到自己的评论

在这里插入图片描述

这种情况就会出现很诡异的情况,自己的评论时有时无的。我们预期是一直能看到最新的评论。

解决方法

解决方法之一是把最近有写入操作的用户的读取操作路由到数据库主节点(Pinning User to Master)。其他用户的读取操作还是走从节点。

当用户写入之后的特定的时间窗口内,将用户的读操作固定到主节点,这样读取和写入都转移到了主节点,这意味着读取将从始终具有最新数据副本的数据节点进行,因此可以实现读写一致性。

在时间窗口之后,读操作可以转移到从节点。
在这里插入图片描述

代码示例

地址:
https://github.com/mamil/data-demo/tree/main/read-after-write%20consistency

创建数据库的连接

这里创建2种连接,一个直接和master相连,另一个使用读写分离的方式连接。

  • master的连接可以看这个函数
func initMasterDb() {
	PDbHost := viper.GetString("MYSQL.HostName")
	PDbPort := viper.GetString("MYSQL.Port")
	PDbUser := viper.GetString("MYSQL.UserName")
	PDbPassWord := viper.GetString("MYSQL.Pwd")
	PDbName := viper.GetString("MYSQL.DatabaseName")

	pathWrite := strings.Join([]string{PDbUser, ":", PDbPassWord, "@tcp(", PDbHost, ":", PDbPort, ")/", PDbName, "?charset=utf8&parseTime=true"}, "")
	db, err := gorm.Open(mysql.Open(pathWrite), &gorm.Config{})
	if err != nil {
		panic(err)
	}

	sqlDB, _ := db.DB()
	sqlDB.SetMaxIdleConns(20)
	sqlDB.SetMaxOpenConns(100)
	sqlDB.SetConnMaxLifetime(time.Second * 30)
	_dbMaster = db
}

直接连接到主数据库,用户有写入操作的时候就用这个连接进行读写

  • 下面是读写分离的连接
func initDatabase() {
	PDbHost := viper.GetString("MYSQL.HostName")
	PDbPort := viper.GetString("MYSQL.Port")
	PDbUser := viper.GetString("MYSQL.UserName")
	PDbPassWord := viper.GetString("MYSQL.Pwd")
	PDbName := viper.GetString("MYSQL.DatabaseName")

	SDbHost := viper.GetString("MYSQLRead.HostName")
	SDbPort := viper.GetString("MYSQLRead.Port")
	SDbUser := viper.GetString("MYSQLRead.UserName")
	SDbPassWord := viper.GetString("MYSQLRead.Pwd")
	SDbName := viper.GetString("MYSQLRead.DatabaseName")

	pathWrite := strings.Join([]string{PDbUser, ":", PDbPassWord, "@tcp(", PDbHost, ":", PDbPort, ")/", PDbName, "?charset=utf8&parseTime=true"}, "")
	pathRead := strings.Join([]string{SDbUser, ":", SDbPassWord, "@tcp(", SDbHost, ":", SDbPort, ")/", SDbName, "?charset=utf8&parseTime=true"}, "")

	db, err := gorm.Open(mysql.Open(pathWrite), &gorm.Config{})
	if err != nil {
		panic(err)
	}
	sqlDB, _ := db.DB()
	sqlDB.SetMaxIdleConns(20)
	sqlDB.SetMaxOpenConns(100)
	sqlDB.SetConnMaxLifetime(time.Second * 30)
	_db = db
	_ = _db.Use(dbresolver.
		Register(dbresolver.Config{
			Sources:  []gorm.Dialector{mysql.Open(pathWrite)}, // 写操作
			Replicas: []gorm.Dialector{mysql.Open(pathRead)},  // 读操作,headless自动选择
			Policy:   dbresolver.RandomPolicy{},               // sources/replicas 负载均衡策略
		}))
}

把写操作指定到主数据库,都操作指定到从数据库。

记录写操作

为了不引入其他复杂的,我们这里还是在mysql中记录哪个用户进行了写操作。
添加下面这个数据结果记录写操作的用户:

type PinningUser struct {
    gorm.Model
    UserId uint
}

结构很简单,就是记录了一些用户的id。
每当用户进行写操作的时候,就把用户id记录下来。超出指定时间之后,就删除这个记录。定时删除这部分代码没有在此进行实现。

进行测试

	if rc1Cmd != 0 { // 有写操作的从主数据库读取
		rcCheck(1, rc1Cmd)
		log.Infof("rc1 check done")
	} else if rc2Cmd != 0 { // 全部从从数据库读取
		rcCheck(2, rc2Cmd)
		log.Infof("rc2 check done")
	} else if userCmd != 0 { // 创建用户
		createUser(userCmd)
	}

这里会根据命令行参数进行操作选择

  • 创建用户使用
    • ./main -u 1000
  • 测试只进行读写分离
    • ./main -c2 100
  • 使用Pinning User to Master这个方法
    • ./main -c1 1000

代码差异

我们先看读写分离的方法

// 全部从节点读取,测试能否复现这个问题
func readUser(userId uint) string {
	user := User{}
	if err := _db.Where("id = ?", userId).
		Find(&user).Error; err != nil {
		log.Errorf("readUser, id:%v, err:%v", userId, err)
		return ""
	} else {
		return user.Email
	}

}

写完数据直接从从节点进行数据读取,这里可能会存在数据没同步到从节点的情况,从而导致读取不到数据。
下面的测试结果也可以印证这个问题

# ./main -c2 100
INFO[0000] register table success
INFO[0000] command: rc1Cmd:0, rc2Cmd:100, userCmd:0
ERRO[0000] rcCheck2, fail, modId:487, modStr:bb6fe76a-6642-11ed-9ace-be2e1b32fdf3, readStr:
ERRO[0000] rcCheck2, fail, modId:780, modStr:bb77e11b-6642-11ed-9ace-be2e1b32fdf3, readStr:
ERRO[0000] rcCheck2, fail, modId:373, modStr:bb872e31-6642-11ed-9ace-be2e1b32fdf3, readStr:
ERRO[0000] rcCheck2, fail, modId:352, modStr:bb87e4f9-6642-11ed-9ace-be2e1b32fdf3, readStr:
INFO[0000] rc2 check done

这100次测试里面有4次失败了。


再看Pinning User to Master的方案

// 如果这个用户数据被修改了,就从主节点读取
func readUserRc(userId uint) string {
	user := User{}
	pinUser := int64(0)
	if err := _dbMaster.Model(&PinningUser{}).
		Where("user_id = ?", userId).
		Count(&pinUser).Error; err != nil {
		log.Errorf("readUser, id:%v, err:%v", userId, err)
		return ""
	}

	if pinUser == 0 {
		if err := _db.Where("id = ?", userId).
			Find(&user).Error; err != nil {
			log.Errorf("readUser, from second id:%v, err:%v", userId, err)
			return ""
		} else {
			return user.Email
		}
	} else {
		if err := _dbMaster.Where("id = ?", userId).
			Find(&user).Error; err != nil {
			log.Errorf("readUser, from master id:%v, err:%v", userId, err)
			return ""
		} else {
			return user.Email
		}
	}
}
  • 先从主节点获取数据,查看这个人有没有写操作。
  • 如果有些操作的话,就从主节点读取数据。
  • 如果没有的话,从从节点读取数据。

测试结果如下:

# ./main -c1 100
INFO[0000] register table success
INFO[0000] command: rc1Cmd:100, rc2Cmd:0, userCmd:0
INFO[0000] rc1 check done

# ./main -c1 1000
INFO[0000] register table success
INFO[0000] command: rc1Cmd:1000, rc2Cmd:0, userCmd:0
INFO[0005] rc1 check done

可以看到,每次写完马上读,都能获取到数据。

总结

从实验结果我们可以看到,Pinning User to Master这个方案确实可以解决主从节点同步不及时这个问题。

现在的代码中,有写操作的用户都存在数据库中。这样会导致每次读数据,至少会去主数据库读一次。主数据库的读取压力会增大。
我们可以使用redis之类的缓存来进行优化。把写用户记录到缓存中,去除这一次主数据库的读取。

还有一个问题是,此方案下针对这个写数据用户的所有读取都会走主数据库。但有些数据可以容忍同步不及时,读到老数据。
这种情况下,我们可以对数据进行路径区分。如果这次读取的数据是容忍不及时的,那还是从从数据库读取。要读取最新数据时,才走主节点。
这样可以把部分读取压力还给从数据库。但这种做法把复杂度放到了业务代码这里,业务代码需要对数据进行分类处理。这要看实际业务逻辑中的选择了。

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

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

相关文章

【Matplotlib绘制图像大全】(十三):甜甜圈饼图

前言 大家好,我是阿光。 本专栏整理了《Matplotlib绘制图像大全》,内包含了各种常见的绘图方法,以及Matplotlib各种内置函数的使用方法,帮助我们快速便捷的绘制出数据图像。 正在更新中~ ✨ 🚨 我的项目环境: 平台:Windows10语言环境:python3.7编译器:PyCharmMatp…

Vue2.0开发之——Vue基础用法-vue-cli(30)

一 概述 vue-cli—介绍并安装vue-clivue-cli—基于vue-cli创建vue项目vue-cli—项目预览效果vue-cli—项目目录结构vue-cli—vue项目运行过程vue-cli—组件的基本使用 二 vue-cli—介绍并安装vue-cli 2.1 什么是单页面应用程序 单页面应用程序(英文名&#xff1a…

C语言刷题系列——6.(递归)实现顺序输出整数

递归实现顺序输出整数 ❄️一) 题目要求☃️1.函数接口定义:☃️2.裁判测试程序样例:❄️二) 非递归 解法☃️step1.统计位数☃️step2.循环,打印每一位☃️step3.实现❄️三) 递归 解法☃️step1.分析☃️step2.图解流程☃️step3.实现)❄️…

举个栗子~Tableau 技巧(245):用辅助标识快速查看标靶图

我们经常会使用 标靶图(靶心图)来参照目标评估指标的表现。例如:销售配额评估、实际花费与预算的比较情况、绩效优劣范围( 优/良/差)等等。 指标不多的情况,标靶图其实是较直观的。但如果指标很多&#xf…

面试官:请问如何提升TCP三次握手的性能?

本文主要分享在 Linux 操作系统下,如何优化 TCP 的三次握手流程,提升握手速度。 TCP 是一个可以双向传输的全双工协议,所以需要经过三次握手才能建立连接。三次握手在一个 HTTP 请求中的平均时间占比在 10% 以上,在网络状况不佳、…

请问财务管理的作用有哪些?

财务管理作为企业管理的一个重要组成部分,如何在当前企业经营方式转变过程中发挥出更大的作用,如何进一步促进财务管理理论和方法的改革与发展,从而使财务管理更具有适应性和有效性,是企业管理理论与实务工作者一个共同关心的话题…

常用数据结构 ——— 队列(环形队列和顺序队列)

目录 一、队列简介 二、顺序队列 三、环形队列 四、环形队列代码 1、队列结构体 2、队列初始化 3、判断队列是否为满 4、判断队列是否为空 5、将数据插入到队列中 6、读取队列中的数据 7、释放队列空间 8、功能测试 一、队列简介 队列只允许在队列头(fr…

_Linux(共享内存)

文章目录0. 共享内存1. 共享内存示意图2. 共享内存函数2.1 shmget函数2.2 shmat函数2.3 shmdt函数2.4 shmctl函数2.5 查看共享内存指令2.6 删除共享方法2.6.1 指令删除2.6.2 代码删除3. 实例代码3.0 log.hpp打印日志信息3.1 comm.hpp(shmServer.cc和shmClicent.cc共有文件)3.2 …

Java 17的这些新特性,Java迈入新时代

前言 2021年9月14日Java 17发布,作为新时代的农民工,有必要了解一下都有哪些新东西。 Java 17是Java 11以来又一个LTS(长期支持)版本,Java 11 和Java 17之间发生了那些变化可以在OpenJDK官网找到JEP(Java…

Unity中用Natrue Renderer做自己的地形Terrain.

效果图 一、下载与导入Nature Renderer Nature Renrderer是个强大的插件,它本身就可以作为地形编辑的工具取代Unity的地形细节和树木的渲染系统。 nature-renderer官网 1.下载链接 推荐(已经购买的许可证,可直接使用)&#xf…

设计原则和设计模式01

一:软件设计原则 1.单一职责原则: 有且只有一个原因引起类的变化(类或者接口的职责单一化) 2.里氏替换原则: 子类可以扩展父类的功能,但不能改变父类原有的功能 3.依赖倒置原则: 1.高层模块不应该依赖于底层模块&#xff0c…

Java注解

Java注解(Annotation) Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。 注解也叫元数据,一种代码级别的说明,说明程序的是给计算机看的,与类,接口…

事件绑定(onsubmit)表单提交

事件绑定(onsubmit)表单提交 学习路线:JavaScript基础语法(输出语句)->JavaScript基础语法(变量)->JavaScript基础语法(数据类型)->JavaScript基础语法(运算符&#xff09…

Python笔记 · Python语言的“动态性”

尽管对于Python程序员来说已经司空见惯,但是当那些从非动态语言转过来的程序员初次看到形如self.xxxxxx的语句就是在定义对象属性时往往会感到“离奇”:一个未经声明的(类似private int a;那样)变量,直接从self中“点”…

java知识回顾笔记(对象、反射、内省、实例、父类、构造方法、封装、泛型、super())

类&对象 在创建了一个类时,只声明但不赋值,其默认值为: 理解下图含义,即可理解对象和类: 实例 对象又被称为实例,实例变量被创建时,系统默认会赋值,例如: Studen…

什么知识库工具适合小团队?看看文档管理系统+NAS的最新解决方案

编者按:还在为团队选那款网盘而发愁吗?试试文档管理系统和NAS结合吧,高效率低成本,适合小团队。 关键词:免维护,免安装,大容量,在线编辑,文档共享,数据安全 对于企业或…

LeetCode-66-加一

1、从后向前遍历 我们可以从后向前遍历数组,针对不同的情况进行操作:1、若当前数字不为9,则我们直接将数字的值加一并返回即可;2、若当前数字为9,我们将当前数字置为0并对前一位执行加一操作;3、若所有数字…

后端接口时通时不通,团队全链路排查实战

背景: 1 最近团队做了一套系统,已经临近上线了; 2 后端的服务和前端的代码都是新写的,两边的服务器,数据库也都是新申请的; 3 本来测试的时候用的测试服务器,一切都挺好的,但部署到线…

基于分发与计算的GRTN全球实时传输网络

一张能同时满足「分发」与「计算」需求的网。 从直播趋势看「分发」与「计算」 阿里云直播产品架构图中,主要分为端和云两个部分:在端侧,主要包含推流端和播放端;在云侧,一是基于分布式节点构建的传输网,二…

mosquitto部署mqtt broker 并测试订阅与发布

mosquitto部署mqtt broker 并测试订阅与发布 1,MQTT协议介绍 MQTT(消息队列遥测传输)是ISO 标准(ISO/IEC PRF 20922)下基于发布/订阅范式的消息协议。它工作在 TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型…