Distributed Lock principles and implementation

Disclaimer: This article is a blogger original article, follow the CC 4.0 BY-SA copyright agreement, reproduced, please attach the original source link and this statement.
This link: https://blog.csdn.net/huyunqiang111/article/details/99694402

First, why should use the Distributed Lock

           Due to the development and complexity of the business, the need to use the cluster, a service is deployed on the server are polymorphic, and then do load balancing. In order to ensure a method or property can only be performed in the same thread at the same time high concurrency, in case of the conventional single stand-alone application deployment, Java can be used concurrently process related API (such as ReentrantLock or Synchronized) mutex control. In a stand-alone environment, Java provides many concurrent processing related to the API. However, as the business development needs, the original single-machine deployment system is evolved into a distributed cluster system, due to the distributed system multi-threaded, multi-process and distributed on different machines, which will concurrently under the original stand-alone deployment control locking strategy fails, a simple Java API does not provide the ability to distributed locks. To solve this problem we need a mutual exclusion mechanism across JVM to control access to shared resources, which is distributed lock problem to be solved!

Second, what conditions should have distributed lock

1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行; 
2、高可用的获取锁与释放锁; 
3、高性能的获取锁与释放锁; 
4、具备可重入特性; 
5、具备锁失效机制,防止死锁; 
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

Third, the implementation within Distributed Lock

基于数据库实现分布式锁; 
基于缓存(Redis等)实现分布式锁; 
基于Zookeeper实现分布式锁;

Fourth, based on the implementation of the database

The core idea of the database based implementation is: Create a table in the database, the table contains the method name and other fields, and create a unique index on the method name field , you want to execute a method, use this method name to the table insert data, successfully inserted Acquires the lock, delete the corresponding row after the completion of the implementation of data releases the lock.

Fifth, based on Redis implementation

1, the choice of implementing distributed lock Redis reasons:

(1) Redis has a high performance; 
(2) Redis commands support this better, achieve more convenient

2, use the command description:

(1) SETNX: SETNX key val: if and only if the key does not exist, a SET key is a string of val, returns 1; if the key is present, then do nothing, 0 is returned.

(2) expire: expire key timeout: Set a timeout for the key, the unit is second, more than this time will automatically release the lock, to avoid deadlock.

(3)delete:delete key:删除key

When using Redis implementation of distributed locks, will be used primarily to these three commands.

3, to realize ideas:

(1) when acquiring the lock, using setnx lock, and the lock using the expire command to add a timeout period, this time is exceeded will automatically release the lock, the lock is a value of the UUID randomly generated, this time by the lock release judge.

(2) to obtain a lock when it set a timeout acquired over this time if you give up to acquire the lock.

(3) when the lock is released, it is not determined by the UUID of the lock, if the lock, the lock is released for the delete.

4, distributed lock the simple implementation code:

package com.fs.fscloudadmin.redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

import java.util.List;
import java.util.UUID;

/**
 * 分布式锁 redis 实现的简单代码
 * @author huyunqiang
 * @Date 2019/8/14 21:10
 */
public class RedisLock {
    private final JedisPool jedisPool;

    //初始化
    public RedisLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    /**
     *
     * @param lockName   锁 key
     * @param acquireTimeout  获取超时时间
     * @param timeout         锁的超时时间
     * @return                锁标识
     */
    public String lockWithTimeOut(String lockName,long acquireTimeout,long timeout){
       Jedis conn = null;
       String retIdentifier = null;
       try {
           //获取连接
           conn = jedisPool.getResource();
           //随机生成一个value
           String identifier = UUID.randomUUID().toString();
           //锁名 key 值
           String lockKey = "lock:"+lockName;
           //设置超时时间,上锁后超时时间自动释放锁
           int lockExpire = (int)(timeout/1000);
           //获取缩的超时时间,超过这个时间放弃获取锁
           long end = System.currentTimeMillis()+acquireTimeout;
           while (System.currentTimeMillis()<end){
               // key 存在什么都不做返回0
               if(conn.setnx(lockKey,identifier) == 1){
                   //设置过期时间
                   conn.expire(lockKey,lockExpire);
                   //返回value用于释放锁时间确认
                   retIdentifier = identifier;
                   return  retIdentifier;
               }

               //返回-1 代表么有设置超时间,为key 设置一个超时时间
               if(conn.ttl(lockKey) == -1){
                    conn.expire(lockKey,lockExpire);
               }
               try {
                   Thread.sleep(10);
               }catch (InterruptedException e){
                    Thread.currentThread().interrupt();
               }

           }


       }catch (JedisException e){
           e.printStackTrace();
       }finally {
           if(conn != null){
//               conn.close();
           }
       }
       return retIdentifier;
    }

