背景
在新存储引擎 floyd 下对于操作多个 key 的命令和以前的处理方式有所不同,例如 msetnx
,inster
,sinterstore
,sunion
,sunionstore
,zinterstore
,zuionstore
等命令需要对多个 key 处理,在旧版 blackwidow 中由于 RocksDB 按照数据类型分类,所以同一类型的数据都在同一个 RocksDB 中,此时只需要对当前的 RocksDB 打一个快照即可解决并发问题,新版本中的 RocksDB 按照 key 的 Hash 计算分配给各个 key,所以同一个数据类型的 key 可能会分配在不同的 RocksDB 中,在并发情况下确保数据的正确性显得尤为重要.
讨论
我们以 sinter
命令举例,来看看在新旧两个存储引擎下目前的实现方法,可以看到在 blackwidow
下直接调用 RedisSets::SInter
接口,不用担心并发的问题,因为在里面对这个 set 类型的整个 RocksDB 实例打了快照,而在 floyd
下我们把一个 SInter
命令拆成了 SMembers
和 SIsmember
两个方法组成,并且执行这两个方法的 RocksDB 实例大概率不会是同一个,所以快照的生成会有先后顺序,如果在调用 Smembers
接口的同时,有对 SIsmember
的 key 的写操作,那么就不能保证数据的一致性。
blackwidow
1 | rocksdb::Status RedisSets::SInter(const std::vector<std::string>& keys, std::vector<std::string>* members) { |
floyd
1 | Status Storage::SInter(const std::vector<std::string>& keys, std::vector<std::string>* members) { |
方案
在收到 sinter
这种读请求时,给 Instance
容器上一把大锁 (即给所有的 RocksDB 实例上锁),然后全局打快照,但是这种方法可能会导致后面的命令阻塞,经过讨论我们决定拆开进行打快照,对于 sinster
命令,我们分别在 SMembers
和 SIsmember
上分别打快照,在收到 sinterstore
这种写请求时,我们依然用上面的方法,然后再对有写操作的 key
进行上锁。