VLL: a lock manager redesign for main memory database systems阅读

为何要有VLL?VLL旨在解决什么问题?

在数据库系统中,锁是广泛使用的并发控制机制。然而对于内存数据库系统,锁管理器却成为了性能瓶颈所在。

一项研究说明内存数据库中有16%~25%的时间用于与锁管理器的交互

在传统的锁管理器中,最常见的实现方式是采用以数据主键为id,请求该数据的锁请求链表为值的hash表,锁请求链表还存在锁头追踪该项的锁状态,以及支持互斥操作。

对于与磁盘相关的数据库,这些hash表查找、锁获取和链表操作都是可以忽略不计的,可对于内存数据库来说,这些额外的内存访问、cache未命中、cpu周期、以及临界区就可能接近或者超过执行实际事务逻辑的成本

VLL在上述锁管理器上做出了两个主要变化

  • 将锁信息从中心hash表中移出到原始数据中
  • 从锁数据结构中删除哪些事务对特定锁未完成请求的信息,取而代之的是原始数据包含未完成的写、读请求的数量(CX记录未完成的写请求数量、CS记录未完成的读请求数量)

在事务获取锁时,会一次请求所有锁,并按照请求锁的顺序对事务排序,按照这种全局事务顺序来确定哪个事务应该先解除堵塞并执行

可是VLL也并非银弹,在高争用负载下,可能会导致并发减少和CPU利用率差。为解决这个问题,又引入了选择性竞争优化(SCA),在需要的时候高效计算最有用的竞争信息子集

VLL算法介绍

锁结构

VLL锁管理结构由

  • 储存到每个原始数据的的未完成锁写请求数量CX和未完成锁读请求数量CS
  • 中心事务请求全局队列TxnQueue

组成

流程

具体流程如下

  1. 当事务到达分区时,会尝试对该分区所有在其生命周期内访问的记录加锁

    每个锁请求都采取相应记录的CX或CS加1的形式

  2. 如果请求后的CX=1且CS=0,则认为独占锁被请求事务抢占

    如果请求后的CX=0,则认为请求事务获取了共享锁

  3. 当事务获取锁之后就被添加到TxnQueue

    请求锁和向队列添加事务都发生在同一临界区中

  4. 在离开临界区时,若事务在请求时获取到所有锁,则该事务为free,否则为堵塞。那么只有free事务才能够立即执行

问题

考虑以下问题

  1. 单线程VLL在需要其他节点协作的事务请求中,如果一直等待,会导致资源的浪费
  2. 在没有中心锁管理结构情况下,如何释放等待中的堵塞事务执行呢?
  3. 如果在CX=0的情况下才能获取锁,而写请求到达之后都能递增CX,那么在存在多个写请求事务堵塞情况下,CX会永远大于0
  4. 随着TxnQueue大小增长,新事务能立刻获取锁的概率大大降低

为解决这些问题

  1. 引入waiting状态,标识事务正在进行中,但需要获取搭配远程结果才能继续执行。因此在进入waiting状态之后就可以挂起,不会导致额外的资源浪费
  2. 让后台线程检查TxnQueue中每个堵塞事务的请求数据CX、CS值,若CX下降到1,CS为0,则说明该事务获取到互斥锁;CX为0,CS大于0,则说明获取到共享锁
  3. 对于队列头的堵塞事务直接解除堵塞并执行。这是因为事务位于队列头,说明在此之前请求锁的事务都已经完结了
  4. 为TxnQueue设立阈值,当超过后,将会停止处理新事务,从TxnQueue中寻找可以被解除堵塞的事务。而在阈值内则会优先处理新请求事务
代码及流程展示

以下是代码展示

func beginTransaction(t *TxnItem) {
	// 开始时,事务状态为Free
	t.TxnStatus = Free

	// 遍历本地读集合,检查是否存在冲突,若存在冲突,则事务状态为Blocked
	for _, readKey := range t.ReadSet {
		if inLocal(readKey) {
			data[readKey].cs++
			if data[readKey].cx > 1 {
				t.TxnStatus = Blocked
			}
		}
	}

	// 遍历本地写集合,检查是否存在冲突,若存在冲突,则事务状态为Blocked
	for _, writeKey := range t.WriteSet {
		if inLocal(writeKey) {
			data[writeKey].cx++
			if data[writeKey].cs > 0 || data[writeKey].cx > 1 {
				t.TxnStatus = Blocked
			}
		}
	}

	// 若事务状态为Free,且事务为分布式事务,则事务状态为Waiting
	if t.TxnType == TxnTypeDistributed && t.TxnStatus == Free {
		t.TxnStatus = Waiting
	}
}

