Redis based distributed lock implementation

Keywords: Redis Jedis Java less

From

1, Distributed lock overview

In the multithreaded environment, in order to ensure that a code block can only be accessed by one thread at a time, we can generally use the synchronized syntax and ReetrantLock in Java to ensure that this is actually the way of local locking. But now companies are all popular distributed architecture, in the distributed environment, how to ensure that the threads of different nodes execute synchronously? In fact, for distributed scenarios, we can use distributed locks, which is a way to control the mutual exclusive access of shared resources between distributed systems.
For example, in a distributed system, multiple services are deployed on multiple machines. When a client user initiates a data insertion request, if there is no guarantee of distributed locking mechanism, multiple services on multiple machines may be inserted concurrently, resulting in repeated data insertion. For some businesses that do not allow redundant data, this will cause problems Question. The purpose of distributed lock mechanism is to solve such problems and ensure mutually exclusive access to shared resources among multiple services. If one service preempts the distributed lock and other services fail to acquire the lock, no subsequent operations will be carried out. The general meaning is as follows

2, Characteristics of distributed lock

Distributed locks generally have the following characteristics:

  • Mutex: only one thread can hold lock at the same time
  • Reentrancy: the same thread on the same node can acquire the lock again after acquiring the lock
  • Lock timeout: like the lock in J.U.C, it supports lock timeout to prevent deadlock
  • High performance and high availability: locking and unlocking need to be efficient, but also need to ensure high availability to prevent the failure of distributed locks
  • Blocking and non blocking: able to wake up in time from blocking state

3, Implementation of distributed lock

We generally implement distributed locks in the following ways:

  1. Database based
  2. Based on Redis
  3. Based on zookeeper

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

4, Redis's distributed lock implementation

  1. Using setnx+expire command (wrong way)
    The SETNX command of Redis, setnx key value, sets the key to value. Only when the key does not exist, can it succeed. If the key exists, do nothing, return 1 for success and 0 for failure. SETNX is actually the abbreviation of SET IF NOT Exists. Because distributed locks also need timeout mechanism, we use the expire command to set them. Therefore, 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);
    // When result = 1, the setting succeeds, otherwise, the setting fails
    if (result == 1L) {
        return jedis.expire(key, timeout) == 1L;
    } else {
        return false;
    }
}

In fact, there is a problem with the above steps. setnx and expire are two separate steps. They are not atomic. If the first instruction is executed and the application is abnormal or restarted, the lock will not expire.
One improvement is to use Lua script to ensure atomicity (including setnx and expire instructions)

  1. Use Lua script (including setnx and expire instructions)
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);
    //Judge success
    return result.equals(1L);
}
  1. Use the set key value [EX seconds][PX milliseconds][NX|XX] Command (correct way)
    Redis started in version 2.6.12, adding a series of options to the SET command:
SET key value[EX seconds][PX milliseconds][NX|XX]

Parameter Description:

  • EX seconds: set expiration time in seconds
  • PX milliseconds: set the expiration time in milliseconds
  • NX: set value only when key does not exist
  • 20: Set value only if key exists
    The nx option of the set command is equivalent to the setnx command. 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));
}

Value must be unique. We can use UUID to do this. Set random string to ensure uniqueness. Why guarantee uniqueness? If value is not a random string but a fixed value, the following problems may exist:

1. Client 1 obtains the lock successfully
2. Client 1 has been blocked for too long on an operation
3. The set key expires, and the lock is released automatically
4. Client 2 obtains the lock corresponding to the same resource
5. Client 1 recovers from blocking. Because the value value is the same, the lock held by client 2 will be released when the lock release operation is performed, which will cause problems

So in general, we need to verify the value when releasing the lock

  1. Implementation of releasing lock
    When releasing a lock, we need to verify the value value. That is to say, we need to set a value when obtaining the lock. We can't use del key directly. Because any client can unlock del key directly, we need to judge 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);
}

Here, Lua script is used to ensure atomicity as much as possible.
Use the set key value [EX seconds][PX milliseconds][NX|XX] Command It seems OK. In fact, there will be problems in Redis cluster. For example, client A has obtained the lock on the master node of Redis, but the locked key has not been synchronized to the slave node. If the master fails and fails over, A slave node is upgraded to the master node, and client B can also obtain the lock of the same key, but client A has also obtained the lock Cause multiple clients to get locks.
So there are other solutions for Redis cluster

  1. Redlock algorithm and implementation of Redisson
    Anti rez, the author of Redis, proposes a more advanced implementation of distributed lock based on the distributed environment. The principle is as follows:
    Refer to article Redlock: The implementation of Redis distributed lock and redis.io/topics/dist...

