本文主要介绍 RocksDB 锁结构设计、加锁解锁过程,并与 InnoDB 锁实现做一个简单对比。

本文由作者授权发布,未经许可,请勿转载。

作者:王刚,网易杭研数据库内核开发工程师

MyRocks 引擎目前是支持行锁的,包括共享锁和排它锁,主要是在 RocksDB 层面实现的,与 InnoDB 引擎的锁系统相比,简单很多。

本文主要介绍 RocksDB 锁结构设计、加锁解锁过程,并与 InnoDB 锁实现做一个简单对比。

事务锁的实现类是:TransactionLockMgr ,它的主要数据成员包括:

private:
PessimisticTransactionDB* txn_db_impl_;
// 默认16个lock map 分片
const size_t default_num_stripes_;
// 每个column family 最大行锁数
const int64_t max_num_locks_;
// lock map 互斥锁
InstrumentedMutex lock_map_mutex_; // Map of ColumnFamilyId to locked key info
using LockMaps = std::unordered_map<uint32_t, std::shared_ptr<LockMap>>;
LockMaps lock_maps_;
std::unique_ptr<ThreadLocalPtr> lock_maps_cache_;
// Must be held when modifying wait_txn_map_ and rev_wait_txn_map_.
std::mutex wait_txn_map_mutex_;
// Maps from waitee -> number of waiters.
HashMap<TransactionID, int> rev_wait_txn_map_;
// Maps from waiter -> waitee.
HashMap<TransactionID, TrackedTrxInfo> wait_txn_map_;
DeadlockInfoBuffer dlock_buffer_;
// Used to allocate mutexes/condvars to use when locking keys
std::shared_ptr<TransactionDBMutexFactory> mutex_factory_;

加锁的入口函数是:TransactionLockMgr::TryLock

Status TransactionLockMgr::TryLock(PessimisticTransaction* txn, //加锁的事务
uint32_t column_family_id, //所属的CF
const std::string& key, //加锁的健 Env* env,
bool exclusive //是否排它锁) {
// Lookup lock map for this column family id
std::shared_ptr<LockMap> lock_map_ptr = GetLockMap(column_family_id); //1. 根据 cf id 查找其 LockMap
LockMap* lock_map = lock_map_ptr.get();
if (lock_map == nullptr) {
char msg[];
snprintf(msg, sizeof(msg), "Column family id not found: %" PRIu32,
column_family_id); return Status::InvalidArgument(msg);
} // Need to lock the mutex for the stripe that this key hashes to
size_t stripe_num = lock_map->GetStripe(key);// 2. 根据key 的哈希获取 stripe_num,默认16个stripe
assert(lock_map->lock_map_stripes_.size() > stripe_num);
LockMapStripe* stripe = lock_map->lock_map_stripes_.at(stripe_num); LockInfo lock_info(txn->GetID(), txn->GetExpirationTime(), exclusive);
int64_t timeout = txn->GetLockTimeout(); return AcquireWithTimeout(txn, lock_map, stripe, column_family_id, key, env,
timeout, lock_info); // 实际加锁函数
}

GetLockMap(column_family_id) 函数根据 cf id 查找其 LockMap , 其逻辑包括两步:

  1. 在 thread 本地缓存 lock_maps_cache 中查找;
  2. 第1步没有查找到,则去全局的 lock_map_ 中查找。
std::shared_ptr<LockMap> TransactionLockMgr::GetLockMap(
uint32_t column_family_id) {
// First check thread-local cache
if (lock_maps_cache_->Get() == nullptr) {
lock_maps_cache_->Reset(new LockMaps());
}
auto lock_maps_cache = static_cast<LockMaps*>(lock_maps_cache_->Get());
auto lock_map_iter = lock_maps_cache->find(column_family_id);
if (lock_map_iter != lock_maps_cache->end()) {
// Found lock map for this column family.
return lock_map_iter->second;
}
// Not found in local cache, grab mutex and check shared LockMaps
InstrumentedMutexLock l(&lock_map_mutex_);
lock_map_iter = lock_maps_.find(column_family_id);
if (lock_map_iter == lock_maps_.end()) {
return std::shared_ptr<LockMap>(nullptr);
} else {
// Found lock map. Store in thread-local cache and return.
std::shared_ptr<LockMap>& lock_map = lock_map_iter->second;
lock_maps_cache->insert({column_family_id, lock_map});
return lock_map;
}
}

