For the questions raised in the previous article, this time I will answer:
It can be seen from the rehash process that in the rehash process, ht [0] and ht [1] have entries at the same time, that is, all entries in the dictionary are distributed in ht [0] and ht [1]
Trouble then came out. The main questions are as follows: (I don't answer how to solve it now)
1. How to find the key.
2. How to insert a new key.
3. How to delete a key.
4. How to ensure that the rehash process continuously inserts and deletes entries without rehash error.
5. How to traverse all entries of dict, how to ensure the traversal order.
6. How to ensure that the iterator is valid and correct.
How to find the key
dictEntry * dictFind (dict * d, const void * key)
{
dictEntry * he;
unsigned int h, idx, table;
if (d-> ht [0] .size == 0) return NULL; / * We do n‘t have a table at all * /
if (dictIsRehashing (d)) _dictRehashStep (d); // If rehash is in progress, perform a rehash operation
h = dictHashKey (d, key); // Calculate the hash of the key
// First look up on the ht [0] table
for (table = 0; table <= 1; table ++) {
idx = h & d-> ht [table] .sizemask;
he = d-> ht [table] .table [idx];
while (he) {
if (dictCompareKeys (d, key, he-> key))
return he;
he = he-> next;
}
// When not found on ht [0], if rehash is being performed now, the key may be on ht [1], you need to find on ht [1]
if (! dictIsRehashing (d)) return NULL;
}
return NULL;
}
Because there are entries on ht [0] and ht [1] when rehash, you need to find the elements in both tables to determine whether the elements exist. As for which table to look up first, the results will not be affected.
In the search process, if rehash is being performed, a rehash operation will be performed. This approach corresponds to the implementation of rehash, because rehash will not be completed at one time and needs to be divided into multiple completions. So how to divide into multiple times, and when should I perform a rehash operation? The dictRehash function already knows how to divide it into multiple times, and the execution is scattered into some operations, such as finding elements. Dispersing the rehash step in this way will not have a great impact on a query request and keep the query performance stable.
2. How to insert a new key
// Add entries to the dictionary
/ * Add an element to the target hash table * /
int dictAdd (dict * d, void * key, void * val)
{
dictEntry * entry = dictAddRaw (d, key); // Insert key
if (! entry) return DICT_ERR;
dictSetVal (d, entry, val); // Set the value corresponding to the key
return DICT_OK;
}
/ * Low level add. This function adds the entry but instead of setting
* a value returns the dictEntry structure to the user, that will make
* sure to fill the value field as he wishes.
*
* This function is also directly exposed to the user API to be called
* mainly in order to store non-pointers inside the hash value, example:
*
* entry = dictAddRaw (dict, mykey);
* if (entry! = NULL) dictSetSignedIntegerVal (entry, 1000);
*
* Return values:
*
* If key already exists NULL is returned.
* If key was added, the hash entry is returned to be manipulated by the caller.
* /
dictEntry * dictAddRaw (dict * d, void * key)
{
int index;
dictEntry * entry;
dictht * ht;
if (dictIsRehashing (d)) _dictRehashStep (d); // rehash
// If key already exists, return null
/ * Get the index of the new element, or -1 if
* the element already exists. * /
if ((index = _dictKeyIndex (d, key)) == -1)
return NULL;
// If rehash is in progress, insert the new element into ht [1], otherwise insert into ht [0]
/ * Allocate the memory and store the new entry * /
ht = dictIsRehashing (d)? & d-> ht [1]: & d-> ht [0];
entry = zmalloc (sizeof (* entry));
entry-> next = ht-> table [index];
ht-> table [index] = entry;
ht-> used ++;
/ * Set the hash entry fields. * /
dictSetKey (d, entry, key); // Insert
return entry;
}
When dict is not rehashed, inserting elements into ht [0] is easier. But if rehash is in progress, the element is inserted into ht [1]. Why do we have to insert elements into ht [1], not ht [0]? The reason lies in the process of rehash. The process of rehash is the process of moving entries from ht [0] to ht [1]. When all the entries have been moved, the process of rehash is completed. To ensure that the rehash process can be completed, there are a few things to note:
a. The element of ht [0] cannot keep increasing, even if the element is growing, it cannot be faster than moving the element to ht [1].
b. Determine the next entry to be moved (if the next entry is determined in some way, can iterate over all entries on ht [0])
c. Determine when all entries have been moved
The reason an element cannot be inserted into ht [0] is to ensure b. During the rehash process, the processed buckets are recorded by rehashidx. Because rehashidx grows linearly, it will eventually traverse all buckets on ht [0]. However, if rehash can traverse all the entries, you must also ensure that it has been processed Can no longer insert new elements. So new elements can only be inserted on ht [1]. In addition, because no new element is inserted into ht [0], a is guaranteed.
3. How to delete a key.
// First look in ht [0], if it can't find it, look in ht [1], delete if there is.
/ * Search and remove an element * /
static int dictGenericDelete (dict * d, const void * key, int nofree)
{
unsigned int h, idx;
dictEntry * he, * prevHe;
int table;
if (d-> ht [0] .size == 0) return DICT_ERR; / * d-> ht [0] .table is NULL * /
if (dictIsRehashing (d)) _dictRehashStep (d);
h = dictHashKey (d, key);
for (table = 0; table <= 1; table ++) {
idx = h & d-> ht [table] .sizemask;
he = d-> ht [table] .table [idx];
prevHe = NULL;
while (he) {
if (dictCompareKeys (d, key, he-> key)) {
/ * Unlink the element from the list * /
if (prevHe)
prevHe-> next = he-> next;
else
d-> ht [table] .table [idx] = he-> next;
if (! nofree) {
dictFreeKey (d, he);
dictFreeVal (d, he);
}
zfree (he);
d-> ht [table] .used--;
return DICT_OK;
}
prevHe = he;
he = he-> next;
}
if (! dictIsRehashing (d)) break;
}
return DICT_ERR; / * not found * /
}
4. How to ensure that the rehash process continuously inserts and deletes entries without rehash error.
It can be seen from the insertion and deletion process that rehash will not cause errors.
5. How to traverse all entries in dict, how to ensure the traversal order.
6. How to ensure that the iterator is valid and correct.
The dict traversal uses iterators. There are two kinds of iterators, one is a normal iterator, and the other is a safe iterator. In contrast, ordinary iterators are not safe.
Iterators are tools that many data structures (containers) have for traversing data elements. There are some issues to be aware of when using iterators:
a. Iterating order
b. Can the elements of the container be changed during the iterative element traversal process, such as the impact of changing the element of the container, such as the traversal order and the failure of the iterator?
Now look at the iterator of dict.
The traversal order is uncertain and basically can be considered as disorder.
Ordinary iterators do not allow personality dicts during traversal. Security iterators are allowed.
Look at the code below,
// Create a normal iterator
dictIterator * dictGetIterator (dict * d)
{
dictIterator * iter = zmalloc (sizeof (* iter));
iter-> d = d; // Record dict
iter-> table = 0;
iter-> index = -1;
iter-> safe = 0; // ordinary iterator
iter-> entry = NULL;
iter-> nextEntry = NULL;
return iter;
}
// Create a secure iterator
dictIterator * dictGetSafeIterator (dict * d) {
dictIterator * i = dictGetIterator (d);
i-> safe = 1; // safe iterator
return i;
}
// traversal process
dictEntry * dictNext (dictIterator * iter)
{
while (1) {
if (iter-> entry == NULL) {
// The current entry is null, it may be just created, it may be an empty bucket, it may be the last entry to reach the bucket, or it may be iterating through all buckets
dictht * ht = & iter-> d-> ht [iter-> table];
if (iter-> index == -1 && iter-> table == 0) {
// the iterator just created
if (iter-> safe)
iter-> d-> iterators ++; // If it is a safe iterator, write it down in the dict
else
iter-> fingerprint = dictFingerprint (iter-> d); // Ordinary Iterator
}
iter-> index ++; // Next bucket
if (iter-> index> = (long) ht-> size) {
// If the table has been traversed, if rehash is currently being performed, and ht [0] is traversed, then ht [1]
if (dictIsRehashing (iter-> d) && iter-> table == 0) {
iter-> table ++;
iter-> index = 0;
ht = & iter-> d-> ht [1];
} else {
break; // Completed
}
}
// Make a note of the current entry
iter-> entry = ht-> table [iter-> index];
} else {
// point to the next entry
iter-> entry = iter-> nextEntry;
}
if (iter-> entry) {
// Find the entry and note down the next entry for this entry
/ * We need to save the ‘next’ here, the iterator user
* may delete the entry we are returning. * /
iter-> nextEntry = iter-> entry-> next;
return iter-> entry; // return the found entry
}
}
// No entry found, dict has been traversed
return NULL;
}
From the traversal process above, you can see the three sequences of iterator traversal:
a. First traverse ht [0], if rehash is in progress, after traversing all buckets of ht [0], traverse ht [1]
b. In an ht, traversal is traversed by bucket from small to large
c. For multiple entries in the same bucket, the traversal order is from the head of the chain to the end of the chain, but the position of the entry in the chain itself is also uncertain.
It can be concluded from the above three sequences that the iterator traversal process is out of order.
Let's discuss whether the iterator can traverse all the entries. At this point, separate the ordinary iterator from the secure iterator to discuss.
Ordinary iterator. It is seen from the code that when the ordinary iterator starts to traverse, the fingerprint of the dict is calculated. During the traversal process, the dict can allow insertion, deletion of entries, and rehash. However, when the iterator is released, it will be compared whether the dict of the traversed dict is the same as the fingerprint of the dict before the traversal. If the dict is not the same, the program exits. At this point, you can know that ordinary iterators do not actually allow traversal. Although the code is not blocked during the traversal, it will eventually cause the program to quit. However, comparing the same fingerprint does not mean that the dict has not changed. It can only be said that if the fingerprint is different, the dict must have changed.
void dictReleaseIterator (dictIterator * iter)
{
if (! (iter-> index == -1 && iter-> table == 0)) {
if (iter-> safe)
iter-> d-> iterators--;
else
assert (iter-> fingerprint == dictFingerprint (iter-> d));
}
zfree (iter);
}
The safe iterator is recorded on the dict at the beginning of the traversal, and the traversal process is no different from the ordinary iterator. So what do you write down a safe iterator on a dict for? By looking up the code, you can see that the safe iterator counter using dict is the _dictRehashStep function.
/ * This function performs just a step of rehashing, and only if there are
* no safe iterators bound to our hash table. When we have iterators in the
* middle of a rehashing we ca n’t mess with the two hash tables otherwise
* some element can be missed or duplicated.
*
* This function is called by common lookup or update operations in the
* dictionary so that the hash table automatically migrates from H1 to H2
* while it is actively used. * /
static void _dictRehashStep (dict * d) {
if (d-> iterators == 0) dictRehash (d, 1); // If the safe iterator counter is 0, rehash operations are allowed
}
And from the function dictReleaseIterator that releases the iterator, you can see that the operation of fingerprint is not checked, so you can get the so-called safe iterator, which actually means:
a. Iterations can be inserted and deleted
b. Rehash will not be performed during the iteration. If rehash has been performed before the iteration is started, the rehash will be suspended after the iteration starts, and the rehash will continue after the iteration is completed.
Since insertion and deletion are allowed during the traversal process, how to traverse the process.
When inserting elements, it has no great impact on the traversal process, but it is uncertain whether it can traverse to the element that has just been inserted.
When deleting an element, there are four cases: deleting the element that has been traversed, deleting the current element, deleting the next element to be traversed, and deleting the non-next element that is not to be traversed.
Deleting the elements that have been traversed has no effect on the traversal process.
Deleting the current element has no effect on the traversal process, because the current element has been accessed, and the iterator no longer depends on the current element when it takes the next element.
Deleting the next element to be traversed can be divided into two cases. The next element has been recorded in the nextEntry of the iterator and not recorded in the iterator. If the next element is not recorded in the nextEntry of the iterator, it has no effect on the traversal process. If it is already recorded in nextEntry, the iterator is invalid at this time, and attempts to access the next element will have unpredictable effects.
Deleting non-next traversed elements will also affect the traversal process, but the deleted elements will not be traversed.
From the discussion above, it can be seen that safe iterators are not really safe. Deleting elements may cause iterators to fail.
Now discuss why the safe iterator does not allow rehash during the traversal process, because if rehash is allowed, the traversal process will not be guaranteed, some elements may be traversed multiple times, and some elements may not be traversed. Here are some scenarios:
a. The iterator now traverses to an element x of ht [0], at this time x is located in bucket # 2. Because rehash can be performed, just move element Y of bucket # 1 of ht [0] to ht [1] After this, the iterator will traverse to ht [1] after traversing ht [0], and will traverse Y again.
b. The iterator is now traversing to bucket 4 of ht [1], and the subsequent buckets have not been traversed. At this time, the rehash process is performed and all elements of ht [0] are just moved to ht [1] The rehash process is complete and ht [1] switches to ht [0]. Because the record in the iterator is currently traversing ht [1], the iterator has no elements when iterating through the elements in bucket 4 of ht [1] (the original ht [0]). The traversal process ends, and in fact There are some elements that have not been traversed.
It can be seen from the above discussion that rehash cannot be allowed during the traversal process.
Based on the above discussion, it can be seen that the use of safe iterators, as long as no element deletion operation is performed, the traversal process is basically no problem, and the elements that already exist at the beginning of the traversal will be traversed. It's just that the use of secure iterators has a certain effect on dicts. One is to suspend the rehash process, and the other is that the rehash process cannot proceed if it has been holding a secure iterator and does not release it.
This article is from the "chhquan" blog, please keep this source http://chhquan.blog.51cto.com/1346841/1827440
dict dictionary of redis underlying data structureTwo