func commitTransaction(t *TxnItem) {
	// 提交事务时,释放读集合的锁
	for _, readKey := range t.ReadSet {
		if inLocal(readKey) {
			data[readKey].cs--
		}
	}

	// 提交事务时,释放写集合的锁
	for _, writeKey := range t.WriteSet {
		if inLocal(writeKey) {
			data[writeKey].cx--
		}
	}
}

func vll() {
	for {
		// 若接收到远程事务消息,则处理该事务的消息,并执行事务、提交事务以及从队列中移除该事务
		t, ok, message := selectReceivedMessageTx()
		if ok {
			handleReadResult(t, message)
			if isReadyToExecute(t) {
				execute(t)
				commitTransaction(t)
				txnQueue.dequeue(t)
			}
			continue
		}

		// 若队列头事务堵塞,在分布式事务时,设置状态为等待,并给其他参与节点发送事务请求
		// 否则,设置状态为空闲,执行事务、提交事务以及从队列中移除该事务
		if txnQueue.front().TxnStatus == Blocked {
			t = txnQueue.front()
			if t.TxnType == TxnTypeDistributed {
				t.TxnStatus = Waiting
				// 给其他参与节点发送事务请求
			} else {
        // 注意!此处,对于队列头的堵塞事务直接解除堵塞并执行
        // 这是因为事务位于队列头,说明在此之前请求锁的事务都已经完结了。并且考虑到存在多个事务增加同数据CX值的情况,使得CX总是会大于1,导致死锁
				t.TxnStatus = Free
				execute(t)
				commitTransaction(t)
				txnQueue.dequeue(t)
			}
		} else if !txnQueue.isFull() {
			// 若队列未满,则获取新事务请求
			// 新事务状态为Free,执行事务、提交事务
			// 若事务状态为Waiting,给其他参与节点发送事务请求,并入队
			// 若事务状态为Blocked,入队
      // 注意!检查未满的缘故在于平衡新旧事务的处理。在阈值内优先处理新事务请求,阈值之外优先处理waiting的事务
			t = getNewTxnRequest()
			beginTransaction(t)
			switch t.TxnStatus {
			case Free:
				execute(t)
				commitTransaction(t)
			case Waiting:
				// 给其他参与节点发送事务请求
				txnQueue.enqueue(t)
			case Blocked:
				txnQueue.enqueue(t)
			}
		}
	}
}

考虑下面场景下的演变,表中展示的是各事务请求的数据

txnread setwrite set
Ax, yx
Bx, yx
Cx0
Dyz
E0y
  1. 最开始,所有数据的CX、CS都是0,TxnQueue为空
  2. 事务A发起读取x、y,并写入x请求,此时数据x的CX递增,数据y的CS递增,并且事务A作为free事务加入TxnQueue
  3. 事务B发起x、y读,x写的请求。当由于x.CX>0,所以B无法获取到x的写锁,因此作为堵塞事务加入到TxnQueue
  4. 当A完成执行后,释放锁。此时x.CX–、y.CS–,并将A从TxnQueue移除,B而后标记为free,获取锁开始执行
  5. 事务C发起读取x的请求,因此x.CS++,由于x.CX>0,因此事务C的读请求无法保证,作为堵塞事务加入到TxnQueue
  6. 事务D发起对y读请求,对z写请求。那么y.CS++,z.CX=1,由于y.CX和z.CX此前均为0,事务D所有锁获取成功,并作为free事务加入TxnQueue
  7. 事务E发起对y的写请求,由于y.CS>0,所以E请求失败,作为堵塞事务加入到TxnQueue
  8. 事务B执行完毕后,释放x写锁、y读锁,并从TxnQueue移除,C此时位于TxnQueue队列最前面,因此标记为free,成功获取到x读锁并执行
  9. 最后事务E也能够安全执行,因为不与TxnQueue中在其之前的任何事务冲突。但又由于VLL算法仅允许TxnQueue最前面事务执行,所以还是不能够执行。SCA将解除该限制
array VLL

array VLL采用向量或数组的数据结构来连续储存锁信息,这会使得数据和锁信息是在两次单独请求中进行的,但在单独线程中,由于在核心缓存中,所以也能在性能上有着较好的发挥

