当前位置: 首页 > ds >正文

数据库的并发控制

并发控制

12.1 并发级别

问题:交错的读写

并发客户端可以随意进入和退出事务,并在中途请求读取和写入。为了简化分析,假设enter/exit/read/write是原子步骤,因此并发事务只是这些步骤的交错。

我们还将区分只读事务和读写事务:

  • 并发读者并发写者更容易处理。
  • 许多应用程序是读密集型的,因此读性能更为重要。
读者-写者锁(RWLock)

如果不了解如何实现并发控制,可以简单地添加一个互斥锁(mutex)来序列化数据访问。为了提升读性能,可以使用读者-写者锁(RWLock),它允许多个并发读者,但只有一个写者。

  • 如果没有写者,数据不会改变,因此并发读者是可以接受的。
  • 当写者想要进入时,它会等待所有读者离开。
  • 写者会阻塞读者,但读者之间不会相互阻塞。

这种方法的用处有限,因为写者之间没有并发能力,且长时间运行的事务会导致读者和写者互相阻塞。

读-复制-更新(RCU)

为了防止读者和写者互相阻塞,可以让读者和写者各自操作自己的数据版本:

  • 数据有一个指向不可变数据的指针,读者只需抓取快照。
  • 单个写者更新自己的副本,然后翻转指针。

由于我们采用的是写时复制(copy-on-write),这种并发级别是免费获得的。然而,单个写者仍然不足,因为事务的生命周期由客户端控制,可能任意延长。

乐观并发控制

并发写者会导致冲突,例如:

SeqTX1:
read a
write b := a
commitSeqTX2:
write a := 1
commit

TX1依赖于TX2修改的同一键,因此它们不能同时成功。

有些看似“仅写”的操作实际上有读依赖性。例如,我们的更新/删除接口会报告键是否被更新或删除,这取决于键的先前状态。因此以下场景也是冲突:

SeqTX1:
write a := 1
commitSeqTX2:
delete a
commit

一种解决冲突的方法是在检测到冲突时中止事务:

  1. 事务开始。
  2. 读取基于快照,但写入缓冲在本地。
  3. 提交前验证与已提交事务是否有冲突。
  4. 事务结束:
    • 如果有冲突,中止并回滚。
    • 否则,将缓冲的写入转移到数据库。

验证和提交是一个原子步骤。这种方法称为乐观并发控制,因为它假设冲突很少发生,因此不采取任何措施预防冲突。

悲观并发控制(替代方法)

在乐观并发控制中,事务在冲突时无法继续,这对应用程序来说并不友好,因为它们只能不断重试。另一种解决冲突的方法是通过锁定来预防冲突。事务会为其依赖项获取锁,潜在冲突的事务会彼此等待。

这种方法听起来更好,尤其是在最后一个示例中,写/删操作可以无问题地进行。然而,这仍然不能保证进展,因为事务可能会因死锁而失败。

死锁是指两个参与者彼此等待对方释放自己持有的锁。如果依赖图中存在循环,超过两个参与者也可能发生死锁。在并发编程中,锁应按预定义顺序获取以避免循环。但对于数据库而言,客户端可以以任意顺序获取锁,因此数据库必须检测并解决死锁,这是一个图问题。


12.2 为读者实现快照隔离

隔离级别指的是事务如何看待其他事务的更改。对于写时复制来说这不是问题,因为每个事务都操作B+树的快照。

捕获本地更新

事务保持数据库的快照和本地更新:

  • 快照只是一个根指针(写时复制)。
  • 本地更新保存在一个内存中的B+树中。
type KVTX struct {snapshot BTree // 只读快照pending  BTree // 捕获的KV更新,值通过1字节标志表示删除的键
}

快照和本地更新都在事务开始时初始化:

func (kv *KV) Begin(tx *KVTX) {tx.snapshot.root = kv.tree.roottx.snapshot.get = ... // 从mmap页面读取pages := [][]byte(nil)tx.pending.get = func(ptr uint64) []byte { return pages[ptr-1] }tx.pending.new = func(node []byte) uint64 {pages = append(pages, node)return uint64(len(pages))}tx.pending.del = func(uint64) {}
}
读取自己的写入

在事务内,客户端应该能够读取刚刚写入的内容,即使尚未提交。查询应先检查KVTX.pending再检查KVTX.snapshot。这就是为什么写入保存在B+树中而不是列表中。