    /**
     * 释放锁
     * @param lockName
     * @param identifier
     * @return
     */
    public boolean releaseLock(String lockName,String identifier){
        Jedis conn = null;
        String lockKey = "lock:"+lockName;
        boolean retFlag = false;
        try {
             conn = jedisPool.getResource();
             while (true){
                 //监视lock, 准备开始事务
                 conn.watch(lockKey);
                 //通过前面返回的value判断是不是该锁,是则删除,释放锁
                 if(identifier.equals(conn.get(lockKey))){
                     //开启redis 事务删除锁
                     Transaction transaction = conn.multi();
                     transaction.del(lockKey);
                     List<Object> result = transaction.exec();
                     if(result == null){
                         continue;
                     }

                     retFlag = true;
                 }
                 conn.unwatch();
                 break;
             }
        }catch (JedisException e){
            e.printStackTrace();
        }finally {
            if(conn != null){
//                conn.close();
            }
        }

        return retFlag;
    }

}

Analog threads spike Service

package com.fs.fscloudadmin.redis;
/**
 * @author huyunqiang
 * @Date 2019/8/14 22:11
 */
public class RedisLockThread extends Thread{

    private RedisService redisService;

    public RedisLockThread(RedisService redisService){
        this.redisService = redisService;
    }

    @Override
    public void run() {
        // 获取锁和释放锁
        redisService.seckill();
    }


    public static void main(String[] args) {
        RedisService redisService = new RedisService();
        for(int i = 0;i<50;i++){
            RedisLockThread thread = new RedisLockThread(redisService);
            thread.start();
        }
    }

}
public void seckill(){
       //返回value, 供释放锁时候进行判断
       String identifier = lock.lockWithTimeOut("resourse",5000,1000);
       System.out.println(Thread.currentThread().getName()+"获得了锁");
       System.out.println(--n);
       //如果注释掉 释放锁一部分是异步进行的  加载比较慢
       lock.releaseLock("resourse",identifier);
   }

 

Was added to the annotation on annotation method can be used to customize the way the code above is encapsulated in a distributed lock requires

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RedisLock {

    String value() default "redis-lock";
}

@Aspect
@Component
public class  RedisLockHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockHandler.class);

    @Pointcut("@annotation(com.snowalker.annotation.RedisLock)")
    public void redisLock() {}

    @Around("@annotation(redisLock)")
    public void around(ProceedingJoinPoint joinPoint, RedisLock redisLock) {
        LOGGER.info("[开始]执行RedisLock环绕通知,获取Redis分布式锁开始");
        String lockName = redisLock.value();
        RedisDistributedLock redisDistributedLock = RedisDistributedLock.getInstance();
        if (redisDistributedLock.lock(lockName)) {
            try {
                LOGGER.info("获取Redis分布式锁[成功]");
                joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
            LOGGER.info("释放Redis分布式锁[成功]");
            redisDistributedLock.release(lockName);
        } else {
            LOGGER.error("获取Redis分布式锁[失败]");
        }
        LOGGER.error("[结束]执行RedisLock环绕通知");
    }
}
/**
 * @author huyunqiang
 * @date 2018-7-9
 * @desc redis分布式锁核心实现
 */
public class RedisDistributedLock implements DistributedLock {

    /**默认锁超时时间为10S*/
    private static final int EXPIRE_SECONDS = 50;
    private static final Logger log = LoggerFactory.getLogger(RedisDistributedLock.class);

