Redis real combat 08. Realize automatic completion, distributed lock and counting semaphore

Time:2021-3-8

Automatic completionP109

Automatic completion can be seen everywhere in daily business, which should be regarded as the most common and common function. The actual business scenario must include the case of containing substrings. In fact, to a certain extent, this is converted into the search function, that is, the string containing a certain substring, and the prefix matching string is given priority. If you include only prefixes, you can useTrieTree, but in the case of containing others, use the database/ESIts own query is enough. The results can be queried respectively according to four conditions (exact matching, prefix, suffix, and inclusion (the latter two can also be merged into inclusion)), until the upper limit of the number of data is reached or all queries are completed. However, this method has some disadvantages: the number of queries is large, and it is difficult to paginate. However, in the actual scenario, the data on the first page is all that is needed to complete.

Automatically complete recent contactsP110

Demand:Record the names of 100 people who have been contacted recently, and support the automatic completion of the input string.P110

The amount of data is very small, so it can be used in theRedisMaintain the most recent contacts in the list, and then filter in the memory to automatically complete the string.

Steps: P111

  1. Maintain a recent contact list with a length of 100

    1. If the specified contact is already in the list, it is removed from the list(LREM)
    2. Adds the specified contact to the top of the list(LPUSH)
    3. If the length of the list exceeds 100 after adding, the list will be pruned and only the 100 contacts in front of the list will be retained(LTRIM)
  2. Get the whole recent contact list and filter it in memory according to four conditions
Automatic completion of address bookP112

Demand:There are a lot of address books. There are thousands of people in each address book (including only lowercase letters). Try to reduce the numberRedisThe amount of data transmitted to the client can realize automatic prefix completion.P112

