redis分布式锁实现

3 篇文章 0 订阅

        java中我们常用的锁有synchronized或者Lock,由于这些锁是线程锁,所以对同一个JVM进程内的多个线程有效。因为锁的本质 是内存中存放一个标记,记录获取锁的线程是谁,这个标记对每个线程都可见。但是目前的形式下,很多时候是多服务集群的,就会是多个jvm,各个jvm之间是不共享的,也就无法再这种情况下使用以上两种线程锁了。就使用到分布式锁了。

        常用的有三种解决方案:1.基于数据库实现 2.基于zookeeper的临时序列化节点实现 3.redis实现。其中数据库实现的锁如果在高并发的情况不合适,redis是现在常用的服务。是一个高性能的key-value数据库。很适合。现在很多应用也是用的redis实现分布式锁。

      分布式锁需要满足:多进程可见,互斥、可重入。

首先生成redis链接信息:

@Configuration
public class RedisConfigurer {

    @Value("${spring.redis.host}")
    String host;
    @Value("${spring.redis.port}")
    int port;
    @Value("${spring.redis.password}")
    String password;
    @Value("${spring.redis.database}")
    int database;

    @Bean("jedisPool")
    public JedisPool getJedisPool(){
        JedisPool jedisPool = new JedisPool(new GenericObjectPoolConfig(), host, port, Protocol.DEFAULT_TIMEOUT, password, database, null);
        return jedisPool;
    }
}

第一种redis分布式锁: 

     所有的进程同时使用一个redis锁,每个进程在获取到锁之后设置锁的超时时间。其他的进程在锁未过期时是无法获取到做的。

// 获取锁之后的超时时间(防止死锁)单位seconds
    private final static int timeOut = 100;

    private static JedisPool jedisPool = SpringContextUtils.getBean(JedisPool.class);
    private static Set<String> lockNameMap = new HashSet<>();

    /**
     * 第一种锁,多个进程使用同样的锁名称, 获取到锁之后设置超时时间
     * @param lockName 锁名称
     * @param value 锁值
     * @return
     */

    public static boolean tryLock(String lockName, String value){
        Jedis jedis = jedisPool.getResource();
        boolean flag = false;
        //获取锁
        try {
            long result = jedis.setnx(lockName, value);
            if (result == 1) {
                //获取成功,设置锁的超时时间
                long i = jedis.expire(lockName, timeOut);
                lockNameMap.add(lockName);
                System.out.println(String.format("获取到锁 %s --- %s, 超时设置返回:%s", lockName, value, i));
                flag = true;
            } else {
                System.out.println(String.format("未获取到锁 %s --- %s, 返回:%s", lockName, value, result));
            }
        } finally {
            close(jedis);
        }
        return  flag;
    }

    /**
     * 释放锁
     * @param lockName 所名称
     */
    public static void unLock(String... lockName){
        Jedis jedis = jedisPool.getResource();
        try {
            //删除锁
            long result = jedis.del(lockName);
            if (result != 0) {
                //删除成功
                lockNameMap.removeAll(Lists.newArrayList(lockName));
                System.out.println(String.format("删除锁成功:%s, 返回:%s", Arrays.toString(lockName), result));
            } else {
                System.out.println(String.format("删除锁失败:%s, 返回:%s", Arrays.toString(lockName), result));
            }
        }finally {
            close(jedis);
        }

    }
private static void close(Jedis jedis){
        if(jedis != null){
            jedis.close();
        }
    }

第一种锁的缺陷:

      缺陷一: 当一个服务获取到锁之后,还没有设置超时时间, 突然服务宕机,就会导致所有的服务都拿不到锁
      解决:必须原子性的同时操作  获取锁和设置锁超时间  操作。redis提供了  nx 与 ex连用的命令--set
      缺陷二:假设有A、B、C三个服务, A获取到锁之后设置了超时时间为 1min, 但是由于某些原因实际执行了 5mim 才去释放锁。在 1min 之后锁被释放, B获取了锁, 设置超时时间为 5mim。这时候 A执行完了,释放了B的锁。 C在这个时候获取锁也可以成功, 如果B 还执行完了C还没执行完了,B 就会释放了C 的锁,这就会导致后续一系列的问题。
      解决:每一个服务在加锁时都带上自己的 线程id  标识,释放锁时,只有 线程id 标识一样时才会释放锁。

第二种redis分布式锁:

     在加锁的同时设置超时时间,每一个进程在加锁的时带上自己的唯一标识。解锁时只有唯一标识一致的数据才能解锁。

   // 获取锁之后的超时时间(防止死锁)单位seconds
    private final static int timeOut = 100;

    private static JedisPool jedisPool = SpringContextUtils.getBean(JedisPool.class);
 
    /**
     * 第二种锁,多个进程使用同样的锁名称并且锁中包含自己的线程id, 获取到锁之后设置超时时间
     * @param lockName 锁名称
     * @param clientId 线程的唯一标识
     * @return
     */

    public static boolean trySecondLock(String lockName, String clientId){
        Jedis jedis = jedisPool.getResource();
        String result = jedis.set(lockName, clientId, "NX", "PX", timeOut);
        if("OK".equals(result)){
            System.out.println(String.format("获取到锁 %s --- %s, 返回:%s", lockName, clientId, result));
            return true;
        }else {
            System.out.println(String.format("没获取到锁 %s --- %s, 返回:%s", lockName, clientId, result));
        }
        return false;
    }

    /**
     * 释放锁
     * @param lockName 所名称
     * @param clientId 锁所属标识
     */
    public static void unSecondLock(String lockName, String clientId){
        Jedis jedis = jedisPool.getResource();
        try {
            //判断clientId是否一致
            String redisClientId = jedis.get(lockName);
            if(clientId.equals(redisClientId)){
                long result = jedis.del(lockName);
                if (result != 0) {
                    //删除成功
                    lockNameMap.removeAll(Lists.newArrayList(lockName));
                    System.out.println(String.format("删除锁成功:%s, clientId= %s, 返回:%s", lockName, clientId, result));
                } else {
                    System.out.println(String.format("删除锁失败:%s, clientId= %s, 返回:%s", lockName, clientId, result));
                }
            } else {
                System.out.println(String.format("删除锁失败:%s, clientId= %s, redis锁的标识:%s", lockName, clientId, redisClientId));
            }
        }finally {
            close(jedis);
        }

    }


    private static void close(Jedis jedis){
        if(jedis != null){
            jedis.close();
        }
    }
