Handwritten Redis distributed lock (2)

Handwritten Redis distributed lock (2)

1. Analysis of locking and unlocking ideas

In “Handwritten Redis Distributed Lock (1)”, we have already obtained the lock through “while judgment and spin retry + setnx with expiration time + Lua script to delete the lock”, but we have not considered the reentrancy of the lock Sex.

? A reliable distributed lock needs to meet the following conditions:

  • exclusivity
  • high availability
  • anti-deadlock
  • No looting
  • Reentrancy

? So what is a reentrant lock? Reentrant lock, also known as recursive lock, means that when the same thread acquires the lock in the outer method, the inner method that enters the thread will automatically acquire the lock (the premise is that the lock object must be the same object). It has been acquired before and has not been released yet blocked. Both ReentrantLock and synchronized in Java are reentrant locks. One advantage of reentrant locks is that deadlocks can be avoided to a certain extent.

? Synchronized reentrant implementation principle:

? 1. Each lock object has a lock counter and a pointer to the thread holding the lock.

  1. When monitorenter is executed, if the counter of the target lock object is zero, it means that it is not held by other threads, and the Java virtual machine sets the holding thread of the lock object as the current thread, and adds 1 to its counter.

  2. If the counter of the target lock object is not zero, if the thread holding the lock object is the current thread, then the Java virtual machine can add 1 to the counter, otherwise it needs to wait until the thread holding the lock releases the lock.

  3. When executing monitorexit, the Java virtual machine needs to decrement the counter of the lock object by 1. A counter of zero indicates that the lock has been released.

? Considering the principle of synchronized reentrancy implementation, we can use the Hash data structure in Redis to realize the reentrancy of locks. Locking and unlocking are realized through HINCRBYs.

? Locking idea:

  1. First judge whether the key of the Redis distributed lock exists (EXISTS key), and the judgment statement returns 0 to indicate that it does not exist, and hset creates a new lock that belongs to the current thread (HSET key UUID:ThreadID), and set the expiration time, and then return 1, indicating that the lock is successful;
  2. If the judgment statement returns 1, it means that there is already a lock, and it is necessary to further judge whether the lock belongs to the current thread itself. (HEXISTS key UUID:ThreadID), the judgment statement returns 1, indicating that it is its own lock, and auto-increment means reentry (HINCRBY key UUID:ThreadID), then return 1, indicating Locking succeeded. Return 0 to indicate that it is not your own, and then return 0 to indicate that the lock failed.

? Translate the above ideas into Lua scripts:

if redis.call('exists','key') == 0 then
  redis.call('hset','key','uuid:threadid',1)
  redis.call('expire','key',30)
  return 1
elseif redis.call('hexists','key','uuid:threadid') == 1 then
  redis.call('hincrby','key','uuid:threadid',1)
  redis.call('expire','key',30)
  return 1
else
  return 0
end

The merge operation is:

if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadid') == 1 then
  redis.call('hincrby','key','uuid:threadid',1)
  redis.call('expire','key',30)
  return 1
else
  return 0
end

The final Lua script is:

if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then
  redis.call('hincrby',KEYS[1],ARGV[1],1)
  redis. call('expire', KEYS[1], ARGV[2])
  return 1
else
  return 0
end

Description, if there is no lock, or there is a lock and it is held by the current thread lock. Then perform a + 1 operation on the lock count and refresh the expiration time.

Unlocking idea: Judging “there is a lock and it is still the lock held by the current thread” (HEXISTS key uuid:ThreadID), if the judgment statement returns 0, it means that there is no lock at all lock, return nil. If the return of the judgment statement is not 0, then it means that there is a lock and it is your own lock. Directly perform HINCRBY -1 to decrement the lock count by one (unlock once). If the lock count becomes 0, delete it the lock.

Translate to Lua script as:

if redis.call('HEXISTS',lock,uuid:threadID) == 0 then
 return nil
elseif redis.call('HINCRBY',lock,uuid:threadID,-1) == 0 then
 return redis. call('del', lock)
else
 return 0
end

The final Lua script is:

if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then
 return nil
elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then
 return redis.call('del',KEYS[1])
else
 return 0
end

2. Implementation of locking and unlocking

  1. Meet the AQS interface specification definition for Lock in JUC to implement the code, create a new RedisDistributedLock class and implement the Lock interface in JUC.
package com.hyw.redislock.mylock;

import cn.hutool.core.util.IdUtil;
import lombok. SneakyThrows;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;


