myrocks之事务处理



title: MySQL · myrocks · myrocks之事务处理

author: 张远

前言

mysql目前支持的事务引擎有innodb,tokudb. rocksdb加入mysql阵营后,mysql支持的事务引擎增长至3个。
myrocks目前支持的事务隔离级别有read-committed和repeatable-read. 同innodb一样,myrocks也支持MVCC机制。
可以说,myrocks提供了很好的事务支持,能够满足的一般业务的事务需求。

sequence number

谈到rocksdb事务,就必须提及rocksdb中的sequence number机制。rocksdb中的每一条记录都有一个sequence number, 这个sequence number存储在记录的key中。

InternalKey: | User key (string) | sequence number (7 bytes) | value type (1 byte) |

对于同样的User key记录,在rocksdb中可能存在多条,但他们的sequence number不同。
sequence number是实现事务处理的关键,同时也是MVCC的基础。

snapshot

snapshot是rocksdb的快照信息,snapshot实际就是对应一个sequence number.
简单的讲,假设snapshot的sequence number为Sa, 那么对于此snapshot来说,只能看到sequence number<=sa的记录,sequence number>sa的记录是不可见的。

  • snapshot 结构
    snapshot 主要包含sequence number和snapshot创建时间,sequence number 取自当前的sequence number.
class SnapshotImpl : public Snapshot {
  SequenceNumber number_;  // sequenct number
  int64_t unix_time_;      // snapshow创建时间
  ......
};
  • snapshot 管理
    snapshot由全局双向链表管理,根据sequence number排序。snapshot的创建和删除都需要维护双向链表。
  • snapshot与compact
    rocksdb的compact操作与snapshot有紧密联系。以我们熟悉的innodb为例,rocksdb的compact类似于innodb的purge操作, 而snapshot类似于InnoDB的read view. innodb做purge操作时会根据已有的read view来判断哪些undo log可以purge,而rocksdb的compact操作会根据已有snapshot信息即全局双向链表来判断哪些记录在compace时可以清理。

    判断的大体原则是,从全局双向链表取出最小的snapshot sequence number Sn. 如果已删除的老记录sequence number <=Sn, 那么这些老记录在compact时可以清理掉。

MVCC

有了snapshot,MVCC实现起来就很顺利了。记录的sequence number天然的提供了记录的多版本信息。
每次查询用户记录时,并不需要加锁。而是根据当前的sequence number Sn创建一个snapshot, 查询过程中只取小于或等于Sn的最大sequence number的记录。查询结束时释放snapshot.

关键代码段