Suppose there are 5 independent Redis nodes (note that the nodes here can be 5 Redis single master instances or 5 Redis Cluster clusters, but they are not cluster clusters with 5 master nodes):

  • Gets the current Unix time in milliseconds
  • Try to acquire locks from five instances in turn using the same key and unique value (such as UUID). When requesting locks from Redis, the client should set a network connection and response timeout, which should be less than the lock's expiration time. For example, if your lock's automatic expiration time is 10s, the timeout should be between 5-50ms, which can avoid service When Redis on the server side has been hung up, the client is still waiting for the response result. If the server fails to respond within the specified time, the client should try to go to another Redis instance as soon as possible to request a lock
  • The client uses the current time minus the time of starting to acquire the lock (the time recorded in step 1) to obtain the time of using the lock. If and only if most of the Redis nodes (N/2+1, here are 3 nodes) acquire the lock, and the time of using the lock is less than the time of lock failure, the lock is successful.
  • If the lock is acquired, the real effective time of the key is equal to the effective time minus the time used to acquire the lock (the result calculated in step 3)
  • If for some reasons, the lock acquisition fails (the lock is not acquired in at least N/2+1 Redis instances or the lock acquisition time has exceeded the effective time), the client should unlock all Redis instances (even if some Redis instances have not been successfully locked at all, to prevent some nodes from acquiring the lock but the client has not received a response, so that the next period of time cannot be Reacquire lock)
  1. Redisson realizes simple distributed lock
    For Java users, we often use jedis, which is the Java client of Redis. In addition to jedis, Redisson is also the Java client. Jedis is blocking I/O, while the bottom layer of Redisson uses Netty to realize non blocking I/O. the client encapsulates the Lock and inherits the J.U.C Lock interface, so we can use Redisson as we use ReentrantLock. For specific use The process is as follows.

A. Add POM dependency first

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.10.6</version>
</dependency>

B. Using Redisson, the code is as follows (similar to using ReentrantLock)

// 1. Profile
Config config = new Config();
config.useSingleServer()
        .setAddress("redis://127.0.0.1:6379")
        .setPassword(RedisConfig.PASSWORD)
        .setDatabase(0);
//2. Construct RedissonClient
RedissonClient redissonClient = Redisson.create(config);

//3. Set lock resource name
RLock lock = redissonClient.getLock("redlock");
lock.lock();
try {
    System.out.println("Obtain lock successfully and implement business logic");
    Thread.sleep(10000);
} catch (InterruptedException e) {
    e.printStackTrace();
} finally {
    lock.unlock();
}

For the implementation of the Redlock algorithm, we can use the Redisson Redlock in Redisson, and the specific details can be Refer to the big guy's article

5, Distributed lock wheel implemented by Redis

Next, use the combination of SpringBoot + Jedis + AOP to implement a simple distributed lock.

  1. Custom annotation
    Customize an annotation, and the annotated method will execute the logic of acquiring the distributed lock
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RedisLock {
    /**
     * Business key
     *
     * @return
     */
    String key();
    /**
     * Lock expiration seconds, default is 5 seconds
     *
     * @return
     */
    int expire() default 5;

    /**
     * Try to lock, waiting time at most
     *
     * @return
     */
    long waitTime() default Long.MIN_VALUE;
    /**
     * Time out unit of lock
     *
     * @return
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
}
  1. Implementation of AOP interceptor
    In AOP, we execute the logic of acquiring and releasing distributed locks. The code is 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("Failed to acquire lock");
                throw new RuntimeException("Failed to acquire lock");
            }
            try {
                return joinPoint.proceed();
            } catch (Throwable throwable) {
                throw new RuntimeException("System exception");
            }
        }  finally {
            logger.info("Release lock");
            redisLockHelper.unlock(jedis,key, value);
            jedis.close();
        }
    }
}
  1. Redis implements distributed lock core class
@Component
public class RedisLockHelper {
    private long sleepTime = 100;
    /**
     * Using setnx + expire to obtain distributed lock directly
     * Non atomicity
     *
     * @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);
        // When result = 1, the setting succeeds, otherwise, the setting fails
        if (result == 1L) {
            return jedis.expire(key, timeout) == 1L;
        } else {
            return false;
        }
    }

    /**
     * Use Lua script, in which setnex+expire command is used for lock operation
     *
     * @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);
        //Judge success
        return result.equals(1L);
    }

    /**
     * In Redis 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 get 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 unlocking method - delete key directly
     *
     * @param key
     */
    public void unlock_with_del(Jedis jedis,String key) {
        jedis.del(key);
    }

    /**
     * Use Lua script to unlock and verify the value 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);
    }
}
  1. Controller layer control
Define a TestController to test the distributed locks we implement
@RestController
public class TestController {
    @RedisLock(key = "redis_lock")
    @GetMapping("/index")
    public String index() {
        return "index";
    }
}

6, Summary

Distributed locks focus on mutual exclusion. At any time, only one client obtains the lock. In the actual production environment, the implementation of distributed locks may be more complex. I'm here to talk about the implementation of Redis based distributed locks in a stand-alone environment. As for the Redis cluster environment, it's not too much involved. Interested friends can refer to relevant materials.

Posted by hillbilly928 on Fri, 19 Jun 2020 00:04:48 -0700