lock_maps_ 是全局锁结构:

 // Map of ColumnFamilyId to locked key info
using LockMaps = std::unordered_map<uint32_t, std::shared_ptr<LockMap>>;
LockMaps lock_maps_;

LockMap 是每个 CF 的锁结构:

struct LockMap {

  // Number of sepearate LockMapStripes to create, each with their own Mutex
const size_t num_stripes_; // Count of keys that are currently locked in this column family.
// (Only maintained if TransactionLockMgr::max_num_locks_ is positive.)
std::atomic<int64_t> lock_cnt{}; std::vector<LockMapStripe*> lock_map_stripes_; };

为了减少加锁时mutex 的争用,LockMap 内部又进行了分片,num_stripes_ = 16(默认值),
LockMapStripe 是每个分片的锁结构:

struct LockMapStripe {

  // Mutex must be held before modifying keys map
std::shared_ptr<TransactionDBMutex> stripe_mutex; // Condition Variable per stripe for waiting on a lock
std::shared_ptr<TransactionDBCondVar> stripe_cv; // Locked keys mapped to the info about the transactions that locked them.
// TODO(agiardullo): Explore performance of other data structures.
std::unordered_map<std::string, LockInfo> keys;
};

LockMapStripe 内部还是一个 unordered_map, 还包括 stripe_mutex、stripe_cv 。这样设计避免了一把大锁的尴尬,减小锁的粒度常用的方法,LockInfo 包含事务id:

struct LockInfo {
bool exclusive;
autovector<TransactionID> txn_ids;
// Transaction locks are not valid after this time in us
uint64_t expiration_time;
};

LockMaps 、LockMap、LockMapStripe 、LockInfo就是RocksDB 事务锁用到的数据结构了,可以看到并不复杂,代码实现起来简单,代价当然也有,后文在介绍再介绍。

AcquireWithTimeout 函数内部先获取 stripe mutex ,获取到了在进入AcquireLocked 函数:

if (timeout < ) {
// If timeout is negative, we wait indefinitely to acquire the lock
result = stripe->stripe_mutex->Lock();
} else {
result = stripe->stripe_mutex->TryLockFor(timeout);
}

获取了 stripe_mutex之后,准备获取锁:

// Acquire lock if we are able to
uint64_t expire_time_hint = ;
autovector<TransactionID> wait_ids;
result = AcquireLocked(lock_map, stripe, key, env, lock_info,
&expire_time_hint, &wait_ids);

AcquireLocked 函数实现获取锁逻辑,它的实现逻辑是:

  1. 在 stripe 的 map 中查找该key 是否已经被锁住。
  2. 如果key没有被锁住,判断是否超过了max_num_locks_,没超过则在 stripe的map 中插入{key, txn_lock_info},超过了max_num_locks_,加锁失败,返回状态信息。
  3. 如果key已经被锁住了,要判断加在key上的锁是排它锁还是共享锁,如果是共享锁,那事务的加锁请求可以满足;
  4. 如果是排它锁,如果是同一事务,加锁请求可以满足,如果不是同一事务,如果锁没有超时,则加锁请求失败,否则抢占过来。
  5. 以上是加锁过程,解锁过程类似,也是需要根据cf_id 和 key 计算出落到哪个stripe上,然后就是从map中把数据清理掉,同时还要唤醒该stripe上的等待线程,这个唤醒的粒度有点大。