一次性获取所有锁的障碍
  • 在运行事务前,事务读写集可能是未知的
  • 由于每个节点都有TxnQueue,并且临界区在本地,那么不同的节点可能处理顺序并不一致,这可能会导致死锁

为解决第一个问题,允许事务在进入临界区前,能给执行所需要的任何读取

为解决节点事务顺序不一致引发的死锁问题,有两种解决方法

  • 允许产生死锁,通过使用死锁检测协议取消死锁事务
  • 协调节点,使各节点事务顺序一致

VLL采用协调节点的方式,可以在增加协调开销的情况下,仍然产生改进的性能

SCA——提升在高争用下的性能

对于高竞争和高百分比的多分区工作负载,VLL引入了SCA,进行模拟标准锁管理器去检测哪些事务应该继承已释放锁的能力

SCA只在真正需要的时候才会激活,比如在CPU处理空闲时。从队列头往尾部进行扫描,分析在事务之前的事务是否存在冲突

func sca(q TxnQueue) *TxnItem {
	// 初始化冲突检测数组
	dx := make([]int8, 100<<10)
	ds := make([]int8, 100<<10)
	for _, item := range q {
		// 如果事务被阻塞
		if item.TxnStatus == Blocked {
			success := true
			// 检查读集合,若读集合中的数据已经被写占用,则事务失败
			for _, read := range item.ReadSet {
				key := hash(read)
				if dx[key] == 1 {
					success = false
				}
				// 读集合中的数据标记为已占用
				ds[key] = 1
			}

			// 检查写集合,若写集合中的数据已经被读、写占用,则事务失败
			for _, write := range item.WriteSet {
				key := hash(write)
				if ds[key] == 1 || dx[key] == 1 {
					success = false
				}
				// 写集合中的数据标记为已占用
				dx[key] = 1
			}
			
			// 若事务成功,则返回该事务
			if success {
				return item
			}
		}else {
			// 事务未被阻塞,仅标记读写集合数据被占用
			for _, read := range item.ReadSet {
				key := hash(read)
				ds[key] = 1
			}
			
			for _, write := range item.WriteSet {
				key := hash(write)
				dx[key] = 1
			}
		}
	}
	
	return nil
}

VLLR——支持范围的VLL

对于频繁读取、写入、删除同一事务中的许多连续行,锁定行范围比锁定多个单独行更有用,可以避免幻读以及在删除一个数据行共享其主键的公共前缀的实体的常见用例中特别有用

VLLR通过锁定位串前缀进行范围锁定。当锁定一个范围主键时,将该范围表示为集合R,然后生成集合P,使得R中任何一个元素都能在P中找到前缀。最简单生成P的方式时选择R中最小和最大位串的最长公共前缀

在得到集合P之后,VLLR采用分层锁的方式进行。即先从数据库获取意图锁,而后从表获取意图锁,接着在页获取意图锁,最后获取行锁

此外,为了更好管理锁状态,VLLR在CX、CS的基础上,引入了P集合的LX、LS计数器,也就是如果当前请求属于集合P的写请求,则LX递增,属于集合P的读请求,则LS递增

func requestLock(item *TxnItem, rg *txnRange, exclusive bool) bool {
	// 事务加入队列
	txnQueue.enqueue(item)

	// 获取锁范围的前缀
	pfs := rg.toPrefix()

	// 请求前缀锁
	success := true
	for _, pf := range pfs {
		success = success && requestPrefixLock(pf, exclusive)
	}
	return success
}

func requestPrefixLock(key string, exclusive bool) bool {
	success := true
	k := hash(key)

	// 检查当前key数据锁是否不可获取
	if exclusive {
		success = success && (cs[k] == 0)
	}
	success = success && (cx[k] == 0)

	// 检查key数据所在前缀锁是否不可获取
	if exclusive {
		success = success && (ls[k] == 0)
	}
	success = success && (lx[k] == 0)

	// 检查key所有前缀的数据锁是否不可获取
	for i := 1; i < len(key); i++ {
		h := hash(key[:i])
		success = success && (cx[h] == 0)
		if exclusive {
			success = success && (cs[h] == 0)
		}
	}

	// 标记key数据锁
	if exclusive {
		cx[k]++
	} else {
		cs[k]++
	}

	// 标记key数据所有前缀的前缀锁
	for i := 1; i < len(key); i++ {
		h := hash(key[:i])
		if exclusive {
			lx[h]++
		} else {
			ls[h]++
		}
	}

	return success
}