Thinking:The ordered set is used to store people’s names, and the characteristic of ordered set is used: when the members’ scores are the same, they will be sorted according to the binary order of member strings. If you want to findabcPrefix string, then it is actually looking for a string betweenabbz...After andabdThe previous string. So the question turns to: how do you find the first oneabcThe ranking of the previous elements and the first one are in the first placeabdRanking of previous elements. We can construct two strings that are not in an ordered set(abb{, abc{)Auxiliary positioning, because{It’s in linezIn this way, we can ensure that the two strings do not exist in the ordered set and meet the limitations of the problem after transformation.P113

In conclusion:By replacing the last character of a given prefix with the first character in front of the character, and then splicing the left curly bracket at the end, we can get the precursor of the prefix. By splicing the left curly bracket at the end of the prefix, we can get the successor of the prefix.

  • Character set: when processing characters that are not limited toa~zThen we should deal with the following three problemsP113

    • Convert all characters to bytes: UsingUTF-8UTF-16perhapsUTF-32Character encoding(be careful: UTF-16andUTF-32Only big end version can be used for the above method)
    • Find out the character range you want to support and make sure that at least one character is left before and after the selected range
    • Use the characters before and after the range instead of the back quotes ` And left curly bracket{

Steps: P114

  1. Use the method in the idea to find the precursor and successor of the prefix (in order to prevent the error of querying the same prefix at the same time, you can add the prefix after the precursor and successor)UUID
  2. Insert precursors and successors into an ordered set
  3. See the ranking of the pioneers and successors
  4. Take out the elements between them
  5. Remove precursors and successors from ordered sets

This technique can also be applied to any sorted index, and can be applied to several different types of range queries by improving (introduced in Chapter 7), without adding elements to create a range.P115

Distributed lockP115

Distributed lock is also very common in the business, which can avoid operating on the same data at the same time in the distributed environment, so as to avoid the concurrency problem.

Causes the lock to appear the incorrect behavior, as well as the lock when incorrect movement symptomP119
  • The lock held by the process is automatically released because of the long operation time, but the process itself does not know this, and may even release the lock held by other processes by mistake
  • A process that holds a lock and intends to perform a long-time operation has crashed, but other processes that want to acquire a lock do not know which process holds the lock, and cannot detect the process that holds the lock, but the process has crashed. They can only waste time waiting for the lock to be released automatically
  • After a process holds the lock but the lock has expired, many other processes attempt to acquire the lock at the same time, and all of them acquire the lock
  • The first case and the third case occur at the same time, resulting in multiple processes acquiring locks, and each process thinks that it is the only one to acquire the lock, but the process does not
Simple example
//Get the key lock on Conn, the lock timeout is expirytime MS, and the longest waiting time is timeout Ms
func acquireLock(conn redis.Conn, key string, expiryTime int, timeout int) (token *int) {
    //In order to simplify, nanosecond time stamp should be used as token, and UUID should be used in practice
    value := int(time.Now().UnixNano())

    for ; timeout >= 0; {
        //Try to lock
        _, err := redis.String(conn.Do("SET", key, value, "PX", expiryTime, "NX"))
        //If the lock is acquired successfully, the token pointer will be returned directly
        if err == nil {
            return &value
        }
        //Sleep 1ms
        time.Sleep(time.Millisecond)
        timeout --
    }

    //If the lock is not acquired successfully in timeout, the acquisition fails and nil is returned
    return nil
}

//Release the key's lock on Conn, and the lock corresponds to the token
func releaseLock(conn redis.Conn, key string, token int) error {
    //Use Lua script to ensure atomicity. Only when the token and value are equal, can it be released
    releaseLua := "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
    script := redis.NewScript(1, releaseLua)
    result, err := redis.Int(script.Do(conn, key, token))
    if err != nil {
        return err
    }
    if result == 0 {
        return errors.New("release failure")
    }
    return nil
}

Counting semaphoreP126

Counting semaphore is a kind of lock, which allows users to limit how many processes can access a resource at the same time. It is usually used to limit the number of resources that can be used at the same time.P126

Basic counting semaphoreP126

The information of the holders of multiple semaphores is stored in the same ordered set, that is, a semaphore is generated for each requestUUID, and put thisUUIDAs a member of an ordered set, the corresponding score of the member is the time stamp of the attempt.P127

Steps of acquiring semaphore: P127

  1. Clean up all expired items in the ordered collectionUUID(time stamp < = current time stamp – expiration time)
  2. generateUUIDUsing the time stamp as the score, theUUIDAdd to ordered collection
  3. Check just nowUUIDRanking of

    • If the ranking is lower than the total number of available semaphores (member ranking is calculated from 0), then the semaphores are obtained successfully
    • If the ranking is equal to or higher than the total number of available semaphores, then if the ranking is not successful, it is necessary toUUIDremove

When semaphores are released, they are directly removed from the ordered setUUIDThat’s it. If the return value is 1, it indicates successful manual release; if the return value is 0, it indicates that it has been released automatically due to expiration.P128

Disadvantages:

  • The expiration time of all semaphores needs to be the same: to facilitate the deletion of expired semaphoresUUID
  • Unfair, system dependent time:

    • In a multi machine environment,ASystem time ratio ofBWhen the system time is 10 ms faster, then whenAWhen we get the last semaphore,BAs long as you try to acquire the semaphore within 10ms, it will result inBThe non-existent semaphore is obtained, which leads to the semaphore exceeding the total number of semaphores.P128
    • It may also cause the semaphore to be released earlier by the acquisition request of other systems
Fair count semaphoreP128

In order to achieve a fair count of semaphores, that is, the client that first sent the request can get the semaphore. We need to be hereRedisA self incrementing counter is maintained in, which is automatically incremented before each request is sent out, and the value after self incrementing is used as the scoreUUIDInsert into another ordered collection. That is, the original ordered set is only used to find and delete expired dataUUIDThe new ordered set is used to obtain the ranking and judge whether the request successfully obtains the semaphore. At the same time, in order to keep the new ordered set, delete the expired data in timeUUIDAfter the original ordered collection is deleted, theZINTERSTORECommand, which only appears in the original ordered collectionUUID (ZINTERSTORE count_set 2 count_set time_set WEIGHTS 1 0)。be careful:If the semaphore acquisition fails, it is necessary to delete the useless data in time.

The above method can solve the problem that the number of semaphores obtained exceeds the total number of semaphores to a certain extent, but the deletion is out of dateUUIDSo try to ensure that the system time gap of each host is small enough.P131

Self thinking: be independent of system time

The original ordered set is removed, and only the counter and count value are left as the ordered set of scoreUUIDSet a key with expiration time. Before each removal, traverse the ordered collection, query whether it is expired, and delete all expired items from the ordered collectionUUID

In this way, not only can it be completely independent of the system time, but also there is no problem that the number of semaphores acquired exceeds the total number of semaphores. Moreover, it can realize that a single acquired semaphore can have different expiration time, and reduce the time complexity to a certain extent, but it will increase the communication cost between client and clientRedisThe number of interactions between servers.

Refresh semaphoreP131

Semaphore users may not be able to process the request within the expiration time, so they need to renew the contract to extend the expiration time. Since the fair count semaphore has separated the time ordered set from the count ordered set, it is only necessary to count in the time ordered setUUIDimplementZADDThat is, if the execution fails, it will be released automatically after expiration.P131

For the method I just proposed, there are two ways to renew the contract:

  • useluaScript guarantees atomicity
  • Read expiration time first

    • Unexpired: reuse tapeXXOf optionsSETCommand to set a new expiration time (the original expiration time needs to be added). If success is returned, the contract will be renewed successfully, otherwise the contract will fail
    • Expired: contract renewal failed
Elimination of competitive conditionsP132

Two processesAandBWhen trying to get the remaining semaphore, even ifAFirst, the counter is auto incremented, but as long asBCan be the first to put their ownUUIDAdd to the count ordered collection and checkUUIDThe ranking, thenBThe semaphore can be acquired successfully. afterAAnd then put your ownUUIDAdd to the ordered collection and checkUUIDRanking, thenAThe semaphore can also be acquired successfully, which eventually leads to the acquisition of more semaphores than the total number of semaphores.P132

In order to eliminate all possible competition conditions and build a correct counting semaphore, we need to use the distributed lock with timeout function. When you want to acquire the semaphore, you first try to acquire the distributed lock. If you succeed in acquiring the lock, you continue to acquire the semaphore. If you fail to acquire the lock, you also fail to acquire the semaphore.P132

Usage scenarios of different counting semaphoresP133
  • Basic counting semaphore: it doesn’t care about the time difference of multi machine system, and it doesn’t need to refresh the semaphore, and the number of received semaphores occasionally exceeds the limit
  • Fair counting semaphore: it is not very sensitive to the time difference of multi machine system, but it can still receive semaphore, but the number occasionally exceeds the limit
  • Correct counting semaphores: the semaphores are expected to be consistent with correct behavior

This article starts with the official account: full Fu machine (click to view the original), open source in GitHub:reading-notes/redis-in-action
Redis real combat 08. Realize automatic completion, distributed lock and counting semaphore