void TransactionLockMgr::UnLock(PessimisticTransaction* txn,
uint32_t column_family_id,
const std::string& key, Env* env) {
std::shared_ptr<LockMap> lock_map_ptr = GetLockMap(column_family_id);//通过cf_id 获取Lock_Map
LockMap* lock_map = lock_map_ptr.get();
if (lock_map == nullptr) {
// Column Family must have been dropped.
return;
}
// Lock the mutex for the stripe that this key hashes to
size_t stripe_num = lock_map->GetStripe(key);//根据key 计算落到哪个stripe上
assert(lock_map->lock_map_stripes_.size() > stripe_num);
LockMapStripe* stripe = lock_map->lock_map_stripes_.at(stripe_num);
stripe->stripe_mutex->Lock();
UnLockKey(txn, key, stripe, lock_map, env); //从锁的map中清理掉该key
stripe->stripe_mutex->UnLock();
// Signal waiting threads to retry locking
stripe->stripe_cv->NotifyAll();
}

ocksdb lock整体结构如下:

如果一个事务需要锁住大量记录,rocksdb 锁的实现方式可能要比innodb 消耗更多的内存,innodb 的锁结构如下图所示:

由于锁信息是常驻内存,我们简单分析下RocksDB锁占用的内存。每个锁实际上是unordered_map中的一个元素,则锁占用的内存为key_length+8+8+1,假设key为bigint,占8个字节,则100w行记录,需要消耗大约22M内存。但是由于内存与key_length正相关,导致RocksDB的内存消耗不可控。我们可以简单算算RocksDB作为MySQL存储引擎时,key_length的范围。对于单列索引,最大值为2048个字节,具体可以参考max_supported_key_part_length实现;对于复合索引,索引最大长度为3072个字节,具体可以参考max_supported_key_length实现。

假设最坏的情况,key_length=3072,则100w行记录,需要消耗3G内存,如果是锁1亿行记录,则需要消耗300G内存,这种情况下内存会有撑爆的风险。因此RocksDB提供参数配置rocksdb_max_row_locks,确保内存可控,默认rocksdb_max_row_locks设置为1048576,对于大部分key为bigint场景,极端情况下,也需要消耗22G内存。而在这方面,InnoDB则比较友好,hash表的key是(space_id, page_no),所以无论key有多大,key部分的内存消耗都是恒定的。InnoDB在一个事务需要锁大量记录场景下是有优化的,多个记录可以公用一把锁,这样也间接可以减少内存。

总结

RocksDB 事务锁的实现整体来说不复杂,只支持行锁,还不支持gap lock ,锁占的资源也比较大,可通过rocksdb_max_row_locks 限制事务施加行锁的数量。

利益相关:

网易轻舟微服务,提供分布式事务框架GTXS,支持跨服务事务、跨数据源事务、混合事务、事务状态监控、异常事务处理等能力,应用案例 工商银行

【数据库内核】RocksDB:事务锁设计与实现的更多相关文章

  1. DTCC 2019 | 深度解码阿里数据库实现 数据库内核——基于HLC的分布式事务实现深度剖析

    摘要:分布式事务是分布式数据库最难攻克的技术之一,分布式事务为分布式数据库提供一致性数据访问的支持,保证全局读写原子性和隔离性,提供一体化分布式数据库的用户体验.本文主要分享分布式数据库中的时钟解决方 ...

  2. 数据库内核——基于HLC的分布式事务实现深度剖析

    DTCC 2019 | 深度解码阿里数据库实现 数据库内核--基于HLC的分布式事务实现深度剖析-阿里云开发者社区 https://developer.aliyun.com/article/70355 ...

  3. 【转】MSSQLServer数据库事务锁机制分析

    锁是网络数据库中的一个非常重要的概念,它主要用于多用户环境下保证数据库完整性和一致性.各种大型数据库所采用的锁的基本理论是一致的,但在具体实现上各有差别.目前,大多数数据库管理系统都或多或少具有自我调 ...

  4. c#数据库事务锁类型

    一.脏读.不可重复读.幻象读的区别   1.脏读:包含未提交数据的读取.例如,事务 a 更改了某行(数据库已发生更改,但尚未提交,有可能发生回滚),事务 b 在事务 a 提交更改之前读取已更改的行.如 ...

  5. MySQL数据库引擎、事务隔离级别、锁

    MySQL数据库引擎.事务隔离级别.锁 数据库引擎InnoDB和MyISAM有什么区别 大体区别为: MyISAM类型不支持事务处理等高级处理,而InnoDB类型支持.MyISAM类型的表强调的是性能 ...

  6. oracle 事务 锁机制

    原文地址:http://www.cnblogs.com/quanweiru/archive/2013/05/24/3097367.html 本课内容属于Oracle高级课程范畴,内容略微偏向理论性,但 ...

  7. innodb事务锁

    计算机程序锁   控制对共享资源进行并发访问 保护数据的完整性和一致性   lock  主要是事务,数据库逻辑内容,事务过程 latch/mutex 内存底层锁:   更新丢失 原因: B的更改还没有 ...

  8. MySQL事务锁等待超时 Lock wait timeout exceeded; try restarting transaction

    工作中处理定时任务分发消息时出现的问题,在查找并解决问题的时候,将相关的问题博客收集整理,在此记录下,以便之后再遇到相同的问题,方便查阅. 问题场景 问题出现的场景: 在消息队列处理消息时,同一事务内 ...

  9. 查看事务锁:innodb_trx+innodb_locks+innodb_lock_waits

    当出现:ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction,要解决是一件麻烦的事情:特别是当一个SQL ...

