@ -1645,14 +1645,9 @@ std::vector<Status> DBImpl::MultiGet(
StopWatch sw ( env_ , stats_ , DB_MULTIGET ) ;
PERF_TIMER_GUARD ( get_snapshot_time ) ;
SequenceNumber snapshot ;
SequenceNumber consistent_seqnum ;
;
struct MultiGetColumnFamilyData {
ColumnFamilyData * cfd ;
SuperVersion * super_version ;
MultiGetColumnFamilyData ( ColumnFamilyData * cf , SuperVersion * sv )
: cfd ( cf ) , super_version ( sv ) { }
} ;
std : : unordered_map < uint32_t , MultiGetColumnFamilyData > multiget_cf_data (
column_family . size ( ) ) ;
for ( auto cf : column_family ) {
@ -1660,86 +1655,20 @@ std::vector<Status> DBImpl::MultiGet(
auto cfd = cfh - > cfd ( ) ;
if ( multiget_cf_data . find ( cfd - > GetID ( ) ) = = multiget_cf_data . end ( ) ) {
multiget_cf_data . emplace ( cfd - > GetID ( ) ,
MultiGetColumnFamilyData ( cfd , nullptr ) ) ;
MultiGetColumnFamilyData ( cfh , nullptr ) ) ;
}
}
bool last_try = false ;
{
// If we end up with the same issue of memtable geting sealed during 2
// consecutive retries, it means the write rate is very high. In that case
// its probably ok to take the mutex on the 3rd try so we can succeed for
// sure
static const int num_retries = 3 ;
for ( auto i = 0 ; i < num_retries ; + + i ) {
last_try = ( i = = num_retries - 1 ) ;
bool retry = false ;
if ( i > 0 ) {
for ( auto mgd_iter = multiget_cf_data . begin ( ) ;
mgd_iter ! = multiget_cf_data . end ( ) ; + + mgd_iter ) {
auto super_version = mgd_iter - > second . super_version ;
auto cfd = mgd_iter - > second . cfd ;
if ( super_version ! = nullptr ) {
ReturnAndCleanupSuperVersion ( cfd , super_version ) ;
}
mgd_iter - > second . super_version = nullptr ;
}
}
std : : function < MultiGetColumnFamilyData * (
std : : unordered_map < uint32_t , MultiGetColumnFamilyData > : : iterator & ) >
iter_deref_lambda =
[ ] ( std : : unordered_map < uint32_t , MultiGetColumnFamilyData > : : iterator &
cf_iter ) { return & cf_iter - > second ; } ;
if ( read_options . snapshot = = nullptr ) {
if ( last_try ) {
TEST_SYNC_POINT ( " DBImpl::MultiGet::LastTry " ) ;
// We're close to max number of retries. For the last retry,
// acquire the lock so we're sure to succeed
mutex_ . Lock ( ) ;
}
snapshot = last_seq_same_as_publish_seq_
? versions_ - > LastSequence ( )
: versions_ - > LastPublishedSequence ( ) ;
} else {
snapshot = reinterpret_cast < const SnapshotImpl * > ( read_options . snapshot )
- > number_ ;
}
for ( auto mgd_iter = multiget_cf_data . begin ( ) ;
mgd_iter ! = multiget_cf_data . end ( ) ; + + mgd_iter ) {
if ( ! last_try ) {
mgd_iter - > second . super_version =
GetAndRefSuperVersion ( mgd_iter - > second . cfd ) ;
} else {
mgd_iter - > second . super_version =
mgd_iter - > second . cfd - > GetSuperVersion ( ) - > Ref ( ) ;
}
TEST_SYNC_POINT ( " DBImpl::MultiGet::AfterRefSV " ) ;
if ( read_options . snapshot ! = nullptr | | last_try ) {
// If user passed a snapshot, then we don't care if a memtable is
// sealed or compaction happens because the snapshot would ensure
// that older key versions are kept around. If this is the last
// retry, then we have the lock so nothing bad can happen
continue ;
}
// We could get the earliest sequence number for the whole list of
// memtables, which will include immutable memtables as well, but that
// might be tricky to maintain in case we decide, in future, to do
// memtable compaction.
if ( ! last_try ) {
auto seq =
mgd_iter - > second . super_version - > mem - > GetEarliestSequenceNumber ( ) ;
if ( seq > snapshot ) {
retry = true ;
break ;
}
}
}
if ( ! retry ) {
if ( last_try ) {
mutex_ . Unlock ( ) ;
}
break ;
}
}
}
bool unref_only =
MultiCFSnapshot < std : : unordered_map < uint32_t , MultiGetColumnFamilyData > > (
read_options , nullptr , iter_deref_lambda , & multiget_cf_data ,
& consistent_seqnum ) ;
// Contain a list of merge operations if merge occurs.
MergeContext merge_context ;
@ -1763,7 +1692,7 @@ std::vector<Status> DBImpl::MultiGet(
Status & s = stat_list [ i ] ;
std : : string * value = & ( * values ) [ i ] ;
LookupKey lkey ( keys [ i ] , snapshot ) ;
LookupKey lkey ( keys [ i ] , consistent_seqnum ) ;
auto cfh = reinterpret_cast < ColumnFamilyHandleImpl * > ( column_family [ i ] ) ;
SequenceNumber max_covering_tombstone_seq = 0 ;
auto mgd_iter = multiget_cf_data . find ( cfh - > cfd ( ) - > GetID ( ) ) ;
@ -1807,7 +1736,7 @@ std::vector<Status> DBImpl::MultiGet(
for ( auto mgd_iter : multiget_cf_data ) {
auto mgd = mgd_iter . second ;
if ( ! last_tr y) {
if ( ! unref_onl y) {
ReturnAndCleanupSuperVersion ( mgd . cfd , mgd . super_version ) ;
} else {
mgd . cfd - > GetSuperVersion ( ) - > Unref ( ) ;
@ -1824,78 +1753,21 @@ std::vector<Status> DBImpl::MultiGet(
return stat_list ;
}
// Order keys by CF ID, followed by key contents
struct CompareKeyContext {
inline bool operator ( ) ( const KeyContext * lhs , const KeyContext * rhs ) {
const Comparator * comparator = cfd - > user_comparator ( ) ;
int cmp = comparator - > Compare ( * ( lhs - > key ) , * ( rhs - > key ) ) ;
if ( cmp < 0 ) {
return true ;
}
return false ;
}
const ColumnFamilyData * cfd ;
} ;
void DBImpl : : MultiGet ( const ReadOptions & read_options ,
ColumnFamilyHandle * column_family , const size_t num_keys ,
const Slice * keys , PinnableSlice * values ,
Status * statuses , const bool sorted_input ) {
autovector < KeyContext , MultiGetContext : : MAX_BATCH_SIZE > key_context ;
for ( size_t i = 0 ; i < num_keys ; + + i ) {
key_context . emplace_back ( keys [ i ] , & values [ i ] , & statuses [ i ] ) ;
}
MultiGetImpl ( read_options , column_family , key_context , sorted_input , nullptr ,
nullptr ) ;
}
void DBImpl : : MultiGetImpl (
const ReadOptions & read_options , ColumnFamilyHandle * column_family ,
autovector < KeyContext , MultiGetContext : : MAX_BATCH_SIZE > & key_context ,
bool sorted_input , ReadCallback * callback , bool * is_blob_index ) {
PERF_CPU_TIMER_GUARD ( get_cpu_nanos , env_ ) ;
StopWatch sw ( env_ , stats_ , DB_MULTIGET ) ;
size_t num_keys = key_context . size ( ) ;
template < class T >
bool DBImpl : : MultiCFSnapshot (
const ReadOptions & read_options , ReadCallback * callback ,
std : : function < MultiGetColumnFamilyData * ( typename T : : iterator & ) > &
iter_deref_func ,
T * cf_list , SequenceNumber * snapshot ) {
PERF_TIMER_GUARD ( get_snapshot_time ) ;
ColumnFamilyHandleImpl * cfh =
reinterpret_cast < ColumnFamilyHandleImpl * > ( column_family ) ;
ColumnFamilyData * cfd = cfh - > cfd ( ) ;
autovector < KeyContext * , MultiGetContext : : MAX_BATCH_SIZE > sorted_keys ;
sorted_keys . resize ( num_keys ) ;
{
size_t index = 0 ;
for ( KeyContext & key : key_context ) {
# ifndef NDEBUG
if ( index > 0 & & sorted_input ) {
KeyContext * lhs = & key_context [ index - 1 ] ;
KeyContext * rhs = & key_context [ index ] ;
const Comparator * comparator = cfd - > user_comparator ( ) ;
int cmp = comparator - > Compare ( * ( lhs - > key ) , * ( rhs - > key ) ) ;
assert ( cmp < = 0 ) ;
}
# endif
sorted_keys [ index ] = & key ;
index + + ;
}
if ( ! sorted_input ) {
CompareKeyContext sort_comparator ;
sort_comparator . cfd = cfd ;
std : : sort ( sorted_keys . begin ( ) , sorted_keys . begin ( ) + index ,
sort_comparator ) ;
}
}
// Keep track of bytes that we read for statistics-recording later
PERF_TIMER_STOP ( get_snapshot_time ) ;
// Acquire SuperVersion
SuperVersion * super_version = GetAndRefSuperVersion ( cfd ) ;
SequenceNumber snapshot ;
bool last_try = false ;
if ( cf_list - > size ( ) = = 1 ) {
// Fast path for a single column family. We can simply get the thread loca
// super version
auto cf_iter = cf_list - > begin ( ) ;
auto node = iter_deref_func ( cf_iter ) ;
node - > super_version = GetAndRefSuperVersion ( node - > cfd ) ;
if ( read_options . snapshot ! = nullptr ) {
// Note: In WritePrepared txns this is not necessary but not harmful
// either. Because prep_seq > snapshot => commit_seq > snapshot so if
@ -1905,10 +1777,10 @@ void DBImpl::MultiGetImpl(
// In WriteUnprepared, we cannot set snapshot in the lookup key because we
// may skip uncommitted data that should be visible to the transaction for
// reading own writes.
snapshot =
reinterpret _cast< const SnapshotImpl * > ( read_options . snapshot ) - > number_ ;
* snapshot =
static _cast < const SnapshotImpl * > ( read_options . snapshot ) - > number_ ;
if ( callback ) {
snapshot = std : : max ( snapshot , callback - > max_visible_seq ( ) ) ;
* snapshot = std : : max ( * snapshot , callback - > max_visible_seq ( ) ) ;
}
} else {
// Since we get and reference the super version before getting
@ -1921,15 +1793,263 @@ void DBImpl::MultiGetImpl(
// version because a flush happening in between may compact away data for
// the snapshot, but the snapshot is earlier than the data overwriting it,
// so users may see wrong results.
snapshot = last_seq_same_as_publish_seq_
* snapshot = last_seq_same_as_publish_seq_
? versions_ - > LastSequence ( )
: versions_ - > LastPublishedSequence ( ) ;
if ( callback ) {
}
} else {
// If we end up with the same issue of memtable geting sealed during 2
// consecutive retries, it means the write rate is very high. In that case
// its probably ok to take the mutex on the 3rd try so we can succeed for
// sure
static const int num_retries = 3 ;
for ( int i = 0 ; i < num_retries ; + + i ) {
last_try = ( i = = num_retries - 1 ) ;
bool retry = false ;
if ( i > 0 ) {
for ( auto cf_iter = cf_list - > begin ( ) ; cf_iter ! = cf_list - > end ( ) ;
+ + cf_iter ) {
auto node = iter_deref_func ( cf_iter ) ;
SuperVersion * super_version = node - > super_version ;
ColumnFamilyData * cfd = node - > cfd ;
if ( super_version ! = nullptr ) {
ReturnAndCleanupSuperVersion ( cfd , super_version ) ;
}
node - > super_version = nullptr ;
}
}
if ( read_options . snapshot = = nullptr ) {
if ( last_try ) {
TEST_SYNC_POINT ( " DBImpl::MultiGet::LastTry " ) ;
// We're close to max number of retries. For the last retry,
// acquire the lock so we're sure to succeed
mutex_ . Lock ( ) ;
}
* snapshot = last_seq_same_as_publish_seq_
? versions_ - > LastSequence ( )
: versions_ - > LastPublishedSequence ( ) ;
} else {
* snapshot = reinterpret_cast < const SnapshotImpl * > ( read_options . snapshot )
- > number_ ;
}
for ( auto cf_iter = cf_list - > begin ( ) ; cf_iter ! = cf_list - > end ( ) ;
+ + cf_iter ) {
auto node = iter_deref_func ( cf_iter ) ;
if ( ! last_try ) {
node - > super_version = GetAndRefSuperVersion ( node - > cfd ) ;
} else {
node - > super_version = node - > cfd - > GetSuperVersion ( ) - > Ref ( ) ;
}
TEST_SYNC_POINT ( " DBImpl::MultiGet::AfterRefSV " ) ;
if ( read_options . snapshot ! = nullptr | | last_try ) {
// If user passed a snapshot, then we don't care if a memtable is
// sealed or compaction happens because the snapshot would ensure
// that older key versions are kept around. If this is the last
// retry, then we have the lock so nothing bad can happen
continue ;
}
// We could get the earliest sequence number for the whole list of
// memtables, which will include immutable memtables as well, but that
// might be tricky to maintain in case we decide, in future, to do
// memtable compaction.
if ( ! last_try ) {
SequenceNumber seq =
node - > super_version - > mem - > GetEarliestSequenceNumber ( ) ;
if ( seq > * snapshot ) {
retry = true ;
break ;
}
}
}
if ( ! retry ) {
if ( last_try ) {
mutex_ . Unlock ( ) ;
}
break ;
}
}
}
// Keep track of bytes that we read for statistics-recording later
PERF_TIMER_STOP ( get_snapshot_time ) ;
return last_try ;
}
void DBImpl : : MultiGet ( const ReadOptions & read_options , const size_t num_keys ,
ColumnFamilyHandle * * column_families , const Slice * keys ,
PinnableSlice * values , Status * statuses ,
const bool sorted_input ) {
autovector < KeyContext , MultiGetContext : : MAX_BATCH_SIZE > key_context ;
autovector < KeyContext * , MultiGetContext : : MAX_BATCH_SIZE > sorted_keys ;
sorted_keys . resize ( num_keys ) ;
for ( size_t i = 0 ; i < num_keys ; + + i ) {
key_context . emplace_back ( column_families [ i ] , keys [ i ] , & values [ i ] ,
& statuses [ i ] ) ;
}
for ( size_t i = 0 ; i < num_keys ; + + i ) {
sorted_keys [ i ] = & key_context [ i ] ;
}
PrepareMultiGetKeys ( num_keys , sorted_input , & sorted_keys ) ;
autovector < MultiGetColumnFamilyData , MultiGetContext : : MAX_BATCH_SIZE >
multiget_cf_data ;
size_t cf_start = 0 ;
ColumnFamilyHandle * cf = sorted_keys [ 0 ] - > column_family ;
for ( size_t i = 0 ; i < num_keys ; + + i ) {
KeyContext * key_ctx = sorted_keys [ i ] ;
if ( key_ctx - > column_family ! = cf ) {
multiget_cf_data . emplace_back (
MultiGetColumnFamilyData ( cf , cf_start , i - cf_start , nullptr ) ) ;
cf_start = i ;
cf = key_ctx - > column_family ;
}
}
{
// multiget_cf_data.emplace_back(
// MultiGetColumnFamilyData(cf, cf_start, num_keys - cf_start, nullptr));
multiget_cf_data . emplace_back ( cf , cf_start , num_keys - cf_start , nullptr ) ;
}
std : : function < MultiGetColumnFamilyData * (
autovector < MultiGetColumnFamilyData ,
MultiGetContext : : MAX_BATCH_SIZE > : : iterator & ) >
iter_deref_lambda =
[ ] ( autovector < MultiGetColumnFamilyData ,
MultiGetContext : : MAX_BATCH_SIZE > : : iterator & cf_iter ) {
return & ( * cf_iter ) ;
} ;
SequenceNumber consistent_seqnum ;
bool unref_only = MultiCFSnapshot <
autovector < MultiGetColumnFamilyData , MultiGetContext : : MAX_BATCH_SIZE > > (
read_options , nullptr , iter_deref_lambda , & multiget_cf_data ,
& consistent_seqnum ) ;
for ( auto cf_iter = multiget_cf_data . begin ( ) ;
cf_iter ! = multiget_cf_data . end ( ) ; + + cf_iter ) {
MultiGetImpl ( read_options , cf_iter - > start , cf_iter - > num_keys , & sorted_keys ,
cf_iter - > super_version , consistent_seqnum , nullptr , nullptr ) ;
if ( ! unref_only ) {
ReturnAndCleanupSuperVersion ( cf_iter - > cfd , cf_iter - > super_version ) ;
} else {
cf_iter - > cfd - > GetSuperVersion ( ) - > Unref ( ) ;
}
}
}
namespace {
// Order keys by CF ID, followed by key contents
struct CompareKeyContext {
inline bool operator ( ) ( const KeyContext * lhs , const KeyContext * rhs ) {
ColumnFamilyHandleImpl * cfh =
static_cast < ColumnFamilyHandleImpl * > ( lhs - > column_family ) ;
uint32_t cfd_id1 = cfh - > cfd ( ) - > GetID ( ) ;
const Comparator * comparator = cfh - > cfd ( ) - > user_comparator ( ) ;
cfh = static_cast < ColumnFamilyHandleImpl * > ( lhs - > column_family ) ;
uint32_t cfd_id2 = cfh - > cfd ( ) - > GetID ( ) ;
if ( cfd_id1 < cfd_id2 ) {
return true ;
} else if ( cfd_id1 > cfd_id2 ) {
return false ;
}
// Both keys are from the same column family
int cmp = comparator - > Compare ( * ( lhs - > key ) , * ( rhs - > key ) ) ;
if ( cmp < 0 ) {
return true ;
}
return false ;
}
} ;
} // anonymous namespace
void DBImpl : : PrepareMultiGetKeys (
size_t num_keys , bool sorted_input ,
autovector < KeyContext * , MultiGetContext : : MAX_BATCH_SIZE > * sorted_keys ) {
# ifndef NDEBUG
if ( sorted_input ) {
for ( size_t index = 0 ; index < sorted_keys - > size ( ) ; + + index ) {
if ( index > 0 ) {
KeyContext * lhs = ( * sorted_keys ) [ index - 1 ] ;
KeyContext * rhs = ( * sorted_keys ) [ index ] ;
ColumnFamilyHandleImpl * cfh =
reinterpret_cast < ColumnFamilyHandleImpl * > ( lhs - > column_family ) ;
uint32_t cfd_id1 = cfh - > cfd ( ) - > GetID ( ) ;
const Comparator * comparator = cfh - > cfd ( ) - > user_comparator ( ) ;
cfh = reinterpret_cast < ColumnFamilyHandleImpl * > ( lhs - > column_family ) ;
uint32_t cfd_id2 = cfh - > cfd ( ) - > GetID ( ) ;
assert ( cfd_id1 < = cfd_id2 ) ;
if ( cfd_id1 < cfd_id2 ) {
continue ;
}
// Both keys are from the same column family
int cmp = comparator - > Compare ( * ( lhs - > key ) , * ( rhs - > key ) ) ;
assert ( cmp < = 0 ) ;
}
index + + ;
}
}
# endif
if ( ! sorted_input ) {
CompareKeyContext sort_comparator ;
std : : sort ( sorted_keys - > begin ( ) , sorted_keys - > begin ( ) + num_keys ,
sort_comparator ) ;
}
}
void DBImpl : : MultiGet ( const ReadOptions & read_options ,
ColumnFamilyHandle * column_family , const size_t num_keys ,
const Slice * keys , PinnableSlice * values ,
Status * statuses , const bool sorted_input ) {
autovector < KeyContext , MultiGetContext : : MAX_BATCH_SIZE > key_context ;
autovector < KeyContext * , MultiGetContext : : MAX_BATCH_SIZE > sorted_keys ;
sorted_keys . resize ( num_keys ) ;
for ( size_t i = 0 ; i < num_keys ; + + i ) {
key_context . emplace_back ( column_family , keys [ i ] , & values [ i ] , & statuses [ i ] ) ;
}
for ( size_t i = 0 ; i < num_keys ; + + i ) {
sorted_keys [ i ] = & key_context [ i ] ;
}
PrepareMultiGetKeys ( num_keys , sorted_input , & sorted_keys ) ;
MultiGetWithCallback ( read_options , column_family , nullptr , & sorted_keys ) ;
}
void DBImpl : : MultiGetWithCallback (
const ReadOptions & read_options , ColumnFamilyHandle * column_family ,
ReadCallback * callback ,
autovector < KeyContext * , MultiGetContext : : MAX_BATCH_SIZE > * sorted_keys ) {
std : : array < MultiGetColumnFamilyData , 1 > multiget_cf_data ;
multiget_cf_data [ 0 ] = MultiGetColumnFamilyData ( column_family , nullptr ) ;
std : : function < MultiGetColumnFamilyData * (
std : : array < MultiGetColumnFamilyData , 1 > : : iterator & ) >
iter_deref_lambda =
[ ] ( std : : array < MultiGetColumnFamilyData , 1 > : : iterator & cf_iter ) {
return & ( * cf_iter ) ;
} ;
size_t num_keys = sorted_keys - > size ( ) ;
SequenceNumber consistent_seqnum ;
bool unref_only = MultiCFSnapshot < std : : array < MultiGetColumnFamilyData , 1 > > (
read_options , callback , iter_deref_lambda , & multiget_cf_data ,
& consistent_seqnum ) ;
# ifndef NDEBUG
assert ( ! unref_only ) ;
# else
// Silence unused variable warning
( void ) unref_only ;
# endif // NDEBUG
if ( callback & & read_options . snapshot = = nullptr ) {
// The unprep_seqs are not published for write unprepared, so it could be
// that max_visible_seq is larger. Seek to the std::max of the two.
// However, we still want our callback to contain the actual snapshot so
// that it can do the correct visibility filtering.
callback - > Refresh ( snapshot ) ;
callback - > Refresh ( consistent_seqnum ) ;
// Internally, WriteUnpreparedTxnReadCallback::Refresh would set
// max_visible_seq = max(max_visible_seq, snapshot)
@ -1940,10 +2060,24 @@ void DBImpl::MultiGetImpl(
// be needed.
//
// assert(callback->max_visible_seq() >= snapshot);
snapshot = callback - > max_visible_seq ( ) ;
consistent_seqnum = callback - > max_visible_seq ( ) ;
}
MultiGetImpl ( read_options , 0 , num_keys , sorted_keys ,
multiget_cf_data [ 0 ] . super_version , consistent_seqnum , nullptr ,
nullptr ) ;
ReturnAndCleanupSuperVersion ( multiget_cf_data [ 0 ] . cfd ,
multiget_cf_data [ 0 ] . super_version ) ;
}
void DBImpl : : MultiGetImpl (
const ReadOptions & read_options , size_t start_key , size_t num_keys ,
autovector < KeyContext * , MultiGetContext : : MAX_BATCH_SIZE > * sorted_keys ,
SuperVersion * super_version , SequenceNumber snapshot ,
ReadCallback * callback , bool * is_blob_index ) {
PERF_CPU_TIMER_GUARD ( get_cpu_nanos , env_ ) ;
StopWatch sw ( env_ , stats_ , DB_MULTIGET ) ;
// For each of the given keys, apply the entire "get" process as follows:
// First look in the memtable, then in the immutable memtable (if any).
// s is both in/out. When in, s could either be OK or MergeInProgress.
@ -1953,8 +2087,8 @@ void DBImpl::MultiGetImpl(
size_t batch_size = ( keys_left > MultiGetContext : : MAX_BATCH_SIZE )
? MultiGetContext : : MAX_BATCH_SIZE
: keys_left ;
MultiGetContext ctx ( & sorted_keys [ num_keys - keys_left ] , batch_size ,
snapshot ) ;
MultiGetContext ctx ( sorted_keys , start_key + num_keys - keys_left ,
batch_size , snapshot ) ;
MultiGetRange range = ctx . GetMultiGetRange ( ) ;
bool lookup_current = false ;
@ -1992,15 +2126,14 @@ void DBImpl::MultiGetImpl(
PERF_TIMER_GUARD ( get_post_process_time ) ;
size_t num_found = 0 ;
uint64_t bytes_read = 0 ;
for ( KeyContext & key : key_context ) {
if ( key . s - > ok ( ) ) {
bytes_read + = key . value - > size ( ) ;
for ( size_t i = start_key ; i < start_key + num_keys ; + + i ) {
KeyContext * key = ( * sorted_keys ) [ i ] ;
if ( key - > s - > ok ( ) ) {
bytes_read + = key - > value - > size ( ) ;
num_found + + ;
}
}
ReturnAndCleanupSuperVersion ( cfd , super_version ) ;
RecordTick ( stats_ , NUMBER_MULTIGET_CALLS ) ;
RecordTick ( stats_ , NUMBER_MULTIGET_KEYS_READ , num_keys ) ;
RecordTick ( stats_ , NUMBER_MULTIGET_KEYS_FOUND , num_found ) ;