func releaseLock(item *TxnItem, rg *txnRange, exclusive bool) {
	// 释放锁范围的前缀
	pfs := rg.toPrefix()
	for _, pf := range pfs {
		releasePrefixLock(pf, exclusive)
	}

	// 事务出队列
	txnQueue.dequeue(item)
}

func releasePrefixLock(key string, exclusive bool) {
	// 释放key数据锁
	k := hash(key)
	if exclusive {
		cx[k]--
	} else {
		cs[k]--
	}

	// 释放key数据所有前缀的前缀锁
	for i := 1; i < len(key); i++ {
		h := hash(key[:i])
		if exclusive {
			lx[h]--
		} else {
			ls[h]--
		}
	}
}

Ref

  1. https://www.cs.umd.edu/~abadi/papers/vldbj-vll.pdf

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

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

相关文章

二分查找的时间复杂度的讲解

二分查找的代码&#xff1a; 二分查找的时间复杂度&#xff1a; 最坏的情况&#xff1a; 就是找不到和查找区间只剩一个值的时候&#xff0c;这两种都是最坏的结果&#xff0c;假设查找了x次&#xff0c;达到了最坏的结果&#xff1a; N代表每一次折半区间数据的个数&#xf…

当你拥有Xbox-GamePass就能更快体验NewGame

如果你有游戏通行证终极通行证&#xff0c;那么你就可以看到很多预售的游戏&#xff0c;以及更多游戏内容。 Shadow of the Tomb Raider: Definitive Edition《古墓丽影:暗影&#xff08;终极版&#xff09;》 征服残酷无情的丛林&#xff0c;并活着走出来。探索充满裂隙和幽深…

I2C,UART,SPI(STM32、51单片机)

目录 基本理论知识&#xff1a; 并行通信/串行通信&#xff1a; 异步通信/同步通信&#xff1a; 半双工通信/全双工通信: UART串口&#xff1a; I2C串口&#xff1a; SPI串口&#xff1a; I2C在单片机中的应用&#xff1a; 软件模拟&#xff1a; 51单片机&#xff1a;…

Linux的进程管理

进程 程序运行在操作系统中&#xff0c;是被操作系统所管理的。 为管理运行的程序&#xff0c;每一个程序在运行的时候&#xff0c;便被操作系统注册为系统中的一个&#xff1a;进程 并会为每一个进程都分配一个独有的&#xff1a;进程ID&#xff08;进程号&#xff09; 查看…

C++进阶——继承

前言&#xff1a;从这篇文章开始&#xff0c;我们进入C进阶知识的分享&#xff0c;在此之前&#xff0c;我们需要先来回顾一个知识&#xff1a; C语言有三大特性&#xff0c;分别是封装、继承和多态&#xff0c;而我们前边所分享的各种容器类&#xff0c;迭代器等&#xff0c;…

基于SpringBoot的“线上教学平台”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“线上教学平台”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 线上教学平台结构图 管理员登录界面图 学员管理界…

网络工程师-----第一天

线缆与进制转换 进制转换: 1.十进制&#xff1a; 都是以0-9这九个数字组成&#xff0c;不能以0开头。 2.二进制&#xff1a; 由0和1两个数字组成。 3.八进制&#xff1a; 由0-7数字组成&#xff0c;为了区分与其他进制的数字区别&#xff0c;开头都是以0开始。 4.十六进制…

Python数据结构【二】查找

前言 可私聊进一千多人Python全栈交流群&#xff08;手把手教学&#xff0c;问题解答&#xff09; 进群可领取Python全栈教程视频 多得数不过来的计算机书籍&#xff1a;基础、Web、爬虫、数据分析、可视化、机器学习、深度学习、人工智能、算法、面试题等。 &#x1f680;&a…

手动实现简易版RPC(下)

手动实现简易版RPC(下) 前言 什么是RPC&#xff1f;它的原理是什么&#xff1f;它有什么特点&#xff1f;如果让你实现一个RPC框架&#xff0c;你会如何是实现&#xff1f;带着这些问题&#xff0c;开始今天的学习。 接上一篇博客 手动实现简易版RPC&#xff08;上&#xff…

抖音小店运营计划表年度电商规划管理模板

