zoukankan      html  css  js  c++  java
  • 3种Redis分布式锁的对比

    我们通常使用的synchronized或者Lock都是线程锁,对同一个JVM进程内的多个线程有效。因为锁的本质 是内存中存放一个标记,记录获取锁的线程是谁,这个标记对每个线程都可见。然而我们启动的多个订单服务,就是多个JVM,内存中的锁显然是不共享的,每个JVM进程都有自己的 锁,自然无法保证线程的互斥了,这个时候我们就需要使用到分布式锁了。常用的有三种解决方案:1.基于数据库实现 2.基于zookeeper的临时序列化节点实现 3.redis实现。本文我们介绍的就是redis的实现方式。
      实现分布式锁要满足3点:多进程可见,互斥,可重入。

    1) 多进程可见

     redis本身就是基于JVM之外的,因此满足多进程可见的要求。

    2) 互斥

     即同一时间只能有一个进程获取锁标记,我们可以通过redis的setnx实现,只有第一次执行的才会成功并返回1,其它情况返回0。

      

     释放锁
     释放锁其实只需要把锁的key删除即可,使用del xxx指令。不过,如果在我们执行del之前,服务突然宕机,那么锁就永远无法删除了。所以我们可以通过setex 命令设置过期时间即可。

    复制代码
    import java.util.UUID;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    /**
     * 第一种分布式锁
     */
    @Component
    public class RedisService {
    
    private final Logger log = LoggerFactory.getLogger(this.getClass());
        
        @Autowired
        JedisPool jedisPool;
         
        // 获取锁之前的超时时间(获取锁的等待重试时间)
        private long acquireTimeout = 5000;
        // 获取锁之后的超时时间(防止死锁)
        private int timeOut = 10000;
        
        /**
         * 获取分布式锁
         * @return 锁标识
         */
        public boolean getRedisLock(String lockName,String val) {
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                // 1.计算获取锁的时间
                Long endTime = System.currentTimeMillis() + acquireTimeout;
                // 2.尝试获取锁
                while (System.currentTimeMillis() < endTime) {
                    // 3. 获取锁成功就设置过期时间
                    if (jedis.setnx(lockName, val) == 1) {
                        jedis.expire(lockName, timeOut/1000);
                        return true;
                    }
                }
            } catch (Exception e) {
                log.error(e.getMessage());
            } finally {
                returnResource(jedis);
            }
            return false;
        }
        /**
         * 释放分布式锁
         * @param lockName 锁名称
         */
        public void unRedisLock(String lockName) {
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                // 释放锁
                jedis.del(lockName);
            } catch (Exception e) {
                log.error(e.getMessage());
            } finally {
                returnResource(jedis);
            }
        }
    // ===============================================      
            public String get(String key) {
            Jedis jedis = null;
            String value = null;
            try {
                jedis = jedisPool.getResource();
                value = jedis.get(key);
                log.info(value);
            } catch (Exception e) {
                log.error(e.getMessage());
            } finally {
                returnResource(jedis);
            }
            return value;
        }    
        
        public void set(String key, String value) {
            Jedis jedis = null;
            try {
                jedis = jedisPool.getResource();
                jedis.set(key, value);
            } catch (Exception e) {
                log.error(e.getMessage());
            } finally {
                returnResource(jedis);
            }
        }
        /**
         * 关闭连接
         */
        public void returnResource(Jedis jedis) {
            try {
                if(jedis!=null) jedis.close();
            } catch (Exception e) {
            }
        }
    }
    复制代码

    上面的分布式锁实现了,但是这时候还可能出现另外2个问题:
     一:获取锁时
      setnx获取锁成功了,还没来得及setex服务就宕机了,由于这种非原子性的操作,死锁又发生了。其实redis提供了 nx 与 ex连用的命令。

      
     二:释放锁时
      1. 3个进程:A和B和C,在执行任务,并争抢锁,此时A获取了锁,并设置自动过期时间为10s 
      2. A开始执行业务,因为某种原因,业务阻塞,耗时超过了10秒,此时锁自动释放了 
      3. B恰好此时开始尝试获取锁,因为锁已经自动释放,成功获取锁 
      4. A此时业务执行完毕,执行释放锁逻辑(删除key),于是B的锁被释放了,而B其实还在执行业务 
      5. 此时进程C尝试获取锁,也成功了,因为A把B的锁删除了。
      问题出现了:B和C同时获取了锁,违反了互斥性!如何解决这个问题呢?我们应该在删除锁之前,判断这个锁是否是自己设置的锁,如果不是(例如自己 的锁已经超时释放),那么就不要删除了。所以我们可以在set 锁时,存入当前线程的唯一标识!删除锁前,判断下里面的值是不是与自己标识释放一 致,如果不一致,说明不是自己的锁,就不要删除了。

    复制代码
    /**
     * 第二种分布式锁
     */
    public class RedisTool {
    
        private static final String LOCK_SUCCESS = "OK";
        
        private static final Long RELEASE_SUCCESS = 1L;
    
        /**
         * 尝试获取分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @param expireTime 超期时间
         * @return 是否获取成功
         */
        public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
            String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        }
    
        /**
         * 释放分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @return 是否释放成功
         */
        public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
            if (jedis.get(lockKey).equals(requestId)) {
                System.out.println("释放锁..." + Thread.currentThread().getName() + ",identifierValue:" + requestId);
                jedis.del(lockKey);
                return true;
            }
            return false;
        }
    }
    复制代码

       按照上面方式实现分布式锁之后,就可以轻松解决大部分问题了。网上很多博客也都是这么实现的,但是仍然有些场景是不满足的,例如一个方法获取到锁之后,可能在方法内调这个方法此时就获取不到锁了。这个时候我们就需要把锁改进成可重入式锁了。

      

    3) 重入锁

      也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。换一种说法:同一个线程再次进入同步代码时,可以使用自己已获取到的锁。可重入锁可以避免因同一线程中多次获取锁而导致死锁发生。像synchronized就是一个重入锁,它是通过moniter函数记录当前线程信息来实现的。实现可重入锁需要考虑两点:
       获取锁:首先尝试获取锁,如果获取失败,判断这个锁是否是自己的,如果是则允许再次获取, 而且必须记录重复获取锁的次数。
       释放锁:释放锁不能直接删除了,因为锁是可重入的,如果锁进入了多次,在内层直接删除锁, 导致外部的业务在没有锁的情况下执行,会有安全问题。因此必须获取锁时累计重入的次数,释放时则减去重入次数,如果减到0,则可以删除锁。

    复制代码
    下面我们假设锁的key为“ lock ”,hashKey是当前线程的id:“ threadId ”,锁自动释放时间假设为20
    获取锁的步骤:
        1、判断lock是否存在 EXISTS lock 
            2、不存在,则自己获取锁,记录重入层数为1.
            2、存在,说明有人获取锁了,下面判断是不是自己的锁,即判断当前线程id作为hashKey是否存在:HEXISTS lock threadId 
                3、不存在,说明锁已经有了,且不是自己获取的,锁获取失败.
                3、存在,说明是自己获取的锁,重入次数+1: HINCRBY lock threadId 1 ,最后更新锁自动释放时间, EXPIRE lock 20
            
    释放锁的步骤:
        1、判断当前线程id作为hashKey是否存在: HEXISTS lock threadId 
            2、不存在,说明锁已经失效,不用管了 
            2、存在,说明锁还在,重入次数减1: HINCRBY lock threadId -1 ,
              3、获取新的重入次数,判断重入次数是否为0,为0说明锁全部释放,删除key: DEL lock
    复制代码

    因此,存储在锁中的信息就必须包含:key、线程标识、重入次数。不能再使用简单的key-value结构, 这里推荐使用hash结构。
    获取锁的脚本(注释删掉,不然运行报错)

    复制代码
    local key = KEYS[1]; -- 第1个参数,锁的key
    local threadId = ARGV[1]; -- 第2个参数,线程唯一标识
    local releaseTime = ARGV[2]; -- 第3个参数,锁的自动释放时间
    
    if(redis.call('exists', key) == 0) then -- 判断锁是否已存在
        redis.call('hset', key, threadId, '1'); -- 不存在, 则获取锁
        redis.call('expire', key, releaseTime); -- 设置有效期
        return 1; -- 返回结果
    end;
    
    if(redis.call('hexists', key, threadId) == 1) then -- 锁已经存在,判断threadId是否是自己    
        redis.call('hincrby', key, threadId, '1'); -- 如果是自己,则重入次数+1
        redis.call('expire', key, releaseTime); -- 设置有效期
        return 1; -- 返回结果
    end;
    return 0; -- 代码走到这里,说明获取锁的不是自己,获取锁失败
    复制代码

    释放锁的脚本(注释删掉,不然运行报错)

    复制代码
    local key = KEYS[1]; -- 第1个参数,锁的key
    local threadId = ARGV[1]; -- 第2个参数,线程唯一标识
    
    if (redis.call('HEXISTS', key, threadId) == 0) then -- 判断当前锁是否还是被自己持有
        return nil; -- 如果已经不是自己,则直接返回
    end;
    local count = redis.call('HINCRBY', key, threadId, -1); -- 是自己的锁,则重入次数-1
    
    if (count == 0) then -- 判断是否重入次数是否已经为0
        redis.call('DEL', key); -- 等于0说明可以释放锁,直接删除
        return nil;    
    end;
    复制代码

     完整代码

    复制代码
    import java.util.Collections;
    import java.util.UUID;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.core.script.DefaultRedisScript;
    import org.springframework.scripting.support.ResourceScriptSource;
    
    /**
     * Redis可重入锁
     */
    public class RedisLock {
    
        private static final StringRedisTemplate redisTemplate = SpringUtil.getBean(StringRedisTemplate.class);
        private static final DefaultRedisScript<Long> LOCK_SCRIPT;
        private static final DefaultRedisScript<Object> UNLOCK_SCRIPT;
        static {
            // 加载释放锁的脚本
            LOCK_SCRIPT = new DefaultRedisScript<>();
            LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
            LOCK_SCRIPT.setResultType(Long.class);
    
            // 加载释放锁的脚本
            UNLOCK_SCRIPT = new DefaultRedisScript<>();
            UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
        }
        /**
         * 获取锁
         * @param lockName 锁名称
         * @param releaseTime 超时时间(单位:秒)
         * @return key 解锁标识
         */
        public static String tryLock(String lockName,String releaseTime) {
            // 存入的线程信息的前缀,防止与其它JVM中线程信息冲突
            String key = UUID.randomUUID().toString();
    
            // 执行脚本
            Long result = redisTemplate.execute(
                    LOCK_SCRIPT,
                    Collections.singletonList(lockName),
                    key + Thread.currentThread().getId(), releaseTime);
    
            // 判断结果
            if(result != null && result.intValue() == 1) {
                return key;
            }else {
                return null;
            }
        }
        /**
         * 释放锁
         * @param lockName 锁名称
         * @param key 解锁标识
         */
        public static void unlock(String lockName,String key) {
            // 执行脚本
            redisTemplate.execute(
                    UNLOCK_SCRIPT,
                    Collections.singletonList(lockName),
                    key + Thread.currentThread().getId(), null);
        }
    }
    复制代码

     至此,一个比较完善的redis锁就开发完成了。

  • 相关阅读:
    【leetcode】236. 二叉树的最近公共祖先
    【leetcode】230. 二叉搜索树中第K小的元素
    【leetcode】309. 最佳买卖股票时机含冷冻期
    【leetcode】306. 累加数
    【leetcode】304. 二维区域和检索
    spring-framework源码编译及导入
    Java8-函数式接口理解及测试
    Mac编译RocketMQ 4.1.0
    首记
    JS表单验证
  • 原文地址:https://www.cnblogs.com/liliuguang/p/13401838.html
Copyright © 2011-2022 走看看