    public RedisDistributedLock() {
    }

    private volatile static RedisDistributedLock redisDistributedLock;

    public static RedisDistributedLock getInstance() {
        if (redisDistributedLock == null) {
            synchronized (RedisDistributedLock.class) {
                redisDistributedLock = new RedisDistributedLock();
            }
        }
        return redisDistributedLock;
    }

    /**
     * 加锁
     *
     * @param lockName
     * @return 返回true表示加锁成功,执行业务逻辑,执行完毕需要主动释放锁,否则就需要等待锁超时重新争抢
     * 返回false标识加锁失败,阻塞并继续尝试获取锁
     */
    @Override
    public boolean lock(String lockName) {
        /**1.使用setNx开始加锁*/
        log.info("开始获取Redis分布式锁流程,lockName={},CurrentThreadName={}", lockName, Thread.currentThread().getName());
        long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("redis.lock.timeout", "5"));
        /**redis中锁的值为:当前时间+超时时间*/
        Long lockResult = RedisPoolUtil.setnx(lockName, String.valueOf(System.currentTimeMillis() + lockTimeout));
         // key 存在什么都不做返回0  等于1 获取到锁 设置超时时间
        if (lockResult != null && lockResult.intValue() == 1) {
            log.info("setNx获取分布式锁[成功],threadName={}", Thread.currentThread().getName());
            RedisPoolUtil.expire(lockName, EXPIRE_SECONDS);
            return true;
        } else {
            log.info("setNx获取分布式锁[失败],threadName={}", Thread.currentThread().getName());
//            return tryLock(lockName, lockTimeout);
            return false;
        }
    }

    private boolean tryLock(String lockName, long lockTimeout) {
        /**
         * 2.加锁失败后再次尝试
         * 2.1获取锁失败,继续判断,判断时间戳,看是否可以重置并获取到锁
         *    setNx结果小于当前时间,表明锁已过期,可以再次尝试加锁
         */
        String lockValueStr = RedisPoolUtil.get(lockName);
        Long lockValueATime = Long.parseLong(lockValueStr);
        log.info("lockValueATime为:" + lockValueATime);
        if (lockValueStr != null && lockValueATime < System.currentTimeMillis()) {

            /**2.2再次用当前时间戳getset--->将给定 key 的值设为 value,并返回 key 的旧值(old value)
             * 通过getset重设锁对应的值: 新的当前时间+超时时间,并返回旧的锁对应值
             */
            String getSetResult = RedisPoolUtil.getSet(lockName, String.valueOf(System.currentTimeMillis() + lockTimeout));
            log.info("lockValueBTime为:" + Long.parseLong(getSetResult));
            if (getSetResult == null || (getSetResult != null && StringUtils.equals(lockValueStr, getSetResult))) {
                /**
                 *2.3旧值判断,是否可以获取锁
                 *当key没有旧值时,即key不存在时,返回nil ->获取锁,设置锁过期时间
                 */
                log.info("获取Redis分布式锁[成功],lockName={},CurrentThreadName={}",
                        lockName, Thread.currentThread().getName());
                RedisPoolUtil.expire(lockName, EXPIRE_SECONDS);
                return true;
            } else {
                log.info("获取锁失败,lockName={},CurrentThreadName={}",
                        lockName, Thread.currentThread().getName());
                return false;
            }
        } else {
            /**3.锁未超时,获取锁失败*/
            log.info("当前锁未失效!!!!,竞争失败,继续持有之前的锁,lockName={},CurrentThreadName={}",
                    lockName, Thread.currentThread().getName());
            return false;
        }
    }

    /**
     * 解锁
     *
     * @param lockName
     */
    @Override
    public boolean release(String lockName) {
        Long result = RedisPoolUtil.del(lockName);
        if (result != null && result.intValue() == 1) {
            log.info("删除Redis分布式锁成功,锁已释放, key= :{}", lockName);
            return true;
        }
        log.info("删除Redis分布式锁失败,锁未释放, key= :{}", lockName);
        return false;
    }
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Guess you like

Origin blog.csdn.net/huyunqiang111/article/details/99694402