本文主要介绍 RocksDB 锁结构设计、加锁解锁过程,并与 InnoDB 锁实现作一个简单对比。数据库
本文由做者受权发布,未经许可,请勿转载。缓存
做者:王刚,网易杭研数据库内核开发工程师数据结构
MyRocks 引擎目前是支持行锁的,包括共享锁和排它锁,主要是在 RocksDB 层面实现的,与 InnoDB 引擎的锁系统相比,简单不少。app
本文主要介绍 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[255]; 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 , 其逻辑包括两步:微服务
- 在 thread 本地缓存 lock_maps_cache 中查找;
- 第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 的锁结构:ui
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{0}; 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 < 0) { // 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 = 0; autovector<TransactionID> wait_ids; result = AcquireLocked(lock_map, stripe, key, env, lock_info, &expire_time_hint, &wait_ids);
AcquireLocked 函数实现获取锁逻辑,它的实现逻辑是:
- 在 stripe 的 map 中查找该key 是否已经被锁住。
- 若是key没有被锁住,判断是否超过了max_num_locks_,没超过则在 stripe的map 中插入{key, txn_lock_info},超过了max_num_locks_,加锁失败,返回状态信息。
- 若是key已经被锁住了,要判断加在key上的锁是排它锁仍是共享锁,若是是共享锁,那事务的加锁请求能够知足;
- 若是是排它锁,若是是同一事务,加锁请求能够知足,若是不是同一事务,若是锁没有超时,则加锁请求失败,不然抢占过来。
以上是加锁过程,解锁过程相似,也是须要根据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(); }
rocksdb 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,支持跨服务事务、跨数据源事务、混合事务、事务状态监控、异常事务处理等能力,应用案例:工商银行。