redis 分布式锁 看门狗_Redis分布式锁的实现

本文介绍了分布式锁的重要性和应用场景,并详细讲解了基于Redis的分布式锁实现,包括使用Lua脚本来保证原子性,以及锁的过期和重试机制。重点在于理解Redis的setnx命令和Lua脚本在分布式锁中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在高可用系统中, 加锁已经是常态, 分布式锁也是面试的热点. 

既然如此重要, 那就必须熟练的掌握对吧

一. 什么是分布式锁

  1. 问题描述

    在分布式环境下, 我们常常需要解决的是, 多台机器对同个资源的争抢问题

例如: 1、库存超卖 2、防止用户重复下单 3、MQ消息去重 4、订单操作变更  等等... 这些情况需要我们对某个资源进行串行化操作

  • 单机情况下, 我们可以简单的通过关键字 synchronize、或者 读写锁 的操作使得流程串行化
  • 多节点、多进程. 就得使用分布式锁了
  1. 具体流程图如下

4fe70496c99fb72dba521b5e7ae59ec5.png

  1. 分布式锁的状态

    获取锁: 各个客户端通过竞争获取锁到锁, 才能对共享的资源进行操作

    占有锁: 操作资源的时间段, 客户端一直会持有这个锁, 以保证别的节点获取不到锁

    阻塞: 当其他节点获取不到锁的时候, 就会进入阻塞状态, 无法对这个资源操作

    释放锁: 资源操作完毕后, 持有锁的客户端, 将会释放锁资源. 让其他节点可以获取锁

  2. 分布式锁特点

  • 互斥性: 任意时刻只能有一个客户端持有锁
  • 高可用, 容错性: 锁服务应该做集群高可用, 保证所有客户端能正常的获取释放锁
  • 避免死锁: 锁机制不能无限时间锁住资源, 应该有一定的机制自动释放锁 (正常释放或者超时释放)
  • 解铃还须系铃人: 加锁解锁要保证同一个客户端所为

二. 分布式锁的实现

分布式锁实现有很多种产品可以实现, 其中比较主流的有

  • 基于zookeeper时节点的分布式锁
  • 基于Redis的分布式锁 (基于内存, 性能突出)
  • DB数据库 实现  (性能较差, 但是原子性可靠性较好)

其中较为大家熟知的是用Redis, 我相信大家也是用Redis去做的对吧, 所以本文也是介绍Redis去实现

1. 获取锁 tryLock

  • Redis2.6.12版本之前,使用Lua脚本保证原子性

    由于 2.6.12 版本之前没有 setnx 命令, 也即是 1.查看是否有 2.设置值 是两步操作, 如果使用代码去执行的话, 将会是两个指令, 这不满足操作的原子性.

    而使用 jedis.eval(script, keys, argv), 我们可以传入 Lua 脚本来保证两个操作的原子性, 因为Lua脚本再执行的时候, 是可以保证所有Lua脚本全部执行完毕, 才继续执行其他指令的.注意: 参数 key 是LIst  他的第一位对应 Lua 脚本中的 KEYS[1] , argv同理

  • 当然你也可以利用这个特性, 编写一段具有事物特性的Redis指令块, 并不一定只拿来做锁

    (关于 Lua 脚本脚本语言, 本文不会深入讲解, 感兴趣的小伙伴可以上菜鸟教程看看....)

  • 以下是我简单写的一段模仿SETNX 的LUA代码, 并且可以直接设置过期时间 (代码写得有些仓促, 有些许地方还需要优化, 大家将就看看)

-- 如果不存在值, 则设置值并且设置过期时间 返回1 , 存在值则返回0if redis.call('EXISTS',KEYS[1]) == 0 then     redis.call('SET',KEYS[1],ARGV[1])     redis.call('EXPIRE',KEYS[1],ARGV[2])     return 1else    return 0end
  • 客户端执行