public class RedisDistributedLock implements Lock
{<!-- -->
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    private long expireTime;

    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuidValue)
    {<!-- -->
        this.stringRedisTemplate = stringRedisTemplate;
        this. lockName = lockName;
        this.uuidValue = uuidValue + ":" + Thread.currentThread().getId();
        this. expireTime = 30L;
    }

    @Override
    public void lock()
    {<!-- -->
        this. tryLock();
    }
    @Override
    public boolean tryLock()
    {<!-- -->
        try
        {<!-- -->
            return this. tryLock(-1L, TimeUnit. SECONDS);
        } catch (InterruptedException e) {<!-- -->
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {<!-- -->
        if(time != -1L)
        {<!-- -->
            expireTime = unit.toSeconds(time);
        }

        String script =
                "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                        "redis. call('hincrby',KEYS[1],ARGV[1],1)" +
                        "redis. call('expire',KEYS[1],ARGV[2])" +
                        "return 1 " +
                        "else " +
                        "return 0 " +
                        "end";
        System.out.println("lockName: " + lockName + "\t" + "uuidValue: " + uuidValue);

        while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))
        {<!-- -->
            try {<!-- --> TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) {<!-- --> e.printStackTrace(); }
        }

        return true;
    }

    @Override
    public void unlock()
    {<!-- -->
        String script =
                "if redis. call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                        "return nil" +
                        "elseif redis. call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
                        "return redis. call('del',KEYS[1]) " +
                        "else " +
                        "return 0 " +
                        "end";
        System.out.println("lockName: " + lockName + "\t" + "uuidValue: " + uuidValue);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if(flag == null)
        {<!-- -->
            throw new RuntimeException("No such lock, no HEXISTS query");
        }
    }

    //================================================== =========
    @Override
    public void lockInterruptibly() throws InterruptedException
    {<!-- -->

    }
    @Override
    public Condition newCondition()
    {<!-- -->
        return null;
    }
}
  1. In order to facilitate future expansion, the factory model is introduced.

?

package com.hyw.redislock.mylock;

import cn.hutool.core.util.IdUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.locks.Lock;

@Component
public class DistributedLockFactory
{<!-- -->
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    public DistributedLockFactory()
    {<!-- -->
        this.uuidValue = IdUtil.simpleUUID();//UUID
    }

    public Lock getDistributedLock(String lockType)
    {<!-- -->
        if(lockType == null) return null;

        if(lockType.equalsIgnoreCase("REDIS")){<!-- -->
            lockName = "RedisLock";
            return new RedisDistributedLock(stringRedisTemplate, lockName, uuidValue);
        } else if(lockType. equalsIgnoreCase("ZOOKEEPER")){<!-- -->
            //TODO zookeeper version distributed lock implementation
            //return new ZookeeperDistributedLock();
            return null;
        } else if(lockType. equalsIgnoreCase("MYSQL")){<!-- -->
            //TODO mysql version distributed lock implementation
            return null;
        }

        return null;
    }
}
  1. Then InventoryService can use the lock we wrote and test the reentrancy of the lock.

    package com.hyw.redislock.service;
    
    import cn.hutool.core.util.IdUtil;
    import com.hyw.redislock.mylock.DistributedLockFactory;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.stereotype.Service;
    
    import java.util.Arrays;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    @Service
    @Slf4j
    public class InventoryService
    {<!-- -->
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        @Value("${server.port}")
        private String port;
        @Autowired
        private DistributedLockFactory distributedLockFactory;
    
        public String sale()
        {<!-- -->
            String retMessage = "";
            Lock redisLock = distributedLockFactory. getDistributedLock("redis");
            // lock
            redisLock. lock();
            try
            {<!-- -->
                //1 Query inventory information
                String result = stringRedisTemplate.opsForValue().get("inventory001");
                //2 Determine whether the inventory is sufficient
                Integer inventoryNumber = result == null ? 0 : Integer. parseInt(result);
                //3 Deduct inventory
                if(inventoryNumber > 0) {<!-- -->
                    stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                    retMessage = "Successfully sold a product, remaining inventory: " + inventoryNumber;
                    System.out.println(retMessage);
                    this.testReEnter();
                }else{<!-- -->
                    retMessage = "The product is sold out, o(╥﹏╥)o";
                }
            }catch (Exception e){<!-- -->
                e.printStackTrace();
            }finally {<!-- -->
                // unlock
                redisLock. unlock();
            }
            return retMessage + "\t" + "Service port number:" + port;
        }
    
        private void testReEnter()
        {<!-- -->
            Lock redisLock = distributedLockFactory. getDistributedLock("redis");
            redisLock. lock();
            try
            {<!-- -->
                System.out.println("################# test reentrant lock ####################### ################");
            }finally {<!-- -->
                redisLock. unlock();
            }
        }
    }
    

