preface

This article mainly introduces the distributed lock implementation based on Redis exactly how to do one thing, which refers to the article written by many big guys, is a summary of distributed lock

Overview of distributed locks

In a multithreaded environment, to ensure that a block of code can only be accessed by one thread at a time, Java typically uses synchronized syntax and ReetrantLock, which are essentially local locks. However, distributed architecture is popular in companies nowadays. In a distributed environment, how to ensure the synchronization of threads on different nodes?

In fact, for distributed scenarios, we can use distributed locks, which are a way of controlling mutually exclusive access to shared resources between distributed systems.

For example in a distributed system, multiple services deployed on multiple machines, when a user initiated a client data into the request, if no guarantees distributed locking mechanism, so that multiple services on the machine may be concurrent insert operation, resulting in repeated injection data, for some business are not allowed to have extra data, it can cause problems. The distributed lock mechanism is used to solve such problems by ensuring mutually exclusive access to shared resources between multiple services. If a service preempts the distributed lock and other services do not obtain the lock, subsequent operations are not performed. The general meaning is shown below (not necessarily accurate) :

Distributed locking features

Distributed locks generally have the following characteristics:

  • Mutual exclusion: Only one thread can hold the lock at a time
  • Reentrancy: The same thread on the same node can acquire the lock again if it has acquired it
  • Lock timeout: Supports lock timeout as in J.U.C to prevent deadlocks
  • High performance and high availability: Locking and unlocking must be efficient, and high availability must be ensured to prevent distributed lock failure
  • Blocking and non-blocking: Able to wake up in a blocked state in a timely manner

The implementation of distributed lock

We generally implement distributed locks in the following ways:

  • Database-based
  • Based on the Redis
  • Based on a zookeeper

This article mainly introduces how to implement distributed lock based on Redis

Redis distributed lock implementation

1. Use the setnx+expire command (incorrect)

Redis SETNX key value command, SETNX key value, set key to value, only if the key does not exist, do nothing, if the key does exist, return 1 on success, return 0 on failure. SETNX is actually an abbreviation for SET IF NOT Exists

Because the distributed lock also requires a timeout mechanism, we use the expire command to set it, so the core code of the setnx+ EXPIRE command is as follows:

public boolean tryLock(String key,String requset,int timeout) {
    Long result = jedis.setnx(key, requset);
    // If result = 1, the setting succeeds; otherwise, the setting fails
    if (result == 1L) {
        return jedis.expire(key, timeout) == 1L;
    } else {
        return false; }}Copy the code

Setnx and EXPIRE are separate and non-atomic operations. If the first command is executed and an exception is applied or the lock is restarted, the lock will not expire.

An improvement is to use Lua scripts to ensure atomicity (including setnx and EXPIRE directives)

2. Use Lua scripts (including setnx and EXPIRE directives)

The following code

public boolean tryLock_with_lua(String key, String UniqueId, int seconds) {
    String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
            "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
    List<String> keys = new ArrayList<>();
    List<String> values = new ArrayList<>();
    keys.add(key);
    values.add(UniqueId);
    values.add(String.valueOf(seconds));
    Object result = jedis.eval(lua_scripts, keys, values);
    // Check whether it succeeded
    return result.equals(1L);
}
Copy the code

3. Using the set key value [EX seconds] [PX milliseconds] [NX | XX] command (right)

Starting with version 2.6.12, Redis added a series of options to the SET command:

SET key value[EX seconds][PX milliseconds][NX|XX]
Copy the code
  • EX seconds: Specifies the expiration time, in seconds
  • PX milliseconds: Sets the expiration time, in milliseconds
  • NX: This value is set only when the key does not exist
  • XX: Set this value only when the key exists

The nx option of the set command is equivalent to the setnx command, and the code process is as follows:

public boolean tryLock_with_set(String key, String UniqueId, int seconds) {
    return "OK".equals(jedis.set(key, UniqueId, "NX"."EX", seconds));
}
Copy the code

Value has to be unique, we can do that with UUID, set random string to be unique, why is it unique? If value is not a random string but a fixed value, the following problems may occur:

  • 1. Client 1 successfully obtains the lock
  • 2. Client 1 blocks an operation for a long time
  • 3. The key expires, and the lock is automatically released
  • 4. Client 2 has obtained the lock corresponding to the same resource
  • 5. Client 1 recovers from the block and releases the lock held by client 2 because the value is the same, causing a problem

So in general, we need to validate value when we release the lock

Release lock implementation