func (tx *KVTX) Get(key []byte) ([]byte, bool) {val, ok := tx.pending.Get(key)switch {case ok && val[0] == FLAG_UPDATED: // 在此事务中更新return val[1:], truecase ok && val[0] == FLAG_DELETED: // 在此事务中删除return nil, falsecase !ok: // 从快照中读取return tx.snapshot.Get(key)default:panic("unreachable")}
}

对于范围查询,新增了一个迭代器类型来合并两棵树。

空闲列表中的版本号

由于读者可能持有旧版本的数据库,空闲列表不能释放这些版本中的页面。我们通过为每个版本分配单调递增的版本号(也称为时间戳)来解决这个问题。

type FreeList struct {maxVer uint64 // 最老的读者版本curVer uint64 // 提交时的版本号
}

12.3 处理写者的冲突

检测历史冲突

所有读取都被添加到KVTX.reads中(点查询和范围查询)。

type KVTX struct {reads []KeyRange
}

每次成功提交都会添加到KV.history中。

type KV struct {history []CommittedTX
}

冲突检测通过检查依赖关系和历史记录之间的重叠来进行:

func detectConflicts(kv *KV, tx *KVTX) bool {for i := len(kv.history) - 1; i >= 0; i-- {if !versionBefore(tx.version, kv.history[i].version) {break}if rangesOverlap(tx.reads, kv.history[i].writes) {return true}}return false
}

代码仓库地址:database-go

序列化内部数据结构

在分析中,事务被简化为交错的步骤。在现实中,这些步骤可以并行运行,因此需要序列化共享的KV结构。

type KV struct {mutex sync.Mutex
}

尽管可以通过锁保护所有KVTX方法,但也可以减少锁定。例如:

  • 写入仅操作KVTX.pending,不会触及KV。
  • 读取仅触及KV.mmap.chunks,这是mmap返回的切片。

提交步骤可能会修改KV.mmap.chunks,因此每个事务使用一个本地副本。

通过这种方式,读写方法不需要加锁,并且可以并行运行。这很好,因为读取操作可能会触发页面错误(page faults)并阻塞线程。

到目前为止,只有BeginCommitAbort是序列化的。但考虑到Commit涉及I/O操作,我们可以通过在等待I/O时释放锁来进一步优化,以允许其他事务进入,并让只读事务退出。提交步骤仍然需要通过另一个锁与其他提交步骤序列化。这一点留作练习。

http://www.xdnf.cn/news/4191.html

相关文章:

  • nats v2.11.3全新上线!MQTT支持增强、JetStream性能优化、关键BUG修复,构建高效可信消息中间件新时代
  • NV287NV291美光固态闪存NV293NV294
  • Deepseek基础-api key申请及应用(java)、硅基流动api key申请及应用(dify)
  • ThreadLocal源码深度剖析:内存管理与哈希机制
  • Lora原理介绍并用Macbook air超快实现本地微调小模型
  • AI日报 · 2025年5月05日|雅诗兰黛与微软合作成立 AI 创新实验室,加速美妆产品研发与营销
  • 【言语理解】片段阅读之下文推断(6)
  • 设计模式每日硬核训练 Day 18:备忘录模式(Memento Pattern)完整讲解与实战应用
  • 全球化电商平台AWS云架构设计
  • 矩阵置零(中等)
  • 设计模式-基础概念学习总结(继承、多态、虚方法、方法重写)
  • 深入理解块级格式化上下文(BFC)
  • 文本三剑客
  • 字符串匹配 之 拓展 KMP算法(Z算法)
  • 数据集-目标检测系列- 印度人脸 检测数据集 indian face >> DataBall
  • 深度解析:从 GPT-4o“谄媚”到 Deepseek“物理腔”,透视大模型行为模式的底层逻辑与挑战
  • Unity:AddTorque()(增加旋转力矩)
  • uniapp 云开发全集 云数据库
  • JavaScript 笔记 --- part7 --- JS进阶 (part2)
  • 【信息系统项目管理师-论文真题】2008上半年论文详解(包括解题思路和写作要点)
  • Python生活手册-NumPy数组创建:从快递分拣到智能家居的数据容器
  • 互联网大厂Java求职面试:AI大模型与云原生架构设计深度解析
  • 【学习心得】Xtuner模型qlora微调时错误记录
  • 【嘉立创EDA】FPCB(Flexible-PCB)柔性软板设计如何增加补强层
  • 反常积分(广义积分)
  • Redis总结(六)redis持久化
  • C++ 适配器模式详解
  • Java中使用Lock简化同步机制
  • 安装SDL和FFmpeg
  • 强化学习ppo算法在大语言模型上跑通