Skip to content

Instantly share code, notes, and snippets.

@adamnoto
Last active February 13, 2022 01:34
Show Gist options
  • Select an option

  • Save adamnoto/17b297db59a4d1a33fcbf394699aeeb2 to your computer and use it in GitHub Desktop.

Select an option

Save adamnoto/17b297db59a4d1a33fcbf394699aeeb2 to your computer and use it in GitHub Desktop.
Differences of Redis Lock

Review of redis-lock

  • A Lock is a struct consisting of:

    • client: the RedisClient
    • key: user-defined value
    • opts: an object of LockOptions
    • token: generated by the library on various life cycles from obtaining a lock, refreshing to releasing it
    • mutex: an instance of sync.Mutex to enure atomicity/exclusivity of certain operations, such as:
      • checking when it IsLocked()
      • performing a Lock()
      • performing an Unlock()
  • It has LockOptions which is a struct consisting of

    • LockTimeout (def 5s)
    • WaitTimeout (def 0; no wait)
    • WaitRetry (def 100ms)

    User doesn't have to construct this struct even if this struct is required in all operations of the library. This is because this object will be instantiated with default values if user doesn't specify it with anything.

  • Common lock methods interface is a RedisClient, and the string key, and also an opts which is a pointer to LockOptions

  • NewLock is for creating a new lock, without performing any locking. At this stage, a LockOptions will be created if the user provided none. This operation can't result in an error.

  • ObtainLock is creating a NewLock() and then Lock it (if there's no error)--otherwise, an error is returned. This operation may result in an error, in which case nil and an error will be returned.

  • IsLocked simply check if token is equal to empty string or not, if it is then it is locked. The checking operation is synchronized through the mutex.

  • Lock will create() a lock if there's no token (apparently, not yet locked), otherwise if there's a token value set (apparently, has been locked before), the token is simply refresh()-ed. The operation is synchronized.

    • In whichever case, create() or refresh(), Lock() ended up with create()

    • create() will replace the token (if exist, that is) with a new one if a lock can be obtain()-ed. If locks couldn't be obtained after several retries (and sleep), it won't change the old token (if exists) and will return false alongside the error; otherwise true and nil.

    • When lock is being obtain()-ed, a key will be set on Redis using SETNX. The key itself is user defined, whereby the value of the key is the library-generated token

      Note however, SETNX has been discouraged in favor of the redlock algorithm. SETNX means: SET if not exists.

    • refresh() will reinstate the same key with the (potentially) same expiry period using PEXPIRE. It's said potentially becaue user can change the LockOptions before calling Lock(). This operation will be performed, instead of create() if the token isn't empty.

      • A token is simply a randomly generated string, this is an internal data
  • Unlock releases the lock. The operation is synchronized. It will set the token to an empty string, if there's no issue with the process. The process is deleting the key that has been set on redis.

  • When lock is being created:

    • a random token is generated
    • an attempt to obtain the token by registering it to Redis is performed,

Review of redislock

  • It has internal error "class" such as ErrNotObtained and ErrLockNotHeld. Library users (read: we) should be more comfortable checking against those predefined set of errors.
  • The RedisClient type is self-defined interface (which modern/newer driver should conform). Noticable different with the RedisClient used by redis-lock is that, functions (such as SetNX which are used in both cases) now accept context.Context as the first argument of the client. This result in functions such as Obtain() to require context as well, something that redis-lock doesn't need.
  • A Lock is a struct consisting of:
    • client which is of type Client (defined in the package), consisting of:
      • client the actual RedisClient
      • tmp a byte array
      • tmpMu a mutex instance, along with tmp used by internal randomToken() function to generate random key
    • key, a string, user-defined value
    • value, a string, library-generated, but we can append metadata to it via Options.Metadata if desired
  • An Options is a struct consisting of:
    • RetryStrategy of type RetryStrategy, if null, will return the value of NoRetry() which acquire the lock only one.
    • Metadata, a string appended to the lock token
    • getMetadata() returning "" if the Metadata is not set (= nil).
    • getRetryStrategy() returning value of NoRetry() if the RetryStrategy is not set.
  • RetryStrategy is an interface consisting of only NextBackoff() function that returns a time.Duration.
    • There's an internal type linearBackoff that "inherits" time.Duration but having this NextBackoff() function implemented
      • NextBackoff() of this simply returns an instance of time.Duration
    • Provided functions that returns RetryStrategy:
      • LinearBackoff given a duration, retries regularly with customized intervals
      • NoRetry returns a linearBackoff(0) so there's no further attempt of retries
      • LimitRetry retries only as much as desired. This constructor function requires another RetryStrategy to be passed of which, its NextBackoff() will be called when possible. Possible only if maximum retries has not exceeded the set max value.
      • ExponentialBackoff is time-based, so it requires two arguments both of type time.Duration: min and max. The NextBackoff() is computed to be 2**n milliseconds where n means number of times, which will set to increase as time went by.
  • A Client.Obtain() is used to try to obtain a new lock within certain deadline. The deadline system is calculated internally, and it requires ttl and thus it's an argument we need to pass when calling the function. It will try to retry as much as possible as long as still not yet out of the deadline, and NextBackoff() of the retry strategy (passed via opt which is an instance of Options) is not less than 1. It returns a *Lock if everything is okay. Otherwise, error it might return:
    • ErrNotObtained when backoff returns a value less than 1.
  • The Lock.Release() can be used to release a lock. However, it's considered an ErrLockNotHeld if no lock has been optained at the time the function is called. Release simply performs quite similar things with the older redis-lock library: simply deleting the key from a Redis instance/cluster.
  • Lock.Key() returns the user-defined key set when obtaining a lock
  • Lock.Token() returns randomly generated value
  • Lock.Metadata() returns metadata of the lock that is set by the user, if set
  • Lock.TTL() returns the remaining time to live of the lock, much better than IsLocked() of the older library. If there's an error when calling this function, whatever the error might be, the returned time to live will be 0.
  • Lock.Refresh() to refresh/renew the lock

Conclusion

  • They both still want to achieve the same thing.
  • redislock has equally simple public methods, but they are much more feature-rich
  • redislock is much more accommodating to modern/later version of Go and libraries
  • redislock has no LockOptions, configuring the timing and stuff is done by "playing around" with the Options.
  • redislock has far superior and customizable RetryStrategy through Options (there is no such thing in LockOptions, their way of doing it is very rigid in the old library).
  • redislock Release is similar in terms of purpose to redis-lock Unlock, and IsLocked() is now TTL which is better since it also tell us the remaining time the lock is valid
  • redislock allowed us to append certain metadata (in the form of a string). This is a feature entirely not possible with the older implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment