The problem background

In the process of project development, I encountered a requirement: for a certain record, one user’s operation on it will last for a long time, and I hope that during the operation of one user, another user is not allowed to operate it, because it is easy to cause confusion.

After discussing with colleagues, I wanted to use the Redis lock at first, but this would add other dependencies to the project, so I switched to the Django-cache cache database to implement this function.

Information search

Based on the cache implementation of distributed lock, in the network to find the implementation method, can be summarized as the following three:

The first lock command is INCR

The idea is that if the key does not exist, the value of the key will be initialized to 0, and then the INCR operation will be performed to add one. If the number returned is greater than 1, then the lock is in use.

The second lock command is SETNX

So the idea of locking this is, if the key doesn’t exist, set the key to value and if the key does exist, then SETNX doesn’t do anything

The third lock command is SET

One problem with both of the above methods is that you will find that you need to set the key expiration. So why set key expiration? If the request execution unexpectedly exits for some reason, resulting in a lock being created but not deleted, the lock will persist so that the cache can never be updated. Therefore, we need to add an expiration date to the lock in case of emergency.

In practice, I combined the second and third approaches, using the key name to set the lock, but also set the expiration time to prevent long occupation.

In addition, the API for how to use Django-cache to use database cache is as follows:

from django.core.cache import caches
Set the lock and timeout period
cache.set('my_key'.'Initial value'.60)
# acquiring a lock
cache.get('my_key')
# update locks
cache.add('add_key'.'New value')
Copy the code

The code

After several iterations and comparing various ways of writing on the Internet, I combined the features of Django-Cache and finally concluded a set of relatively concise writing methods.

The first is a CacheLock class, and the initialization method can pass the execution timeout, and the time to wait for the lock. There are two main methods of the CacheLock class, a method to hold a lock and a method to release a lock.

In the lock holding method, the key name is determined by the specific object, the key value is the UUID value, and the default timeout period is 60s. Once the lock is found, the UUID value is returned.

To release a lock, first compare the key value and the UUID value to see if they are the same. If they are the same, release the lock. In this way, other ongoing locks will not be released due to timeout.

class CacheLock(object):
    def __init__(self, expires=60, wait_timeout=0):
        self.cache = cache
        self.expires = expires  # Timeout for function execution
        self.wait_timeout = wait_timeout  # Lock wait timeout

    def get_lock(self, lock_key):
        # Obtain the cache lock
        wait_timeout = self.wait_timeout
        identifier = uuid.uuid4()
        while wait_timeout >= 0:
            if self.cache.add(lock_key, identifier, self.expires):
                return identifier
            wait_timeout -= 1
            time.sleep(1)
        raise LockTimeout({'msg': 'Another user is currently editing the collection configuration, please try again later'})

    def release_lock(self, lock_key, identifier):
        Release the cache lock
        lock_value = self.cache.get(lock_key)
        if lock_value == identifier:
            self.cache.delete(lock_key)
Copy the code

In addition, the cache lock can be written as a decorator that can be added to the lock where it is needed.

def lock(cache_lock):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            lock_key = 'bk_monitor:lock:xxx' # The specific lock_key depends on the parameters passed in the call
            identifier = cache_lock.get_lock(lock_key)
            try:
                return func(*args, **kwargs)
            finally:
                cache_lock.release_lock(lock_key, identifier)
        return wrapper
    return my_decorator
Copy the code

Here’s another example from an actual call:

@lock(CacheLock())
def f(a):
    pass
Copy the code

In addition, when I set the name of the cache key, I pass the corresponding parameters to the decorator according to the function’s operation object, so I won’t give you any examples here.

Optimization to improve

, of course, is a better way to realize the functional requirements above a certain, on the implementation of the lock, the Internet has a lot of other ways, such as based on a zookeeper implementation distributed lock, based on the database to realize distributed lock and so on, they all have different length in terms of reliability or performance, according to the tradeoff between the specific scene, so there is very much worth studying area.

Here I am just throwing bricks to attract jade, welcome to pat bricks ~

The resources

zhuanlan.zhihu.com/p/42056183

www.hollischuang.com/archives/17…

www.jianshu.com/p/182b5ff76…

www.cnblogs.com/huim/p/1086…

Chris – lamb. Co. UK/posts/distr…

Gist.github.com/adewes/6103…

Docs.djangoproject.com/en/1.8/topi…