rosedb 事务实践

一、前言

事务是传统关系型数据库中必不可少的功能,例如 Mysql、Oracle、PostgreSql 都支持事务,但是在 NoSQL 数据库中,事务的概念比较弱化,在实现上也没有关系型数据库那么复杂。

但是为了数据的完整一致性,大多数 k-v 都会实现事务的基本特性,例如 k-v 数据库的两大鼻祖 LevelDB 和 RocksDB,一些 Go 语言实现的开源 k-v 也都支持事务,例如 Bolt,Badger 等。

rosedb 的事务目前刚实现了一个初级的版本,代码还比较简单,只不过在我的预期构思内,后续可能会慢慢演化得更加复杂。

需要说明的是,在实现 rosedb 的事务之前,我对事务的理解也仅限于 ACID 这些基础概念,所以这次实现完全是摸着石头过河,可能存在一些槽点,大家有什么疑问可以指出来,我后面也会继续学习并完善。

二、基本概念

说到事务,就很容易想到事务的 ACID 特性,带大家回顾一下:

原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部失败,不会在中间环节结束。如果事务执行过程中发生错误,能够被回滚至事务开始之前的状态。•一致性(Consistency):在事务开始前和结束后,数据库的完整性没有被破坏,这意味着数据状态始终符合预期。•隔离性(Isolation):隔离性描述的是多个执行中的事务相互影响的程度,有常见的四种隔离级别,表示事务之间不同的影响程度:•读未提交(read uncommitted):一个事务还未提交,另一个事务就能看到它所做的修改(存在脏读)•读提交(read committed):一个事务对数据的修改,只能等到它提交之后,其他事务才能看到(没有脏读,但是不可重复读)•可重复读(repeatable read):一个事务在执行过程中获取到的数据,和事务开始时的数据一致(没有脏读,可以重复读,但是有幻读)•串行化(serializable):读写互斥,避免事务并发,一个事务必须等到前一个事务提交后才能执行(无脏读,可重复读,无幻读)•持久性(Durability):一个事务提交之后,它所做的修改是永久的,即使数据库崩溃之后也能够保证安全。

ACID 的概念看起来挺多,但并不难理解,要实现事务,其实就是保证在数据读写时,满足事务的这几个基本概念,其中 AID 是必须保证的。

而 Consistency 即一致性,可以简单理解为它就是事务的最终目标,数据库通过 AID 来保证一致性,而我们在应用层面也要保证一致性,假如我们写入的数据本身逻辑上就是错误的,那么即使数据库事务再完善,也无法保证一致性。

三、具体实现

在讲解事务实现之前,先来看看 rosedb 当中事务的基本用法:

// 打开数据库实例
db, err := rosedb.Open(rosedb.DefaultConfig())
if err != nil {
   panic(err)
}

// 在事务中操作数据
err = db.Txn(func(tx *Txn) (err error) {
   err = tx.Set([]byte("k1"), []byte("val-1"))
   if err != nil {
      return
   }
   err = tx.LPush([]byte("my_list"), []byte("val-1"), []byte("val-2"))
   if err != nil {
      return
   }
   return
})

if err != nil {
   panic(fmt.Sprintf("commit tx err: %+v", err))
}

首先还是会打开一个数据库实例,然后调用 Txn 方法,这个方法的入参是一个函数,事务的操作都在这个函数中完成,在提交的时候一次性执行。

像这样使用的话,事务会自动提交,当然也可以手动开启事务并提交,并且在有错误发生时手动回滚,如下:

// 打开数据库实例
db, err := rosedb.Open(rosedb.DefaultConfig())
if err != nil {
   panic(err)
}

// 开启事务
tx := db.NewTransaction()
err = tx.Set([]byte("k1"), []byte("val-1"))
if err != nil {
   // 有错误发生时回滚
   tx.Rollback()
   return
}

// 提交事务
if err = tx.Commit(); err != nil {
   panic(fmt.Sprintf("commit tx err: %+v", err))
}

当然还是推荐第一种用法,省去了手动提交事务和回滚。

Txn 方法表示的是读写事务,此外还有一个 TxnView 方法,表示的是只读事务,使用方式完全一致,只不过在 TxnView 方法内的写入命令都会被忽略。

db.TxnView(func(tx *Txn) error {
   val, err := tx.Get([]byte("k1"))
   if err != nil {
      return err
   }
   // 处理 val

   hVal := tx.HGet([]byte("k1"), []byte("f1"))
   // 处理 hVal
  
   return nil
})