第三种锁:重入锁
    重入锁也叫做递归锁,指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。可重入锁可以避免因同一线程中多次获取锁而导致死锁发生。像synchronized就是一个重入锁,它是通过moniter函数记录当前线程信息来实现的。
  获取锁:首先判断锁是否存在,如果不存在则加锁,设置锁的次数是1和超时时间。如果是存在,则判断锁是否是自己的,是自己的就所次数加1和设置超时时间,否则不处理。
  释放锁:释放锁不能直接删除了,因为锁是可重入的,如果锁进入了多次,在内层直接删除锁, 导致外部的业务在没有锁的情况下执行,会有安全问题。因此必须获取锁时累计重入的次数,释放时则减去重入次数,如果减到0,则可以删除锁。
   重入锁在redis中是按照一个类似map的形式,key值是线程的唯一标识,value值是重入的错次数,也就是同一个线程加了多少次锁。

 

实现:

重入锁需要用到lua脚本来实现:

加锁脚本lock.lua

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';   --返回string结果
end;

if(redis.call('hexists', key, threadId) == 1) then   --根据threadId判断锁是否是自己加的
    local count = redis.call('hincrby', key, threadId, '1');   --是,则重入次数加1
    redis.call('expire', key, releaseTime);  --设置超时时间
    return tostring(count);  -- 返回string结果
end;
return '0';   --到这里表示已经加了锁,但是锁不是自己加的。

 释放锁脚本unlock.lua

local key = KEYS[1];   --第一个参数,锁的key
local threadId = ARGV[1];   -- 第2个参数,线程唯一标识

if (redis.call('hexists', key, threadId) == 0) then   -- 判断当前锁是否还是被自己持有
    return  '-1';   --不是自己的锁,返回
end;
local count = redis.call('hincrby', key, threadId, -1);   --到这里表示锁是自己的,重入次数减1表示,释放一次锁

if (count == 0) then   --判断锁的重入次数是否为0
    redis.call('DEL', key);  --为0表示已经全部释放了锁,所以需要删掉锁值。
end;
return tostring(count);   --返回剩余需要释放的次数

代码实现: 

private void thirdLock(){
        String lockName = "REDIS:TEST:LOCK:lock";

        String clientId = "1";
        String clientId2 = "2";

        //获取锁测试
        DefaultRedisScript<String> LOCK_SCRIPT;
        DefaultRedisScript<String> UNLOCK_SCRIPT;
        // 加载释放锁的脚本
        LOCK_SCRIPT = new DefaultRedisScript<>();
        LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
        LOCK_SCRIPT.setResultType(String.class);

        // 加载释放锁的脚本
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
        UNLOCK_SCRIPT.setResultType(String.class);

        for(int i = 0; i < 5; i++) {
            JedisLockUtil.tryThirdLock(stringRedisTemplate, LOCK_SCRIPT, lockName, clientId);
        }

        for(int i = 0; i < 5; i++) {
            JedisLockUtil.unlock(stringRedisTemplate, UNLOCK_SCRIPT, lockName, clientId);
        }
    }

   /**
     * 第三种锁,重入锁
     */
    public static void tryThirdLock(StringRedisTemplate redisTemplate, DefaultRedisScript<String> LOCK_SCRIPT, String lockName, String clientId){

        try {
            // 执行脚本
            String result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockName), clientId, "100");

            // 判断结果
            if(!"0".equals(result)) {
                System.out.println(String.format("获取到锁 %s,clientId = %s, 返回:%s", lockName, clientId, result));
            }else{
                System.out.println(String.format("未获取到锁 %s, clientId = %s, 返回:%s", lockName, clientId, result));
            }
        } catch (Exception e){
            e.printStackTrace();
        }
    }
    /**
     * 释放锁
     * @param lockName 锁名称
     * @param clientId 解锁标识
     */
    public static void unlock(StringRedisTemplate redisTemplate, DefaultRedisScript<String> UNLOCK_SCRIPT, String lockName,String clientId) {
        try {
            // 执行脚本
            String result = redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(lockName), clientId);
            if(!"-1".equals(result)) {
                System.out.println(String.format("释放锁 %s, clientId = %s, 返回:%s", lockName, clientId, result));
            } else {
                System.out.println(String.format("释放锁失败 %s, clientId = %s, 返回:%s", lockName, clientId, result));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

测试结果如图:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值