Simple implementation of redis distributed lock java

Locks are generally used in multithreaded systems. The serial execution of threads is guaranteed by acquiring and releasing locks, that is, synchronous queued execution.

In a single application, we can use the synchronized keyword to ensure the synchronized execution of threads, but in a distributed scenario, because the uniqueness of the lock cannot be guaranteed, the synchronized method is no longer feasible. Need to use a public lock

The use of redis to implement distributed locks mainly uses redis's set (setnx) command and expire command

Before introducing distributed locks, let’s briefly understand redis transaction transactions (official document address https://redis.io/topics/transactions )

MULTI open transaction    EXEC commit transaction

All the commands in a transaction are serialized and executed sequentially

All commands in the transaction are executed serially (note that the failed command in the transaction will not affect the successful command)

Redis script is transactional by definition

The redis script is a transaction by default

The following describes the distributed locks used in two different distributed scenarios (official document address https://redis.io/topics/distlock )

Scenario 1: A service that monitors the redis key expiration is deployed on two servers. When the key expiration event is monitored, only the code on either server needs to be executed (return directly if the lock is not acquired)

The spring-data-redis code is as follows

    @Autowired
    private StringRedisTemplate redisTemplate;

    private Boolean lock(String lockKey){
        Long timeOut = redisTemplate.getExpire(lockKey);
        SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
            List<Object> exec = null;
            @Override
            @SuppressWarnings("unchecked")
            public Boolean execute(RedisOperations operations) throws DataAccessException {
                operations.multi();
                operations.opsForValue().setIfAbsent(lockKey,"lock");
                if(timeOut == null || timeOut == -2) {
                    operations.expire(lockKey, 30, TimeUnit.SECONDS);
                }
                exec = operations.exec();
                if(exec.size() > 0) {
                    return (Boolean) exec.get(0);
                }
                return false;
            }
        };
        return redisTemplate.execute(sessionCallback);
    }

    private void unlock(String lockKey){
        redisTemplate.delete(lockKey);
    }

The jedis code is as follows

    private JedisPool pool = new JedisPool("192.168.92.128",6380);

    public boolean lock(String lockKey){
        Jedis jedis = pool.getResource();

        Long timeOut = jedis.ttl(lockKey);

        Transaction transaction = jedis.multi();
        transaction.setnx(lockKey,"lock");
        if(timeOut == null || timeOut == -2) {
            transaction.expire(lockKey,30);
        }
        List<Object> list = transaction.exec();
        if(!list.isEmpty()){
            Long l = (Long)list.get(0);
            return l == 1L;
        }
        return false;
    }

 Use the SET resource_name my_random_value NX PX 30000 command Jedis's String set (String key, String value, SetParams params) interface

    private JedisPool pool = new JedisPool("192.168.92.128",6380);
    
    public boolean lock(String lockKey){
        Jedis jedis = pool.getResource();
        //不同jedis版本可能接口不一样
        String str = jedis.set(lockKey,"lock", SetParams.setParams().nx().ex(30));
        return str != null && str.equals("OK");
    }

 Use lua script

    private JedisPool pool = new JedisPool("192.168.92.128",6380);
    
    public boolean lockLua(String lockKey){
        String script = "if redis.call('exists',KEYS[1]) == 1 then return 0 else " +
                        "redis.call('set',KEYS[1],ARGV[1]); redis.call('expire',KEYS[1],ARGV[2]);return 1 end;";
        Jedis jedis = pool.getResource();       
        String argv1 = "lock";
        String argv2 = "30";
        List<String> keys = new ArrayList<>();
        List<String> args = new ArrayList<>();
        keys.add(lockKey);
        args.add(argv1);
        args.add(argv2);
        Long result = (Long) jedis.eval(script, keys,args);
        return result == 1;
    }

Scenario 2: An interface for reducing inventory is deployed on two servers. When the inventory is less than a certain amount, 4 users a, b, c, and d request this interface at the same time, and it is necessary to ensure that a, b, c, and d are executed serially ( The execution order is the same as the order in which the lock is acquired)

The spring-data-redis code is as follows

    public boolean lock(String lockKey){
        long timeout = 30000;
        long start = System.currentTimeMillis();
        while (true){
            Boolean b = redisTemplate.execute(new SessionCallback<Boolean>() {
                List<Object> exec = null;
                @Override
                @SuppressWarnings("unchecked")
                public Boolean execute(RedisOperations operations) throws DataAccessException {
                    operations.multi();
                    operations.opsForValue().setIfAbsent(lockKey,"lock");
                    operations.expire(lockKey, 30, TimeUnit.SECONDS);
                    exec = operations.exec();
                    if(exec.size() > 0) {
                        return (Boolean) exec.get(0);
                    }
                    return false;
                }
            });
            if(b != null && b){
                return true;
            }
            long l = System.currentTimeMillis() - start;
            //超时未获取锁直接返回false
            if (l>=timeout) {
                return false;
            }
            try {
                //未获取锁时每100毫秒尝试获取锁
                Thread.sleep(100);
            } catch (InterruptedException e) {
                //
            }
        }
    }

    public void unlock(String lockKey){
        redisTemplate.delete(lockKey);
    }

The test code is as follows

    @Test
    public void testLock(){
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                if(lock("lock_key")){
                    try {
                        System.out.println("t1 thread start");
                        Thread.sleep(3000);
                        System.out.println("t1 thread end");
                        unlock("lock_key");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                if(lock("lock_key")){
                    try {
                        System.out.println("t2 thread start");
                        Thread.sleep(3000);
                        System.out.println("t2 thread end");
                        unlock("lock_key");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();
        t2.start();

        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

The print result is as follows

t1 thread start
t1 thread end
t2 thread start
t2 thread end

When you remove the lock of the run method, you will obviously see the parallel execution of threads

There is a problem with the above locking process. When the execution time of the method exceeds the expiration time set by the redis lock, the lock will be released and other threads will acquire the lock, which will lead to the parallel execution of the method.

The solution can be to start a daemon thread in the method that is executed after acquiring the lock to poll whether the lock expires, and it will automatically be postponed if it expires. The test code is as follows

    @Test
    public void testDaemon() throws Exception{
        if(lock("lock_key")){
            boolean[] flag = new boolean[]{false};
            Thread d = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true){
                        String value = redisTemplate.opsForValue().get("lock_key");
                        if(StringUtils.isEmpty(value)){
                            lock("lock_key");
                        }
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        //如果不是守护线程则在方法外定义一个跳出循环的flag
                        if(flag[0]){
                            break;
                        }
                    }
                }
            });
//            d.setDaemon(true);//开启守护线程
            d.start();
            String value = redisTemplate.opsForValue().get("lock_key");
            System.out.println(value);
            Thread.sleep(31000);
            value = redisTemplate.opsForValue().get("lock_key");
            System.out.println(value);
            flag[0] = true;
        }
    }

 

Use redisson to implement redis distributed lock    https://github.com/redisson/redisson     https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers

Import dependencies

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

Single node configuration    https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95#26-%E5% 8D%95redis%E8%8A%82%E7%82%B9%E6%A8%A1%E5%BC%8F

    Config config = new Config();
    {
        config.useSingleServer().setAddress("redis://192.168.92.128:6380");
    }
    

An instance of acquiring a lock

    RedissonClient client = Redisson.create(config);
    {
        RLock lock = client.getLock("lock_key");
        lock.lock();
        lock.unlock();
    }

The lock and unlock codes are as follows

    Config config = new Config();
    {
        config.useSingleServer().setAddress("redis://192.168.92.128:6380");
    }
    RedissonClient client = Redisson.create(config);

    public boolean lockRedisson(String lockKey){
        RLock lock = client.getLock(lockKey);
        //获取锁  如果20秒内没有获取到锁则获取失败,获取到锁后如果30秒内锁未失效则自动失效
        try {
            return lock.tryLock(20,30,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            return false;
        }
    }

    public void unlockRedisson(String lockKey){
        RLock lock = client.getLock(lockKey);
        lock.unlock();
    }

The test code is as follows (same as the testLock method above)

    @Test
    public void testLockRedisson(){
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                if(lockRedisson("lock_key")){
                    try {
                        System.out.println("t1 thread start");
                        Thread.sleep(3000);
                        System.out.println("t1 thread end");
                        unlockRedisson("lock_key");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                if(lockRedisson("lock_key")){
                    try {
                        System.out.println("t2 thread start");
                        Thread.sleep(3000);
                        System.out.println("t2 thread end");
                        unlockRedisson("lock_key");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();
        t2.start();

        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

The two threads are also executed serially

The process of redisson locking and unlocking is implemented through lua scripts

Lua script to acquire lock

        <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit,     
                            long threadId, RedisStrictCommand<T> command) {

        //过期时间
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  //如果锁不存在,则通过hset设置它的值,并设置过期时间
                  "if (redis.call('exists', KEYS[1]) == 0) then " +
                      "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  //如果锁已存在,并且锁的是当前线程,则通过hincrby给数值递增1
                  "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                      "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                      "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                      "return nil; " +
                  "end; " +
                  //如果锁已存在,但并非本线程,则返回过期时间ttl
                  "return redis.call('pttl', KEYS[1]);",
        Collections.<Object>singletonList(getName()), 
                internalLockLeaseTime, getLockName(threadId));
    }

You can see that the lock in redis uses a hash structure

This LUA code does not look complicated, there are three judgments:

Judging by exists, if the lock does not exist, set the value and expiration time, and the lock is successful

Judging by hexists, if the lock already exists and the current thread is locked, the corresponding value+1 will be executed when hincrby is executed, and the lock is successful (re-entry lock). For example:

    @Test
    public void testRent(){
        //可重入锁 无论一个线程中锁几次都是获取成功的  b=true  b1= true  锁几次就需要释放几次锁
        boolean b = lockRedisson("lock_key");
        boolean b1 = lockRedisson("lock_key");
        System.out.println(b);
        System.out.println(b1);
        //锁了两次需要释放两次
        unlockRedisson("lock_key");
        unlockRedisson("lock_key");
    }

If the lock already exists, but the lock is not the current thread, it proves that there are other threads holding the lock. Returns the expiration time of the current lock, the lock failed

Release lock

    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, EVAL,
    
            //如果锁已经不存在, 发布锁释放的消息
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; " +
            "end;" +
            //如果释放锁的线程和已存在锁的线程不是同一个线程,返回null
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                "return nil;" +
            "end; " +
            //通过hincrby递减1的方式,释放一次锁
            //若剩余次数大于0 ,则刷新过期时间
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                "return 0; " +
            //否则证明锁已经释放,删除key并发布锁释放的消息
            "else " +
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
    Arrays.<Object>asList(getName(), getChannelName()), 
        LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

}

If the lock no longer exists, publish the lock release message through publish, and the unlock is successful

If the unlocked thread and the currently locked thread are not the same, the unlocking fails and an exception is thrown

Decrement by hincrby by 1, release the lock first. If the remaining number of times is still greater than 0, it proves that the current lock is a re-entry lock, and the expiration time is refreshed; if the remaining number of times is equal to 0, delete the key and issue a lock release message, and the unlock is successful

Note that a thread unlocks the lock acquired by b thread will throw an exception

 

Guess you like

Origin blog.csdn.net/name_is_wl/article/details/113704071