了解了事务的 ACID 基本概念和 rosedb 事务基本用法之后,再来看看在 rosedb 当中,事务究竟是怎么实现的,也可以认为是如何来保证 AID 特性的。

3.1 原子性

前面已经说到,原子性指的是的事务执行的完整性,要么全部成功,要么全部失败,不能停留在中间状态。

要实现原子性其实不难,可以借助 rosedb 的写入特性来解决。先来回顾一下 rosedb 数据写入的基本流程,两个步骤:首先数据会先落磁盘,保证可靠性,然后更新内存中的索引信息。

对于一个事务操作,要保证原子性,可以先将需要写入的数据在内存中暂存,然后在提交事务的时候,一次性写入到磁盘文件当中。

这样存在一个问题,那就是在批量写入磁盘的时候出错,或者系统崩溃了怎么办?也就是说可能有一些数据已经写入成功,有一些写入失败了。按照原子性的定义,这一次事务没有提交完成,是无效的,那么应该怎么知道已经写入的数据是无效的呢?

目前 rosedb 采用了一种最容易理解,也是比较简单的一种办法来解决这个问题。

具体做法是这样的:每一次事务开始时,都会分配一个全局唯一的事务 id,需要写入的数据都会带上这个事务 id 并写入到文件。当所有的数据写入磁盘完成之后,将这个事务 id 单独存起来(也是写入到一个文件当中)。

在数据库启动的时候,会先加载这个文件中的所有事务 id,维护到一个集合当中,称之为已提交的事务 id。

这样的话,就算数据在批量写入时出错,由于没有存放对应的事务 id,所以在数据库启动并取出数据构建索引的时候(回忆一下 rosedb 的启动流程),能够检查到数据对应的事务 id 没有在已提交事务 id 集合当中,所以会认为这些数据无效。

大多数 LSM 流派的 k-v 都是利用类似的思路来保证事务的原子性,例如 rocksdb 是将事务中所有的写入都存放到了一个 WriteBatch 中,在事务提交的时候一次性写入。

3.2 隔离性

目前 rosedb 支持两种事务类型:读写事务和只读事务。只能同时开启一个读写事务,只读事务则可以同时开启多个。

在这种模式下,读会加读锁,写会加写锁,也就是说,读写会互斥,不能同时进行。可以理解为这是四种隔离级别中的串行化,它的优点是简单易实现,缺点是并发能力差。

需要说明的是,目前的这种实现在后面大概率会进行调整,我的设想是可以使用快照隔离的方式来支持读提交或者可重复读,这样数据读取能够读到历史版本,不会造成写操作的阻塞,只不过在实现上要复杂得多了。

3.3 持久性

持久性需要保证数据已经写到了非易失性存储介质当中,比如最常见的有磁盘或者 SSD,这样即使发生系统异常,也能够保证数据安全。

在 rosedb 当中,写入数据时,如果走默认的刷盘策略,是将数据写到了操作系统页缓存当中,实际上并没有落磁盘。如果操作系统还没来来得及将页缓存的数据刷到磁盘,那么会造成数据丢失。这样虽不能完全保证持久性,但性能是相对更好的,因为 Sync 刷磁盘是一次极其慢速的操作。

如果在启动 rosedb 的时候指定了配置项 Sync 为 true,那么每次写入都会强行 Sync,能够保证数据不丢,但是写性能会下降。

实际应该怎么选择,可以根据自己的使用场景来,如果系统稳定,对性能的要求较高,并且能够容忍丢失少量数据,那么可以采用默认策略,即 Sync 为 false,否则可以强制刷盘。

四、缺陷

经过上面的简单分析,可以看到 rosedb 已经基本实现了事务的 AID 特性,整体来说还是挺简单的,易于学习和使用,并且能够很好理解便于进一步的扩展。当然,目前也存在一些缺陷亟待解决。

第一个便是上面提到的隔离级别的问题,目前这种方式太过简单,使用一把全局大锁搞成了串行化,后续可以考虑只锁定需要操作的某个 key,减小锁的粒度。

还有一个问题便是,由于 rosedb 支持了多种数据结构,但是像 List、ZSet 这种结构,在事务中支持全部命令的难度较大,因此目前 List 只支持了 LPush 和 RPush,ZSet 只支持了 ZAdd、ZScore、ZRem 命令。

主要的原因是如果在事务中对已经存在的 key 进行读写,那么去支持像范围查找这种类型的命令就会很困难,目前我还没有想到比较好的解决方案。

最后,附上项目地址:https://github.com/roseduan/rosedb,欢迎各位前来围观吐槽。