DBIter::FindNextUserEntryInternal

 if (ikey.sequence <= sequence_) {
   if (skipping &&
      user_comparator_->Compare(ikey.user_key, saved_key_.GetKey()) <= 0) {
     num_skipped++;  // skip this entry
     PERF_COUNTER_ADD(internal_key_skipped_count, 1);
   } else {
     switch (ikey.type) {
       case kTypeDeletion:
       case kTypeSingleDeletion:
         // Arrange to skip all upcoming entries for this key since
         // they are hidden by this deletion.
         saved_key_.SetKey(
             ikey.user_key,
             !iter_->IsKeyPinned() || !pin_thru_lifetime_ / copy /);
         skipping = true;
         num_skipped = 0;
         PERF_COUNTER_ADD(internal_delete_skipped_count, 1);
         break;
       case kTypeValue:
         valid_ = true;
         saved_key_.SetKey(
             ikey.user_key,
             !iter_->IsKeyPinned() || !pin_thru_lifetime_ / copy /);
         return;
       case kTypeMerge:

       ......

隔离级别

隔离级别也是通过snapshot来实现的。在innodb中,隔离级别为read-committed时,事务中每的个stmt都会建立一个read view, 隔离级别为repeatable-read时,只在事务开启时建立一次read view. rocksdb同innodb类似,隔离级别为read-committed时,事务中每的个stmt都会建立一个snapshot, 隔离级别为repeatable-read时,只在事务开启时第一个stmt建立一次snapshot.

关键代码片段

rocksdb_commit:

  if (my_core::thd_tx_isolation(thd) <= ISO_READ_COMMITTED)
  {
    // For READ_COMMITTED, we release any existing snapshot so that we will
    // see any changes that occurred since the last statement.
    tx->release_snapshot();
  }
  • 隔离级别实现差异
    在read committed隔离级别下,如果一个大事务要更新1000w行,当它更新了前900w行时,
    同时另一个事务已经更新了后100w行,那么myrocks会重新获取快照,再次尝试更新,这样
    更新的是新提交的数据,也符合read committed逻辑。具体的讨论可以参考最近的issue#340.
    而之前的处理方式是直接报死锁错误。
rocksdb::Status ha_rocksdb::get_for_update(
    Rdb_transaction*             tx,
    rocksdb::ColumnFamilyHandle* column_family,
    const rocksdb::Slice&        key,
    std::string*                 value) const
{
  rocksdb::Status s= tx->get_for_update(column_family, key, value);

  // If we have a lock conflict and we are running in READ COMMITTTED mode
  // release and reacquire the snapshot and then retry the get_for_update().
  if (s.IsBusy() && my_core::thd_tx_isolation(ha_thd()) == ISO_READ_COMMITTED)
  {
    tx->release_snapshot();
    tx->acquire_snapshot(false);

    s= tx->get_for_update(column_family, key, value);
  }

  return s;
}

innodb不会出现上述情况,当第一个大事更新是会持有b树的index lock, 第二个事务会一直等待index lock直至第一个事务提交完成。

myrocks目前只支持一种锁类型:排他锁(X锁),并且所有的锁信息都保存在内存中。

  • 锁结构
    每个锁实际上存储的哪条记录被哪个事务锁住。
struct LockInfo {
  TransactionID txn_id;

  // Transaction locks are not valid after this time in us
  uint64_t expiration_time;
  ......
  }

每个锁实际是key和LockInfo的映射. 锁信息都保存在map中

struct LockMapStripe {
  std::unordered_map<std::string, LockInfo> keys;
  ......
}

为了减少全局锁信息访问的冲突, rocksdb将锁信息进行按key hash分区,

struct LockMap {
    std::vector<LockMapStripe*> lock_map_stripes_;
}

同时每个column family 存储一个这样的LockMap.

using LockMaps = std::unordered_map<uint32_t, std::shared_ptr<LockMap>>;
LockMaps lock_maps_;

锁相关参数:
max_num_locks:事务锁个数限制
expiration:事务过期时间

通过设置以上两个参数,来控制事务锁占用过多的内存。

  • 死锁检测

rocksdb内部实现了简单的死锁检测机制,每次加锁发生等待时都会向下面的map中插入一条等待信息,表示一个事务id等待另一个事务id.
同时会检查wait_txn_map_是否存在等待环路,存在环路则发生死锁。

std::unordered_map<TransactionID, TransactionID> wait_txn_map_;

死锁检测关键代码片段

TransactionLockMgr::IncrementWaiters:

    for (int i = 0; i < txn->GetDeadlockDetectDepth(); i++) {
      if (next == id) {
        DecrementWaitersImpl(txn, wait_id);
        return true;
      } else if (wait_txn_map_.count(next) == 0) {
        return false;
      } else {
        next = wait_txn_map_[next];
      }
    }

死锁检测相关参数
deadlock_detect:是否开启死锁检测
deadlock_detect_depth:死锁检查深度,默认50

  • gap lock

    innodb中是存在gap lock的,主要是为了实现repeatable read和唯一性检查的。
    而在rocksdb中,不支持gap lock(rocksdb insert是也会多对唯一键加锁,以防止重复插入,
    严格的来讲也算是gap lock).

    那么在rocksdb一些需要gap lock的地方,目前是报错和打印日志来处理的。

    相关参数
    gap_lock_write_log: 只打印日志,不返回错误
    gap_lock_raise_error: 打印日志并且返回错误

  • 锁示例

    直接看例子

binlog XA & 2pc

myrocks最近也支持了binlog xa.
在开启binlog的情况下,myrocks提交时,会经历两阶段提交阶段。
prepare阶段,根据server层生成的xid(由MySQLXid+server_id+qurey_id组成),在rockdb内部执行2pc操作,生成Prepare(xid),EndPrepare()记录。
commit阶段,根据事务成还是失败,生成Commit(xid)或Rollback(xid)记录。

rocksdb 2pc参考这里

总结

myrocks在事务处理方面还有些不完善的地方,比如锁类型只有单一的X锁,不支持gap lock,纯内存锁占用内存等。 myrocks社区正在持续改进中,一起期待。

时间: 2024-10-04 00:52:50

myrocks之事务处理的相关文章

MySQL · myrocks · myrocks之事务处理

前言 mysql目前支持的事务引擎有innodb,tokudb. rocksdb加入mysql阵营后,mysql支持的事务引擎增长至3个. myrocks目前支持的事务隔离级别有read-committed和repeatable-read. 同innodb一样,myrocks也支持MVCC机制. 可以说,myrocks提供了很好的事务支持,能够满足的一般业务的事务需求. sequence number 谈到rocksdb事务,就必须提及rocksdb中的sequence number机制.roc

阿里数据库内核月报:2016年11月

# 01 PgSQL · 特性分析 · 金融级同步多副本分级配置方法 # 02 MySQL · myrocks · myrocks之事务处理 # 03 MySQL · TokuDB · rbtree block allocator # 04 MySQL · 引擎特性 · Column Compression浅析 # 05 MySQL · 引擎介绍 · Sphinx源码剖析(一) # 06 PgSQL · 特性分析 · PostgreSQL 9.6 如何把你的机器掏空 # 07 PgSQL · 特

MyRocks写入分析

title: MySQL · myrocks · myrocks写入分析 author: 张远 写入流程 myrocks的写入流程可以简单的分为以下几步来完成 将解析后的记录(kTypeValue/kTypeDeletion)写入到WriteBatch中 将WAL日志写入log文件 将WriteBatch中的内容写到memtable中,事务完成 其中第2,3步在提交时完成 WriteBatch与Myrocks事务处理密切相关,事务中的记录提交前都以字符串的形式存储在WriteBatch->rep

MySQL · myrocks · myrocks写入分析

写入流程 myrocks的写入流程可以简单的分为以下几步来完成 将解析后的记录(kTypeValue/kTypeDeletion)写入到WriteBatch中 将WAL日志写入log文件 将WriteBatch中的内容写到memtable中,事务完成 其中第2,3步在提交时完成 WriteBatch与Myrocks事务处理密切相关,事务中的记录提交前都以字符串的形式存储在WriteBatch->rep_中,要么都提交,要么都回滚. 回滚的逻辑比较简单,只需要清理WriteBatch->rep_

myrocks记录格式分析

title: MySQL · myrocks · myrocks记录格式分析 author: 张远 概况 rocksdb作为KV存储引擎,那么myrocks记录最终会以kv的形式存储在rocksdb中.MySQL中的表一般由若干索引组成, 在innodb存储引擎中,每个索引对应一颗B树,而在rocksdb存储引擎中,索引对应于rocksdb中一段连续范围的数据. 具体来说,这个范围是此索引id和id+1之间的所有数据.如果表的所有索引都在一个column family, 那表的这些索引数据在物理

SQL和Oracle对数据库事务处理的差异性

在吉日嘎拉的软件编程走火入魔之:数据库事务处理入门(适合初学者阅读)文章中关于MS SQL Server和Oracle对数据库事务处理的差异性引起一些争论,因此记录我对数据库事务处理的想法. 简介 本文讲述MS SQL Server和Oracle对数据库事务处理的差异性,以及Oracle如何对事务处理的实现. 什么是事务 数据库事务(Database Transaction)是一组数据库操作的处理单元.事务符合ACID的特性: Atomic:原子性,要么全部要么一无所有.All or None.

Sqlserver2000中的事务处理

server|sqlserver|事务处理 Sqlserver2000中的事务处理 一.       定义及其性质:事务:事务是作为单个逻辑工作单元执行的一系列操作.属性:一个逻辑工作单元必须有四个属性,称为 ACID(原子性.一致性.隔离性和持久性)属性,只有这样才能成为一个事务:1.      原子性:事务必须是原子工作单元:对于其数据修改,要么全都执行,要么全都不执行.2.      一致性:事务在完成时,必须使所有的数据都保持一致状态.在相关数据库中,所有规则都必须应用于事务的修改,以保

asp事务处理的另外一个方法

事务处理|事务处理   <%'asp事务处理.'测试数据库为sql server,服务器为本机,数据库名为test,表名为a,两个字段id(int)主键标识,num(int)set conn=server.CreateObject("adodb.connection") strConn="provider=sqloledb.1;persist security info=false;uid=sa;pwd=sa;Initial Catalog=test;Data Sour

利用ASP实现事务处理的方法

事务处理|事务处理 在开发Web应用时,无一例外地需要访问数据库,以完成对数据的查询.插入.更新.删除等操作.受应用逻辑的影响,有时需要将多条数据库操作指令组成一个工作单元(事务).在数据库中,所谓事务是指一组逻辑操作单元,它使数据从一种状态变换到另一种状态.为确保数据库中数据的一致性,应当用离散的成组的逻辑单元操作数据:当它全部完成时,数据的一致性可以保持:而当单元中的一部分操作失败时,整个事务会被全部忽略,所有从起始点以后的操作全部退回到开始状态. 实际上,在默认方式下对数据库的每一次操作都