Redis实现分布式锁(入门)

单机版redis解决方案(redis集群还在学)
此博客解决方案适用于绝大部分业务场景,不能容忍任何一点的race condition(如钱相关的业务)并不适用
原理分析参考另一篇博客

什么分布式锁?
  • 本地锁:在多个线程中,保证只有一个线程执行(线程安全的问题)

  • 分布锁:在分布式中,保证只有一个jvm执行(多个jvm线程安全问题)
    如果我们服务器是集群的时候,定时任务可能会重复执行 可以采用分布式锁解决

分布式锁解决方案:
  • 基于数据库方式实现
  • 基于Zk方式实现 采用临时节点+事件通知
  • 基于Redis方式实现 setnx 方式
解决分布式锁核心思路:
  • 获取锁
    多个不同的jvm 同时创建一个相同的标记(全局唯一的) 只要谁能够创建成功谁就能够获取锁
  • 释放锁
    释放该全局唯一的标记,其他的jvm重新进入到获取锁资源。
  • 超时锁(没有获取锁、已经获取锁)
    等待获取锁的超时时间
    已经获取到锁 锁的有效期 5s
分析:基于Redis实现分布式锁思路
  • 获取锁
    多个不同的jvm 同时创建一个相同的标记使用Setnx命令,因为Rediskey必须保证是唯一的,只要谁能够创建成功谁就能够获取锁
    Set命令的时候:如果key不存在则创建,如果key已经存在则修改原值;
    SetNx命令: 如果key不存在则创建 返回1,如果已经存在则不执行任何操作返回0
    1 不存在创建成功 0 已经存在 不执行任何操作。

  • 释放锁
    对我们的redis的key设置一个有效期(或者是主动删除该key)可以灵活的自动的释放该全局唯一的标记,其他的jvm重新进入到获取锁资源。

  • 超时锁(没有获取锁、已经获取锁)
    等待获取锁的超时时间
    已经获取到锁 锁的有效期 5s

分析基于Zk实现分布式锁思路
  • 获取锁
    多个不同的jvm在zk集群上创建一个相同的全局唯一的临时路径,只要谁能够创建成功谁就能够获取到锁。
    分析:临时节点对我们节点设置有效期

  • 释放锁
    人为主动删除该节点或者使用Session有效期

  • 超时锁(没有获取锁、已经获取锁)
    等待获取锁的超时时间
    已经获取到锁 锁的有效期 5s

代码实现
  • maven依赖
   <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>
    <!--用于判断字符串是否为空,测试类用到了-->
    <dependency>
         <groupId>org.apache.commons</groupId>
         <artifactId>commons-lang3</artifactId>
         <version>3.9</version>
    </dependency>
  • redis工具类配置redis
public class RedisUtil {
    //protected static Logger logger = Logger.getLogger(RedisUtil.class);
    
    private static String IP = "你自己的redis IP";

    //Redis的端口号
    private static int PORT = 6379;

    //可用连接实例的最大数目,默认值为8;
    //如果赋值为-1,则表示不限制;如果pool已经分配了maxActive个jedis实例,则此时pool的状态为exhausted(耗尽)。
    private static int MAX_ACTIVE = 100;

    //控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8。
    private static int MAX_IDLE = 20;

    //等待可用连接的最大时间,单位毫秒,默认值为-1,表示永不超时。如果超过等待时间,则直接抛出JedisConnectionException;
    private static int MAX_WAIT = 3000;

    private static int TIMEOUT = 3000;

    //在borrow一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
    private static boolean TEST_ON_BORROW = true;

    //在return给pool时,是否提前进行validate操作;
    private static boolean TEST_ON_RETURN = true;

    private static JedisPool jedisPool = null;

    /**
     * redis过期时间,以秒为单位
     */
    public final static int EXRP_HOUR = 60 * 60; //一小时
    public final static int EXRP_DAY = 60 * 60 * 24; //一天
    public final static int EXRP_MONTH = 60 * 60 * 24 * 30; //一个月

