参考代码:pika-2.4.0
pika作为类redis的存储系统,为了弥补在性能上的不足,在整个系统中大量使用多线程的结构,涉及到多线程编程,势必需要为线程加锁来保证数据访问的一致性和有效性。其中主要用到了三种锁:
简介
1.互斥锁
pika-2.4.0
./include/pika_binlog.h: slash::Mutex mutex_; //保证写入binlog的命令顺序性
./include/pika_hub_manager.h: slash::Mutex hub_stage_protector_;
./include/pika_hub_manager.h: slash::Mutex sending_window_protector_;
./include/pika_monitor_thread.h: slash::Mutex monitor_mutex_protector_;
./include/pika_slot.h: slash::Mutex slotsmgrt_cond_mutex_;
./include/pika_server.h: slash::Mutex slave_mutex_; // protect slaves_;
./include/pika_server.h: //slash::Mutex slotsmgrt_protector_;
./include/pika_server.h: slash::Mutex binlogbg_mutex_; //主从binlog同步
./include/pika_server.h: slash::Mutex slowlog_mutex_;
./include/pika_server.h: slash::Mutex ping_thread_protector_; //主从维护心跳
//admin命令中比如bgsave 、dbsync、keyscan之间会有互斥操作
在flushall命令中添加bgsave_protector_、bgsave_protector_是否多余)
./include/pika_server.h: slash::Mutex bgsave_protector_;
./include/pika_server.h: slash::Mutex db_sync_protector_;
./include/pika_server.h: slash::Mutex key_scan_protector_;
2.读写锁
pika2.4.0
./include/pika_binlog.h: pthread_rwlock_t rwlock_; //dealmsg中可写命令都要记录入binlog中,且都需按序写入
./include/pika_conf.h: pthread_rwlock_t rwlock_; //conf中各配置项使用的读写锁
./include/pika_slot.h: pthread_rwlock_t rwlock_db_;
./include/pika_slot.h: pthread_rwlock_t rwlock_batch_;
./include/pika_slot.h: pthread_rwlock_t rwlock_ones_;
./include/pika_server.h: pthread_rwlock_t rwlock_; //对db保护,flushall、flushdb、ReadonlyCmd获取写锁,其他dealmsg中命令(set、get等)获取读锁
./include/pika_server.h: pthread_rwlock_t state_protector_; //protect below, use for master-slave mode
pika3.2
./include/pika_binlog.h: pthread_rwlock_t rwlock_;
./include/pika_binlog_reader.h: pthread_rwlock_t rwlock_;
./include/pika_cmd_table_manager.h: pthread_rwlock_t map_protector_;
./include/pika_conf.h: pthread_rwlock_t rwlock_;
./include/pika_meta.h: pthread_rwlock_t rwlock_;
./include/pika_partition.h: pthread_rwlock_t db_rwlock_;
./include/pika_repl_server.h: pthread_rwlock_t client_conn_rwlock_;
./include/pika_rm.h: pthread_rwlock_t partitions_rw_;
./include/pika_server.h: pthread_rwlock_t tables_rw_;
./include/pika_server.h: pthread_rwlock_t state_protector_; //protect below, use for master-slave mode
./include/pika_server.h: pthread_rwlock_t slowlog_protector_;
./include/pika_table.h: pthread_rwlock_t partitions_rw_;
3.行锁
./include/pika_server.h: slash::RecordMutex mutex_record_;//同一时间只有一个线程对一个key进行写操作
锁在命令中应用
下面从命令分类的角度去看不同类型的命令。在pika执行过程中,需要加几道锁。
1.Suspend的admin命令:trySync、BgSave、FlushAll、Flushdb、readonly
写锁:RWLockWriter(rwlock_)-->互斥锁:slash::Mutex(bgsave_protector_、db_sync_protector_等)-->跟命令本身操作相关的其他锁
注意:并不是所有的admin命令都会写入binlog中,目前查看的只有flush相关命令会。
2.非Suspend的admin命令 :info、config等
读锁:RWLockReader(rwlock_)-->跟命令本身操作相关的其他锁
3.读命令:get、hget等
读锁:RWLockReader(rwlock_)
4.写命令:set、hset等
pika2.4 读锁:RWLockReader(rwlock_)--->pika行锁slash::RecordMutex(mutex_record_)-->binlog互斥锁g_pika_server->logger_->Lock()
pika3.2 读锁:RWLockReader(rwlock_)--->pika行锁slash::RecordMutex(mutex_record_)-->blackwidow记录锁:ScopeRecordLock(lock_mgr_)-->binlog互斥锁g_pika_server->logger_->Lock()
应用场景
1.挂起指令
在挂起指令的执行中,会添加写锁,以确保,此时没有其他任何指令的执行。其他指令(非suspend的admin命令以及读写命令)会添加读锁,可以并行访问。
其中pika-2.4.0挂起指令有:
trysync
bgsave
flushall
Flushdb
readonly
作用和意义:
保证当前服务器在执行挂起指令时,起到阻写作用。比如flush操作会涉及到创建新db的操作,此时其他命令都不应该执行。
2.行锁
行锁,用于对一个key加锁,保证同一时间只有一个线程对一个key进行操作。
在pika2.4.0中只应用于对key的写操作(写命令)。
作用和意义:
pika中存取的数据都是(key,value)数据,不同key所对应的数据完全独立,所以只需要对key加锁可以保证数据在并发访问时的一致性,行锁相对来说,锁定粒度小,也可以保证数据访问的高效性。
应用场景:
在pika系统中,主要在应用于两个地方,在系统上层指令过程中和在数据引擎层面。对于写指令(如SET,HSET)除了需要更新数据库状态,还涉及到pika的增量同步,需要在binlog中添加所执行的写指令,用于保证master和slave的数据库状态一致。故一条写指令的执行,主要有两个部分:
- 更改数据库状态
- 将指令添加到binlog中
pika3.2 其加锁情况,如下图(图片来源于网络):
在图中可以看到,pika对同一个key,加了两次行锁.在实际应用中,pika上所加的锁就已经能够保证数据访问的正确性。如果只是为了pika所需要的业务,层面使用行锁是多余的,但是blackwidow的设计初衷就是通过对rocksdb的改造和封装提供一套完整的类redis数据访问的解决方案,而不仅仅是为pika提供数据库引擎。这种设计思路也是秉承了Unix中的设计原则:Write programs that do one thing and do it well。
这样设计大大降低了pika与blackwidow之间的耦合,也使得blackwidow可以被单独拿出来测试和使用,在pika中的数据迁移工具就是完全使用blackwidow来完成,不必依赖任何pika相关的东西。
具体实现:
在pika系统中,一把行锁就可以维护所有key。在行锁的实现上是将一个key与一把互斥锁相绑定,并将其放入哈希表中维护,来保证每次访问key的线程只有一个,但是不可能也不需要为每一个key保留一把互斥锁,只需要当有多条线程访问同一个key时才需要锁,在所有线程都访问结束之后,就可以销毁这个绑定key的互斥锁,释放资源。具体实现如下:
class RecordLock {
public:
RecordLock(port::RecordMutex *mu, const std::string &key)
: mu_(mu), key_(key) {
mu_->Lock(key_);
}
~RecordLock() { mu_->Unlock(key_); }
private:
port::RecordMutex *const mu_;
std::string key_;
// No copying allowed
RecordLock(const RecordLock&);
void operator=(const RecordLock&);
};
void RecordMutex::Lock(const std::string &key) {
mutex_.Lock();
std::unordered_map<std::string, RefMutex *>::const_iterator it = records_.find(key);
if (it != records_.end()) {
//log_info ("tid=(%u) >Lock key=(%s) exist, map_size=%u", pthread_self(), key.c_str(), records_.size());
RefMutex *ref_mutex = it->second;
ref_mutex->Ref();
mutex_.Unlock();
ref_mutex->Lock();
//log_info ("tid=(%u) <Lock key=(%s) exist", pthread_self(), key.c_str());
} else {
//log_info ("tid=(%u) >Lock key=(%s) new, map_size=%u ++", pthread_self(), key.c_str(), records_.size());
RefMutex *ref_mutex = new RefMutex();
records_.insert(std::make_pair(key, ref_mutex));
ref_mutex->Ref();
mutex_.Unlock();
ref_mutex->Lock();
//log_info ("tid=(%u) <Lock key=(%s) new", pthread_self(), key.c_str());
}
}
完整代码可参考:slash_mutex.cc slash_mutex.h
参考 https://www.w3cschool.cn/pika/pika-4j1f2dk1.html
3. 互斥锁
应用场景:
上图中在执行add binlog过程中,pika用互斥锁(./include/pika_binlog.h: pthread_rwlock_t rwlock_):保证同一时间只有一个线程往binlog中写入数据,从而保证binlog写入的顺序性。
其次,pika内部有些命令内部的操作是互斥的,比如在做flushall涉及到换新DB,此时是不允许进行bgsave和scan操作的,否则会发生错误。为了保证顺利进行,进行flushall操作之前,就需要先加互斥锁bgsave_protector_、key_scan_protector_。