In other words, we need to set a value when acquiring the lock. We cannot use del key directly, because any client can unlock the lock directly. Therefore, when unlocking, we need to determine whether the lock is our own, based on the value. The code is as follows:

public boolean releaseLock_with_lua(String key,String value) {
    String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
            "return redis.call('del',KEYS[1]) else return 0 end";
    return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L);
}
Copy the code

The way Lua scripts are used here is as atomistic as possible.

Using the set key value [EX seconds] [PX milliseconds] [NX | XX] command looks OK, in fact also can appear when Redis cluster problems, such as A client has A lock on the master node of Redis and However, the locked key has not been synchronized to the slave node. The master fails and failover occurs. A slave node is upgraded to the master node.

So there are other solutions for Redis clustering

4. Redlock algorithm and Redisson implementation

Redis author Antirez proposed a more advanced distributed lock implementation Redlock based on distributed environment, the principle is as follows:

Redlock: Redis distributed lock the best implementation and Redis. IO /topics/dist…

Suppose there are 5 independent Redis nodes (note that the nodes can be 5 single master instances of Redis or 5 clusters of Redis, but not a Cluster of 5 primary nodes) :

  • Gets the current Unix time in milliseconds
  • When requesting a lock from Redis, the client should set a network connection and response timeout that is less than the lock expiration time. For example, your lock expires automatically for 10 seconds. The timeout period should be between 5 and 50 milliseconds, so that the client is not still waiting for the response result when the server Redis has been suspended. If the server does not respond within the specified time, the client should try to obtain the lock from another Redis instance as soon as possible
  • The client obtains the lock usage time by subtracting the current time from the time of starting the lock acquisition (the time recorded in Step 1). The lock is successfully acquired if and only if the lock is obtained from most Redis nodes (N/2+1, here is 3 nodes) and the usage time is less than the lock failure time.
  • If a lock is obtained, the true valid time of the key is equal to the valid time minus the time used to obtain the lock (calculated in Step 3).
  • If, for some reason, the lock fails to be acquired (not in at least N/2+1 Redis instances or the lock has been acquired for an extended period of time), the client should unlock all Redis instances (even if some Redis instances are not locked at all). Prevents some node from acquiring the lock but the client does not get the response so that the lock cannot be reacquired for a later period of time.

Redisson implements simple distributed locks

Jedis is a Java client of Redis. In addition to Jedis, Redisson is also a Java client of Redis. Jedis is blocking I/O, while Redisson uses Netty to implement non-blocking I/O. This client encapsulates the Lock and inherits the Lock interface from J.U.C, so we can use Redisson just like ReentrantLock, as follows.

  1. Add POM dependencies first
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.10.6</version>
</dependency>
Copy the code
  1. To use Redisson, the code is as follows (similar to using ReentrantLock)
// 1. Configuration file
Config config = new Config();
config.useSingleServer()
        .setAddress("Redis: / / 127.0.0.1:6379")
        .setPassword(RedisConfig.PASSWORD)
        .setDatabase(0);
/ / 2. RedissonClient construction
RedissonClient redissonClient = Redisson.create(config);

//3. Set the name of the locked resource
RLock lock = redissonClient.getLock("redlock");
lock.lock();
try {
    System.out.println("Lock obtained successfully, business logic implemented");
    Thread.sleep(10000);
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    lock.unlock();
}
Copy the code

About Redlock algorithm implementation, we can use in Redisson RedissonRedLock to complete, use specific details you can refer to the articles of: mp.weixin.qq.com/s/8uhYult2h…

Redis implements distributed locking wheels

The following uses SpringBoot + Jedis + AOP combination to achieve a simple distributed lock.

1. Custom annotations

