开发者社区> 加多> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

使用Redis单实例实现分布式锁

简介:
+关注继续查看

一、前言

在同一个jvm进程中时,可以使用JUC提供的一些锁来解决多个线程竞争同一个共享资源时候的线程安全问题,但是当多个不同机器上的不同jvm进程共同竞争同一个共享资源时候,juc包的锁就无能无力了,这时候就需要分布式锁了。常见的有使用zk的最小版本,redis的set函数,数据库锁来实现,本节我们谈谈Redis单实例情况下使用set函数来实现分布式锁。

二、使用Redis单实例实现分布式锁

首先我们来具体看代码:

package com.jiaduo.DistributedLock;

import java.util.Collections;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class DistributedLock {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;

    private static void validParam(JedisPool jedisPool, String lockKey, String requestId, int expireTime) {
        if (null == jedisPool) {
            throw new IllegalArgumentException("jedisPool obj is null");
        }

        if (null == lockKey || "".equals(lockKey)) {
            throw new IllegalArgumentException("lock key  is blank");
        }

        if (null == requestId || "".equals(requestId)) {
            throw new IllegalArgumentException("requestId is blank");
        }

        if (expireTime < 0) {
            throw new IllegalArgumentException("expireTime is not allowed less zero");
        }
    }

    /**
     * 
     * @param jedis
     * @param lockKey
     * @param requestId
     * @param expireTime
     * @return
     */
    public static boolean tryLock(JedisPool jedisPool, String lockKey, String requestId, int expireTime) {

        validParam(jedisPool, lockKey, requestId, expireTime);

        Jedis jedis = null;
        try {

            jedis = jedisPool.getResource();
            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
        } catch (Exception e) {
            throw e;
        } finally {
            if (null != jedis) {
                jedis.close();
            }
        }

        return false;
    }

    /**
     * 
     * @param jedis
     * @param lockKey
     * @param requestId
     * @param expireTime
     */
    public static void lock(JedisPool jedisPool, String lockKey, String requestId, int expireTime) {

        validParam(jedisPool, lockKey, requestId, expireTime);

        while (true) {
            if (tryLock(jedisPool, lockKey, requestId, expireTime)) {
                return;
            }
        }
    }

    /**
     * 
     * @param jedis
     * @param lockKey
     * @param requestId
     * @return
     */
    public static boolean unLock(JedisPool jedisPool, String lockKey, String requestId) {

        validParam(jedisPool, lockKey, requestId, 1);

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        Jedis jedis = null;
        try {

            jedis = jedisPool.getResource();
            Object result = jedis.eval(script, Collections.singletonList(lockKey),
                    Collections.singletonList(requestId));

            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }

        } catch (Exception e) {
            throw e;
        } finally {
            if (null != jedis) {
                jedis.close();
            }
        }

        return false;

    }

}

首先Redis的 public String set(final String key, final String value, final String nxxx, final String expx,

  final int time)方法参数说明:
  • 其中前面两个是key,value值;
  • nxxx为模式,这里我们设置为NX,意思是说如果key不存在则插入该key对应的value并返回OK,否者什么都不做返回null;
  • 参数expx这里我们设置为PX,意思是设置key的过期时间为time 毫秒

通过tryLock方法尝试获取锁,内部是具体调用Redis的set方法,多个线程同时调用tryLock时候会同时调用set方法,但是set方法本身是保证原子性的,对应同一个key来说,多个线程调用set方法时候只有一个线程返回OK,其它线程因为key已经存在会返回null,所以返回OK的线程就相当与获取到了锁,其它返回null的线程则相当于获取锁失败。

另外这里我们要保证value(requestId)值唯一是为了保证只有获取到锁的线程才能释放锁,这个下面释放锁时候会讲解。

通过lock 方法让使用tryLock获取锁失败的线程本地自旋转重试获取锁,这类似JUC里面的CAS。

Redis有一个叫做eval的函数,支持Lua脚本执行,并且能够保证脚本执行的原子性,也就是在执行脚本期间,其它执行redis命令的线程都会被阻塞。这里解锁时候使用下面脚本:

if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

其中keys[1]为unLock方法传递的key,argv[1]为unLock方法传递的requestId;脚本redis.call('get', KEYS[1])的作用是获取key对应的value值,这里会返回通过Lock方法传递的requetId, 然后看当前传递的RequestId是否等于key对应的值,等于则说明当前要释放锁的线程就是获取锁的线程,则继续执行redis.call('del', KEYS[1])脚本,删除key对应的值。

三、总结

本文使用redis单实例结合redis的set方法和eval函数实现了一个简单的分布式锁,但是这个实现还是明显有问题的。虽然使用set方法设置了超时时间,以避免线程获取到锁后redis挂了后锁没有被释放的情况,但是超时时间设置为多少合适那?如果设置太小,可能会存在线程获取锁后执行业务逻辑时间大于锁超时时间,那么就会存在逻辑还没执行完,锁已经因为超时自动释放了,而其他线程可能获取到锁,那么之前获取锁的线程的业务逻辑的执行就没有保证原子性。

另外还有一个问题是Lock方法里面是自旋调用tryLock进行重试,这就会导致像JUC中的AtomicLong一样,在高并发下多个线程竞争同一个资源时候造成大量线程占用cpu进行重试操作。这时候其实可以随机生成一个等待时间,等时间到后在进行重试,以减少潜在的同时对一个资源进行竞争的并发量。

最后

欢迎关注微信公众号:‘技术原始积累’ 获取更多技术干货__

image.png

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
浅谈Redis实现分布式锁
浅谈Redis实现分布式锁
0 0
redis实现分布式锁
redis实现分布式锁
0 0
【Redis】Redis实现分布式锁
分布式锁无论在开发中还是面试里都是老八股了,本篇文章整理江湖上常见的Redis分布式锁解决方案
0 0
基于Redis实现分布式锁
基于Redis实现分布式锁
0 0
你是不是用redis来实现分布式锁?这些坑不得不防
在工作中,我们或多或少都用到过锁,今天我们就来讨论分布式场景下,我们可以通过哪种方式来解决锁的问题,这也是我在面试中经常遇到的一个问题,搞定他,非常重要。
0 0
Redis实现分布式锁
Redis实现分布式锁
0 0
基于 Redis SETNX 实现分布式锁手记
基于 Redis SETNX 实现分布式锁手记
0 0
Redis如何实现分布式锁?
一篇文章学会Redis实现分布式锁的原理!
0 0
用redis实现分布式锁时容易踩的5个坑
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 近有不少小伙伴投入短视频赛道,也出现不少第三方数据商,为大家提供抖音爬虫数据。 小伙伴们有没有好奇过,这些数据是如何获取的,普通技术小白能否也拥有自己的抖音爬虫呢? 本文会全面解密抖音爬虫的幕后原理,不需要任何编程知识,还请耐心阅读。
286 0
+关注
加多
加多,目前在阿里巴巴淘宝技术部-基础平台部-移动基础服务组工作,热衷并发编程,开源框架,架构设计。
文章
问答
文章排行榜
最热
最新
相关电子书
更多
阿里云Redis容灾体系介绍--夏周
立即下载
阿里云Redis服务助力游戏行业发展--王义成
立即下载
国内开发者与Redis开源社区的发展
立即下载