3. Automatic renewal

? Consider that if the business has not been executed after the lock expires, then the lock needs to be automatically renewed.

? Automatic renewal idea: If the lock still exists, it means that the business has not been executed, and then reset the expiration time of the lock.

? The Lua script is:

if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then
  return redis. call('expire', KEYS[1], ARGV[2])
else
  return 0
end

And add the business of regularly scanning whether the business is completed in RedisDistributedLock

private void renewExpire()
    {<!-- -->
        String script =
                "if redis. call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
                        "return redis. call('expire',KEYS[1],ARGV[2])" +
                        "else " +
                        "return 0 " +
                        "end";
// Scan every third of the expiration time. If the renewal is successful this time, it means that the business has not been completed, and the scan is performed again recursively.
        new Timer().schedule(new TimerTask()
        {<!-- -->
            @Override
            public void run()
            {<!-- -->
                if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {<!-- -->
                    renewExpire();
                }
            }
        },(this. expireTime * 1000)/3);
    }

Then RedisDistributedLock is modified to:

package com.hyw.redislock.mylock;

import cn.hutool.core.util.IdUtil;
import lombok. SneakyThrows;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Arrays;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;


public class RedisDistributedLock implements Lock
{<!-- -->
    private StringRedisTemplate stringRedisTemplate;
    private String lockName;
    private String uuidValue;
    private long expireTime;

    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuidValue)
    {<!-- -->
        this.stringRedisTemplate = stringRedisTemplate;
        this. lockName = lockName;
        this.uuidValue = uuidValue + ":" + Thread.currentThread().getId();
        this. expireTime = 30L;
    }

    @Override
    public void lock()
    {<!-- -->
        this. tryLock();
    }
    @Override
    public boolean tryLock()
    {<!-- -->
        try
        {<!-- -->
            return this. tryLock(-1L, TimeUnit. SECONDS);
        } catch (InterruptedException e) {<!-- -->
            e.printStackTrace();
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {<!-- -->
        if(time != -1L)
        {<!-- -->
            expireTime = unit.toSeconds(time);
        }

        String script =
                "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then " +
                        "redis. call('hincrby',KEYS[1],ARGV[1],1)" +
                        "redis. call('expire',KEYS[1],ARGV[2])" +
                        "return 1 " +
                        "else " +
                        "return 0 " +
                        "end";
        System.out.println("lockName: " + lockName + "\t" + "uuidValue: " + uuidValue);

        while (!stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))
        {<!-- -->
            try {<!-- --> TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) {<!-- --> e.printStackTrace(); }
        }
        // auto-renew
        this.renewExpire();
        return true;
    }

    @Override
    public void unlock()
    {<!-- -->
        String script =
                "if redis. call('HEXISTS',KEYS[1],ARGV[1]) == 0 then " +
                        "return nil" +
                        "elseif redis. call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then " +
                        "return redis. call('del',KEYS[1]) " +
                        "else " +
                        "return 0 " +
                        "end";
        System.out.println("lockName: " + lockName + "\t" + "uuidValue: " + uuidValue);
        Long flag = stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));
        if(flag == null)
        {<!-- -->
            throw new RuntimeException("No such lock, no HEXISTS query");
        }
    }
    private void renewExpire()
    {<!-- -->
        String script =
                "if redis. call('HEXISTS',KEYS[1],ARGV[1]) == 1 then " +
                        "return redis. call('expire',KEYS[1],ARGV[2])" +
                        "else " +
                        "return 0 " +
                        "end";
        // Scan every third of the expiration time. If the renewal is successful this time, it means that the business has not been completed, and the scan is performed again recursively.
        new Timer().schedule(new TimerTask()
        {<!-- -->
            @Override
            public void run()
            {<!-- -->
                if (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName),uuidValue,String.valueOf(expireTime))) {<!-- -->
                    renewExpire();
                }
            }
        },(this. expireTime * 1000)/3);
    }

    //================================================== =========
    @Override
    public void lockInterruptibly() throws InterruptedException
    {<!-- -->

    }
    @Override
    public Condition newCondition()
    {<!-- -->
        return null;
    }
}

Four. Summary

So far, our handwritten Redis distributed lock has exclusivity, high availability, anti-deadlock, no random grabbing, and reentrancy.

But what if the Redis we use for lock and unlock operations hangs up? Redisson distributed locks can solve this problem.