    /**
     * 初始化Redis连接池
     */
    private static void initialPool() {
        try {
            JedisPoolConfig config = new JedisPoolConfig();
            config.setMaxTotal(MAX_ACTIVE);
            config.setMaxIdle(MAX_IDLE);
            config.setMaxWaitMillis(MAX_WAIT);
            config.setTestOnBorrow(TEST_ON_BORROW);

            jedisPool = new JedisPool(config, IP, PORT, TIMEOUT);
          //有密码用下面这种构造方法
          // jedisPool = new JedisPool(config, IP, PORT, TIMEOUT, "123456");
        } catch (Exception e) {
            //logger.error("First create JedisPool error : "+e);
            e.getMessage();
        }
    }

    /**
     * 在多线程环境同步初始化
     */
    private static synchronized void poolInit() {
        if (jedisPool == null) {
            initialPool();
        }
    }

    /**
     * 同步获取Jedis实例
     *
     * @return Jedis
     */
    public synchronized static Jedis getJedis() {
        if (jedisPool == null) {
            poolInit();
        }
        Jedis jedis = null;
        try {
            if (jedisPool != null) {
                jedis = jedisPool.getResource();
            }
        } catch (Exception e) {
            e.getMessage();
            // logger.error("Get jedis error : "+e);
        }
        return jedis;
    }

    /**
     * 释放jedis资源
     *
     * @param jedis
     */
    public static void returnResource(final Jedis jedis) {
        if (jedis != null && jedisPool != null) {
            jedisPool.returnResource(jedis);
        }
    }

    public static Long sadd(String key, String... members) {
        Jedis jedis = null;
        Long res = null;
        try {
            jedis = getJedis();
            res = jedis.sadd(key, members);
        } catch (Exception e) {
            //logger.error("sadd  error : "+e);
            e.getMessage();
        }
        return res;
    }
}
  • 分布式锁实现工具类
public class RedisLock {

    private static int lockSuccss = 1;

    /**
     * @param lockKey      在Redis中创建的key值
     * @param notLockTime  尝试获取锁超时时间
     * @return 返回lock成功值
     */
    public String getLock(String lockKey,int notLockTime, int timeOut){
        //获取Redis连接
        Jedis jedis=RedisUtil.getJedis();
        //计算超时时间
        long endTime = System.currentTimeMillis() + notLockTime;
        try {
            //当前系统时间小于endTime说明获取锁没有超时
            while (System.currentTimeMillis()<endTime){
                String lockValue = UUID.randomUUID().toString();
                // 当多个不同的jvm同时创建一个相同的rediskey 只要谁能够创建成功谁就能够获取锁
                if(jedis.setnx(lockKey,lockValue)==1){
                    jedis.expire(lockKey,timeOut/1000);
                    return lockValue;
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        finally {
            if(jedis!=null){
                jedis.close();
            }
        }
        return null;
    }

    /**
     * 释放锁
     * @return
     */
    public boolean unLock(String lockKey,String lockValue){
        //获取Redis连接
        Jedis jedis=RedisUtil.getJedis();
        try {
            // 判断获取锁的时候保证自己删除自己(防止空指针将lockValue写前面,因为redis可能获取到空值)
            if(lockValue.equals(jedis.get(lockKey))){
                return jedis.del(lockKey)>0 ? true:false;
            }
        }
        catch (Exception e){
            e.printStackTrace();
        }finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return false;
    }
}

  • 测试类
  public class TestService {

    private static final String LOCKKEY = "lock";

    public static void service() {
        // 1.获取锁
        RedisLock mayiktRedisLock = new RedisLock();
        String lockValue = mayiktRedisLock.getLock(LOCKKEY, 5000, 5000);
        if (StringUtils.isEmpty(lockValue)) {
            System.out.println(Thread.currentThread().getName() + ",获取锁失败了");
            return;
        }
        // 执行我们的业务逻辑
        System.out.println(Thread.currentThread().getName() + ",获取锁成功:lockValue:" + lockValue);

        // 3.释放锁(设置了失效时间,不释放也不会出现死锁)
        mayiktRedisLock.unLock(LOCKKEY, lockValue);
    }

    public static void main(String[] args) {
        service();
    }

    /***
     *
     * 尝试获取锁为什么次数限制?
     * 如果我们业务逻辑5s 内没有执行完毕呢?
     *
     * 分场景:
     * 1.锁的超时时间根据业务场景来预估
     * 2.可以自己延迟锁的时间
     * 3.在提交事务的时候检查锁是否已经超时 如果已经超时则回滚(手动回滚)否则提交。
     *
     * 仅限于单机版本
     */
}

From 蚂蚁课堂

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容