【干货资料持续更新&#xff0c;以防走丢】 抖音小店运营计划表年度电商规划管理模板 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 抖音店铺运营表格 &#xff08;完整资料包含以下内容&#xff09; 目录 抖音店铺运营计划&#xff1a; 一、店铺定位与目标…

MySql运维篇

目录 一.日志 1.1日志分类 1.2Error Log 1.3BinaryLog 1.4SlowQuery Log 二.备份 2.1备份原因 2.2备份目标 2.3备份技术 2.3.1物理备份 2.3.2逻辑备份 2.4备份方式 2.4.1完全备份 2.4.2增量备份 2.4.3差异备份 2.5备份环境准备 2.6完全备份实验 2.6.1完全备…

书生·浦语大模型全链路开源体系-第4课

书生浦语大模型全链路开源体系-第4课 书生浦语大模型全链路开源体系-第4课相关资源XTuner 微调 LLMXTuner 微调小助手认知环境安装前期准备启动微调模型格式转换模型合并微调结果验证 将认知助手上传至OpenXLab将认知助手应用部署到OpenXLab使用XTuner微调多模态LLM前期准备启动…

连锁服装卖场进销存一般怎么管理

连锁服装卖场的进销存管理是保证业务顺畅运作和最大化利润的关键之一。随着市场竞争的加剧和消费者需求的变化&#xff0c;良好的进销存管理能够帮助企业及时调整库存&#xff0c;减少滞销品&#xff0c;提高资金周转率&#xff0c;从而增强市场竞争力。本文将探讨连锁服装卖场…

单独设置浏览器滚动条上下箭头

解决方法 重点 ::-webkit-scrollbar-button:vertical 给垂直方向的滚动条设置样式 ::-webkit-scrollbar-button:vertical:start 上方向的按钮 ::-webkit-scrollbar-button:vertical:start:decrement 上方向单个按钮 下方向同理 不知道为啥搜索出来的single-button不生效&#…

制造业的数字化转型如何做?

随着科技的迅速发展&#xff0c;数字化转型已经成为制造型企业提高竞争力的关键因素。它可以帮助制造型企业&#xff0c;在产品优化设计、材料采购、生产流程方面实现精细化管理&#xff1b;提升上下游协同生产能力&#xff0c;提高生产效率、降低生产成本、优化产品质量&#…

华为的AI战略地图上,才不是只有大模型

大模型火热了一年&#xff0c;现在还没做AI化改造的企业&#xff0c;就像是工业革命浪潮伊始与火车赛跑的那辆马车。 最早的蒸汽火车缓慢又笨重&#xff0c;甚至铁轨上还预留了马匹行走的空间&#xff0c;以便随时用马拉火车来替代蒸汽火车&#xff0c;一辆华丽的马车试图和火…

浮点数的存储方式、bf16和fp16的区别

目录 1. 小数的二进制转换2. 浮点数的二进制转换3. 浮点数的存储3.1 以fp32为例3.2 规约形式与非规约形式 4. 各种类型的浮点数5. BF16和FP16的区别Ref 1. 小数的二进制转换 十进制小数转换成二进制小数采用「乘2取整&#xff0c;顺序排列」法。具体做法是&#xff1a;用 2 2…

C++语言·类和对象

1. 类的引入 C语言结构体中只能定义变量&#xff0c;但在C中&#xff0c;结构体内不仅可以定义变量&#xff0c;也可以定义函数&#xff0c;同时C中struct的名称就可以代表类型&#xff0c;不用像C那样为了方便还要typedef一下。 在C中我们管定义的结构体类型叫做类(student)&a…

idea 将项目上传到gitee远程仓库具体操作

目录标题 一、新建仓库二、初始化项目三、addcommit四、配置远程仓库五、拉取远程仓库内容六、push代码到仓库 一、新建仓库 新建仓库教程 注意&#xff1a;远程仓库的初始文件不要与本地存在名字一样的文件&#xff0c;不然拉取会因为冲突而失败。可以把远程一样的初始文件删…

汇舟问卷:推荐一个挣外国人钱项目

在海外&#xff0c;问卷调查作为一种普遍的市场研究手段&#xff0c;它们能够为企业下一季度的营销策略调整提供有力的数据支撑。 每份问卷的报酬金额各不相同&#xff0c;最低为1美元&#xff0c;最高可以达到10几美元。大多数问卷的报酬在3到5美元之间。 然而&#xff0c;在…
最新文章