@Testpublic void test() throws InterruptedException {    Jedis jedis = jedisPool.getResource();    for(int i = 0 ;i<=10; i++){        System.out.println("此时的key" + jedis.get("REDIS_TEST_KEY"));        Long eval = (Long) jedis.eval(LUA, Arrays.asList("REDIS_TEST_KEY"), Arrays.asList("value","1")); // 设置锁的值为: value, 过期时间为1s        if (0l == eval){            System.out.println("获取锁失败!");        }else {            System.out.println("获取锁成功" + jedis.get("REDIS_TEST_KEY"));        }        Thread.sleep(500l);    }}控制台打印此时的keynull获取锁成功value此时的keyvalue获取锁失败!此时的keynull获取锁成功value此时的keyvalue获取锁失败!此时的keynull获取锁成功value此时的keyvalue
  • 增加重试机制
@Testpublic void testTryLock() throws InterruptedException {    for(int i = 0 ;i<=10; i++){        if (tryLock("REDIS_TEST_KEY",1000l,0l,3)){            System.out.println("取锁成功");        }else{            System.out.println("取锁失败");        }    }}/** * @param key       解锁 key * @param weitTime  最多等待时间(单位:毫秒) * @param leaseTime 上锁后自动释放锁时间(单位:毫秒) */public Boolean tryLock(final String key,                       final Long weitTime,                       final Long nowTime,                       final int leaseTime){    Long DEF_WEIT = 500L; //默认每次叠加0.5s的等待时间    //尝试去获取设置锁标志    Jedis jedis = jedisPool.getResource();    Long lock = (Long) jedis.eval(LUA, Arrays.asList(key), Arrays.asList("value",String.valueOf(leaseTime)));    // Redis2.6.12 之后也可以直接使用SETNX 进行设值    //long lock = jedis.setnx(key, "value");    //if (01 == lock){    //    jedis.expire(key, leaseTime);    //}    jedis.close(); //释放资源    if (0l == lock){        if (weitTime >= nowTime) { //如果还未超过等待时间            //获取锁失败            try {                Thread.sleep(DEF_WEIT);                LOG.info("【加锁】等待重试:{}",nowTime);            } catch (InterruptedException e) {                LOG.warn("【加锁】【等待时间】等待失效,key={}", key);            }            return tryLock(key,weitTime, nowTime + DEF_WEIT, leaseTime);        }        return Boolean.FALSE;    }else {        return Boolean.TRUE;    }}控制台打印取锁成功[2020-09-05 23:58:37.411] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:0[2020-09-05 23:58:37.928] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:500[2020-09-05 23:58:38.442] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:1000取锁失败[2020-09-05 23:58:38.970] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:0[2020-09-05 23:58:39.484] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:500[2020-09-05 23:58:39.999] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:1000取锁成功[2020-09-05 23:58:40.527] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:0[2020-09-05 23:58:41.041] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:500[2020-09-05 23:58:41.556] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:1000取锁失败[2020-09-05 23:58:42.085] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:0[2020-09-05 23:58:42.600] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:500[2020-09-05 23:58:43.115] INFO  cn.test.TestRedisLuaLock - 【加锁】等待重试:1000........
  • 因为我们在tryLock方法内 , 没有主动的去使用 del 命令去删除锁. 所以锁只有被过期失效掉, 或者是正常业务的结束被删除.

    这也符合了 解铃还须系铃人的原则了 (当然铃铛挂久了也会老化, 就自己失效调咯)

三. 总结

Redis 分布式锁的实现的主要思路是, 设置成功就返回设置结果 的 思路. 这个SETNX不谋而合, SETNX命令就是: 如果设置成功则返回1.

当然在版本2.6.1的Redis是没有SETNX命令的, 这个时候就使用Lua脚本来帮助我们将多个命令合并成一个原子性的操作

Redis的过期机制, 主要是防止单一程序长时间占用资源, 或者是不正常的结束进程. 导致锁没有正常的释放.

重试机制: 主要是在特短时间内, 允许线程阻塞等待一段时间, 直至取锁成功

关于分布式锁暂时说这么多

小编熬夜码字不易

需要好心人点点关注鼓励鼓励

实在不行点个?也还行

祝各位好梦

5622322e325ed0e48cf9434add88ac3c.gif

往期回顾

反向操作-Eurka的读写锁

Collection 太快受不了 - 集合迭代稳定性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值