随机推荐

  1. 分享按钮(QQ,微信,微博等)移入动画效果

    ps:最近写的很多博客都是在以前在项目里写过的,之所以现在写出来,最大的目的就是希望自己以后用到的时候比较容易找,而且现在再写一遍,有助于加深印象! 很简单的效果,说先实现方式: 1.图标来自 阿里巴 ...

  2. Java实现 LeetCode 524 通过删除字母匹配到字典里最长单词(又是一道语文题)

    524. 通过删除字母匹配到字典里最长单词 给定一个字符串和一个字符串字典,找到字典里面最长的字符串,该字符串可以通过删除给定字符串的某些字符来得到.如果答案不止一个,返回长度最长且字典顺序最小的字符 ...

  3. Java实现 LeetCode 236 二叉树的最近公共祖先

    236. 二叉树的最近公共祖先 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先. 百度百科中最近公共祖先的定义为:"对于有根树 T 的两个结点 p.q,最近公共祖先表示为一个结点 x ...

  4. Java实现 LeetCode 147 对链表进行插入排序

    147. 对链表进行插入排序 对链表进行插入排序. 插入排序的动画演示如上.从第一个元素开始,该链表可以被认为已经部分排序(用黑色表示). 每次迭代时,从输入数据中移除一个元素(用红色表示),并原地将 ...

  5. Java实现 LeetCode 91 解码方法

    91. 解码方法 一条包含字母 A-Z 的消息通过以下方式进行了编码: 'A' -> 1 'B' -> 2 - 'Z' -> 26 给定一个只包含数字的非空字符串,请计算解码方法的总 ...

  6. PAT 跟奥巴马一起编程

    美国总统奥巴马不仅呼吁所有人都学习编程,甚至以身作则编写代码,成为美国历史上首位编写计算机代码的总统.2014 年底,为庆祝“计算机科学教育周”正式启动,奥巴马编写了很简单的计算机代码:在屏幕上画一个 ...

  7. Centos宝塔安装NextCloud

    官方版本列表链接:https://download.nextcloud.com/server/releases/ 我下载的是 16.0.6版本,下载链接:https://download.nextcl ...

  8. java创建透明背景的PNG图片加自定义文字水印

    人在码上走,需求天天有.这不,今天前端让我返回一个带自定义水印的背景图片.一通google,有现成的代码,但是基本是直接在源图上添加水印,生成出来的文字样式也没有控制好,看来又只有自己造轮子了. 过程 ...

  9. OAuth + Security - 6 - 自定义授权模式

    我们知道OAuth2的官方提供了四种令牌的获取,简化模式,授权码模式,密码模式,客户端模式.其中密码模式中仅仅支持我们通过用户名和密码的方式获取令牌,那么我们如何去实现一个我们自己的令牌获取的模式呢? ...

  10. mysql域名解析引起的远程访问过慢?

    MYSQL远程连接速度慢的解决方法 PHP远程连接MYSQL速度慢,有时远程连接到MYSQL用时4-20秒不等,本地连接MYSQL正常,出现这种问题的主要原因是, 默认安装的MYSQL开启了DNS的反 ...