本文分享自微信公众号 - roseduan写字的地方(rose_duan),作者:roseduan

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-08-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 转岗记

    最近我的工作方面发生了一些变化,先说结论:我通过内部转岗的方式,正式加入到 B 站基础架构部,会去做分布式存储相关的工作了。

    roseduan
  • 从零实现一个 k-v 存储引擎

    写这篇文章的目的,是为了帮助更多的人理解 rosedb,我会从零开始实现一个简单的包含 PUT、GET、DELETE 操作的 k-v 存储引擎。

    roseduan
  • 使用 Go 语言写一个数据库—6 完结撒花

    Hello 大家好,我是 roseduan,前面的几篇文章,我已经讲述了 rosedb 最基础也是最核心的知识,如果你没有印象的话,可以温习一下:

    roseduan
  • 我的 Java 转 Go 之路

    从毕业到现在已经接近两年了,在这段时间里,我日常开发都是使用的 Java,因为大学那时候自学的是 Java,然后毕业找到的工作也是做 Java 开发的。

    roseduan
  • 我写了一个数据库。。。

    大家好,我是 roseduan,今天我向大家推荐一下我写的一个 Go 语言实战项目—rosedb。

    roseduan
  • 使用 Go 语言写一个数据库—5 命令行

    Hello 大家好,我是 roseduan,上一次给大家分享了 rosedb 项目当中所涉及到的一些数据结构,有链表、哈希表、跳表、有序集合,内容比较的硬核,你...

    roseduan
  • rosedb 连续两天上榜

    昨天上午,我刚打开电脑,习惯性的逛了一下 Github ,点开了 Trending。因为 GIthub Trending 精选了一些最近比较活跃并且优质的开源项...

    roseduan
  • 开源月刊《HelloGitHub》第 62 期

    这里有实战项目、入门教程、黑科技、开源书籍、大厂开源项目等,涵盖多种编程语言 Python、Java、Go、C/C++、Swift...让你在短时间内感受到开源...

    HelloGitHub
  • 分布式事务原理与实践

    事务简介 事务的核心是锁和并发,采用同步控制的方式保证并发的情况下性能尽可能高,且容易理解。这种方式的优势是方便理解;它的劣势是性能比较低。 计算机可以简单的理...

    用户1263954
  • 使用 Go 语言写一个数据库—4 数据结构

    前面几篇文章,我已经对 rosedb 有了一定的讲解了,如果还没有看前面的内容,请先看一下之前的内容,这样你才能更好的理解本篇文章的内容。

    roseduan
  • 再论分布式事务:从理论到实践

    本文补充一种分布式事务解决方法:Best Effort. Best Effort   best effort即尽最大努力交付,主要用于在这样一种场景:不同的服...

    用户1263954
  • Saga分布式事务解决方案与实践

    我先介绍一下我自己,我叫姜宁,来自于华为开源研究中心,现在负责的是ServiceComb这个开源项目。ServiceComb这个项目已经进到Apache孵化,应...

    程序员小王
  • TiDB 最佳实践系列(三)乐观锁事务

    在前两篇的文章中,我们分别介绍了 TiDB 高并发写入常见热点问题及规避方法 和 PD 调度策略最佳实践,本文我们将深入浅出介绍 TiDB 乐观事务原理,并给出...

    PingCAP
  • Java+Oracle实现事务——JDBC事务

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/huyuyang6688/article/...

    DannyHoo
  • [转载]微服务实践(五):微服务的事件驱动数据管理

    【编者的话】本文是使用微服务创建应用系列的第五篇文章。第一篇文章介绍了微服务架构模式,并且讨论了使用微服务的优缺点;第二和第三篇描述了微服务架构模块间通讯的不同...

    干货满满张哈希
  • 微服务实践

    什么是微服务 微服务的两个核心: 微:服务粒度更细,即服务要细到API 服务:提供好服务,让服务好用 总结以上两点,来看这张图: ? 从图可以看出,微服务很简单...

    春哥大魔王
  • Nginx服务实践

    注意下载页面最好选择稳定版:http://nginx.org/en/download.html

    陈雷雷
  • 事件总线方案实践

    private final Runnable mPostValueRunnable = new Runnable() {

    杨充
  • 微服务中台技术解析之分布式事务方案和实践

    随着软件系统从单体应用迈向微服务架构以及数据库选型去中心化、异构化的趋势,传统的 ACID 事务在分布式系统上能否延续,如何落地,有哪些注意事项?本文将围绕分布...

    深度学习与Python

扫码关注云+社区

领取腾讯云代金券