Floyd的多key命令在并发场景下的解决方案

背景

在新存储引擎 floyd 下对于操作多个 key 的命令和以前的处理方式有所不同,例如 msetnxinstersinterstoresunionsunionstorezinterstorezuionstore等命令需要对多个 key 处理,在旧版 blackwidow 中由于 RocksDB 按照数据类型分类,所以同一类型的数据都在同一个 RocksDB 中,此时只需要对当前的 RocksDB 打一个快照即可解决并发问题,新版本中的 RocksDB 按照 key 的 Hash 计算分配给各个 key,所以同一个数据类型的 key 可能会分配在不同的 RocksDB 中,在并发情况下确保数据的正确性显得尤为重要.

讨论

我们以 sinter命令举例,来看看在新旧两个存储引擎下目前的实现方法,可以看到在 blackwidow下直接调用 RedisSets::SInter接口,不用担心并发的问题,因为在里面对这个 set 类型的整个 RocksDB 实例打了快照,而在 floyd下我们把一个 SInter 命令拆成了 SMembersSIsmember两个方法组成,并且执行这两个方法的 RocksDB 实例大概率不会是同一个,所以快照的生成会有先后顺序,如果在调用 Smembers接口的同时,有对 SIsmember的 key 的写操作,那么就不能保证数据的一致性。

blackwidow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rocksdb::Status RedisSets::SInter(const std::vector<std::string>& keys, std::vector<std::string>* members) {
if (keys.empty()) {
return rocksdb::Status::Corruption("SInter invalid parameter, no keys");
}

rocksdb::ReadOptions read_options;
const rocksdb::Snapshot* snapshot;

std::string meta_value;
int32_t version = 0;
ScopeSnapshot ss(db_, &snapshot);
read_options.snapshot = snapshot; // 生成快照
...
return rocksdb::Status::OK();
}

floyd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Status Storage::SInter(const std::vector<std::string>& keys, std::vector<std::string>* members) {
Status s = Status::OK();
// in codis mode, users should garentee keys will be hashed to same slot
auto inst = GetDBInstance(keys[0]);
if (!g_pika_conf->classic_mode()) {
s = inst->SInter(keys, members); // 生成快照
return s;
}

std::vector<std::string> key0_members = {};
s = inst->SMembers(keys[0], &key0_members);
if (!s.ok()) {
return s;
}

int32_t exist = 0;
for (const auto member : key0_members) {
exist = 0;
for (size_t idx = 1; idx < keys.size(); idx++) {
inst = GetDBInstance(keys[idx]);
s = inst->SIsmember(keys[idx], member, &exist); // 生成快照
if (!s.ok()) {
return s;
}
}
members->emplace_back(member);
}
return Status::OK();
}

方案

在收到 sinter这种读请求时,给 Instance容器上一把大锁 (即给所有的 RocksDB 实例上锁),然后全局打快照,但是这种方法可能会导致后面的命令阻塞,经过讨论我们决定拆开进行打快照,对于 sinster命令,我们分别在 SMembersSIsmember上分别打快照,在收到 sinterstore这种写请求时,我们依然用上面的方法,然后再对有写操作的 key 进行上锁。