Define a custom annotation that performs the logic to acquire the distributed lock

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisLock {
    /** ** Business key **@return* /
    String key(a);
    /** * The number of seconds the lock will expire. The default is 5 seconds **@return* /
    int expire(a) default 5;

    /** * Try to lock, Max wait time **@return* /
    long waitTime(a) default Long.MIN_VALUE;
    /** * Lock timeout time unit **@return* /
    TimeUnit timeUnit(a) default TimeUnit.SECONDS;
}
Copy the code

2. AOP interceptor implementation

In AOP we execute the logic to acquire and release distributed locks as follows:

@Aspect
@Component
public class LockMethodAspect {
    @Autowired
    private RedisLockHelper redisLockHelper;
    @Autowired
    private JedisUtil jedisUtil;
    private Logger logger = LoggerFactory.getLogger(LockMethodAspect.class);

    @Around("@annotation(com.redis.lock.annotation.RedisLock)")
    public Object around(ProceedingJoinPoint joinPoint) {
        Jedis jedis = jedisUtil.getJedis();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        RedisLock redisLock = method.getAnnotation(RedisLock.class);
        String value = UUID.randomUUID().toString();
        String key = redisLock.key();
        try {
            final boolean islock = redisLockHelper.lock(jedis,key, value, redisLock.expire(), redisLock.timeUnit());
            logger.info("isLock : {}",islock);
            if(! islock) { logger.error("Lock acquisition failed");
                throw new RuntimeException("Lock acquisition failed");
            }
            try {
                return joinPoint.proceed();
            } catch (Throwable throwable) {
                throw new RuntimeException("System exception"); }}finally {
            logger.info("Release lock"); redisLockHelper.unlock(jedis,key, value); jedis.close(); }}}Copy the code

3. Redis implements distributed lock core classes

@Component
public class RedisLockHelper {
    private long sleepTime = 100;
    /** * Use setnx + EXPIRE to get distributed locks directly@param key
     * @param value
     * @param timeout
     * @return* /
    public boolean lock_setnx(Jedis jedis,String key, String value, int timeout) {
        Long result = jedis.setnx(key, value);
        // If result = 1, the setting succeeds; otherwise, the setting fails
        if (result == 1L) {
            return jedis.expire(key, timeout) == 1L;
        } else {
            return false; }}/** * Use the Lua script, which uses the setnex+expire command to lock **@param jedis
     * @param key
     * @param UniqueId
     * @param seconds
     * @return* /
    public boolean Lock_with_lua(Jedis jedis,String key, String UniqueId, int seconds) {
        String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
                "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
        List<String> keys = new ArrayList<>();
        List<String> values = new ArrayList<>();
        keys.add(key);
        values.add(UniqueId);
        values.add(String.valueOf(seconds));
        Object result = jedis.eval(lua_scripts, keys, values);
        // Check whether it succeeded
        return result.equals(1L);
    }

    /** * In 2.6.12 and later, use the set key value [NX] [EX] command **@param key
     * @param value
     * @param timeout
     * @return* /
    public boolean lock(Jedis jedis,String key, String value, int timeout, TimeUnit timeUnit) {
        long seconds = timeUnit.toSeconds(timeout);
        return "OK".equals(jedis.set(key, value, "NX"."EX", seconds));
    }

    /** * Custom lock timeout **@param jedis
     * @param key
     * @param value
     * @param timeout
     * @param waitTime
     * @param timeUnit
     * @return
     * @throws InterruptedException
     */
    public boolean lock_with_waitTime(Jedis jedis,String key, String value, int timeout, long waitTime,TimeUnit timeUnit) throws InterruptedException {
        long seconds = timeUnit.toSeconds(timeout);
        while (waitTime >= 0) {
            String result = jedis.set(key, value, "nx"."ex", seconds);
            if ("OK".equals(result)) {
                return true;
            }
            waitTime -= sleepTime;
            Thread.sleep(sleepTime);
        }
        return false;
    }
    /** * Wrong unlock method - delete key ** directly@param key
     */
    public void unlock_with_del(Jedis jedis,String key) {
        jedis.del(key);
    }

    /** * Use the Lua script to unlock, and verify the value ** when unlocking@param jedis
     * @param key
     * @param value
     * @return* /
    public boolean unlock(Jedis jedis,String key,String value) {
        String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
                "return redis.call('del',KEYS[1]) else return 0 end";
        return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L); }}Copy the code

4. Controller Layer control

Define a TestController to test our implementation of the distributed lock

@RestController
public class TestController {
    @RedisLock(key = "redis_lock")
    @GetMapping("/index")
    public String index(a) {
        return "index"; }}Copy the code

summary

Distributed locking focuses on mutual exclusion, with only one client acquiring the lock at any one time. In the actual production environment, the implementation of distributed lock may be more complex, and I here mainly for the stand-alone environment based on Redis distributed lock implementation, as for Redis cluster environment did not involve too much, interested friends can refer to the relevant information.

Project source code address: github.com/pjmike/redi…

References & acknowledgements

  • Mp.weixin.qq.com/s/eHsuEc8Dq…
  • Mp.weixin.qq.com/s/y2HPj2ji2…
  • Mp.weixin.qq.com/s/8uhYult2h…
  • Mp.weixin.qq.com/s/xCe2ljuhM…
  • Crossoverjie. Top / 2018/03/29 /…
  • Blog.battcn.com/2018/06/13/…
  • Redis. IO/switchable viewer/dist…
  • Zhangtielei.com/posts/blog-…