小工具知识网

当前位置: 首页 > 行业知识 > Redis(十七)分布式锁

行业知识

Redis(十七)分布式锁

2026-11-08 10:49:09 来源:互联网转载

文章目录

  • 面试题
  • 分布式锁
    • 锁的种类
    • 分布式锁需要具备的条件和刚需
    • 分布式锁
    • 案例
      • nginx分布式微服务部署,单机锁问题
      • 分布式锁注意事项
      • lock/unlock+lua脚本自研版的redis分布式锁搞定
        • lua脚本
        • 可重入锁
          • 可重入锁种类
          • 可重入锁hset实现,对比setnx(重要)
          • 分布式锁需要具备的条件和刚需
          • lua脚本
          • 工厂模式分布式锁
          • 自动续期
            • CAP再提起
            • 总结
              • 引入分布式锁

                面试题

                基于Redis的什么用法?

                1. 数据共享,分布式Session
                2. 分布式锁
                3. 全局ID
                4. 计算器、点赞
                5. 位统计
                6. 购物车
                7. 轻量级消息队列
                8. 抽奖
                9. 回来的题目
                10. 点赞、签到、打卡
                11. 差集交集并集,用户关注、可能认识的人,推荐模型
                12. 热点新闻、热搜排行榜
                • Redis 做分布式锁的时候有需要注意的问题?
                • 你们公司自己实现的分布式锁是否用的setnx命令实现?

                  不可以

                  • 这个是最合适的吗?你如何考虑分布式锁的可重入问题?
                  • 如果是 Redis 是单点部署的,会带来什么问题?那你准备怎么解决单点问题呢?
                  • Redis集群模式下,比如主从模式,CAP方面有没有什么问题呢?

                    CAP:

                    C:一致性:在分布式系统中的任意一个节点都会查询到相同的信息(拿到的都是最新的)

                    A:可用性:服务一直可用,而且是正常响应时间,好的可用性主要是指系统能够很好的为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。(只要我访问你就给我返回,如果要满足分布式(P),机器之间网络断掉的话,直接和C冲突)

                    P:分区容错性:当分布式系统中一部分节点崩溃的时候,当前系统仍旧能够正常对外提供服务(多台机器,分布式,不满足P就是单机么)

                    Redis集群:是AP,Redis单机是C,一致性

                    区别Zookeeper集群:是CP,全部节点收到后返回ack

                    • 那你简单的介绍-下 Redlock吧?你简历上写redisson,你谈谈
                    • Redis分布式锁如何续期?看门狗知道吗?

                      分布式锁

                      JUC中AQS锁的规范落地参考+可重入锁考虑+Lua脚本+Redis命令实现分布式锁

                      锁的种类

                      1. 单机版同一个M虚拟机内,synchronized或者Lock接口
                      2. 分布式多个不同M虚拟机,单机的线程锁机制不再起作用,资源类在不同的服务器之间共享了。

                      分布式锁需要具备的条件和刚需

                      1. 独占性:OnlyOne,任何时刻只能有且仅有一个线程持有
                      2. 高可用:若redis集群环境下,不能因为某一个节点挂了而出现获取锁和释放锁失败的情况;高并发请求下,依旧高性能
                      3. 防***锁:杜绝***锁,必须有超时控制机制或者撤销操作,有个兜底终止跳出方案
                      4. 不乱抢:防止张冠李戴,不能私下unlock别人的锁,只能自己加锁自己释放,自己约的锁含着泪也要自己解
                      5. 重入性:同一个节点的同一个线程如果获得锁之后,它也可以再次获取这个锁。

                      分布式锁

                      案例

                      nginx分布式微服务部署,单机锁问题

                      分布式部署后,单机锁还是出现超卖现象,需要分布式锁

                      分布式锁注意事项

                      1. 重试:递归重试,容易导致stackoverflowerror
                      2. 宕机-防止***锁:部署了微服务的Java程序机器挂了,代码层面根本没有走到finally这块
                      3. 防止误删key:stringRedisTemplate.delete(key);只能自己删除自己的锁,不可以删除别人的,需要添加判断
                      4. Lua保证原子性:存在问题就是最后的判断+del不是一行原子命令操作,需要用lua脚本进行修改
                      5. 可重入锁+设计模式:不满足可重入性

                      lock/unlock+lua脚本自研版的redis分布式锁搞定

                      lua脚本

                      https://redis.io/docs/reference/patterns/distributed-locks/

                      使用示例

                      可重入锁

                      可重入锁(递归锁):可以再次进入的同步锁

                      进入:进入同步域(即同步代码块/方法或显式锁定的代码)

                      一句话:一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入

                      可重入锁种类

                      1. 隐式锁(即synchronized关键字使用的锁)默认是可重入锁
                      //同步块public class ReEntryLockDemo{    public static void main(String[] args)    {        final Object objectLockA = new Object();        new Thread(() -> {            synchronized (objectLockA)            {                System.out.println("-----外层调用");                synchronized (objectLockA)                {                    System.out.println("-----中层调用");                    synchronized (objectLockA)                    {                        System.out.println("-----内层调用");                    }                }            }        },"a").start();    }}
                      1. Synchronized的重入的实现机理
                      //同步方法public class ReEntryLockDemo{    public synchronized void m1()    {        System.out.println("-----m1");        m2();    }    public synchronized void m2()    {        System.out.println("-----m2");        m3();    }    public synchronized void m3()    {        System.out.println("-----m3");    }    public static void main(String[] args)    {        ReEntryLockDemo reEntryLockDemo = new ReEntryLockDemo();        reEntryLockDemo.m1();    }}
                      1. 显式锁(即Lock)也有ReentrantLock这样的可重入锁。
                      //显式锁public class ReEntryLockDemo{    static Lock lock = new ReentrantLock();    public static void main(String[] args)    {        new Thread(() -> {            lock.lock();            try            {                System.out.println("----外层调用lock");                lock.lock();                try                {                    System.out.println("----内层调用lock");                }finally {                    // 这里故意注释,实现加锁次数和释放次数不一样                    // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。                    lock.unlock(); // 正常情况,加锁几次就要解锁几次                }            }finally {                lock.unlock();            }        },"a").start();        new Thread(() -> {            lock.lock();            try            {                System.out.println("b thread----外层调用lock");            }finally {                lock.unlock();            }        },"b").start();    }}

                      切记,一般而言,你lock了几次就要unlock几次

                      public class ReEntryLockDemo{Lock lock = new ReentrantLock();    public void entry()    {        new Thread(() -> {            lock.lock();            try            {                System.out.println(Thread.currentThread().getName()+"\t"+"外层调用lock");                lock.lock();                try                {                    System.out.println(Thread.currentThread().getName()+"\t"+"内层调用lock");                }finally {                //这里不解锁,已经加了两次锁                    //lock.unlock();                }            }finally {                lock.unlock();            }        },"t1").start();        //暂停毫秒        try { TimeUnit.MILLISECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }        new Thread(() -> {            lock.lock();            try            {                System.out.println(Thread.currentThread().getName()+"\t"+"外层调用lock");            }finally {                lock.unlock();            }        },"t2").start();    }    public static void main(String[] args)    {        ReEntryLockDemo demo = new ReEntryLockDemo();        demo.entry();        //在一个Synchronized修饰的方法或代码块的内部调用本类的其他Synchronized修饰的方法或代码块时,是永远可以得到锁的    }}

                      可重入锁hset实现,对比setnx(重要)

                      可重入锁模拟redis

                      hset redis锁名字(zzyyRedisLock) 某个请求线程的UUID+ThreadID 加锁的次数

                      setnx:只能解决有无的问题,但是不完美

                      hset:不但解决有无,还解决可重入问题

                      分布式锁需要具备的条件和刚需

                      1. 独占性
                      2. 高可用
                      3. 防***锁
                      4. 不乱抢

                      lua脚本

                      |-先判断redis分布式锁这个key是否存在EXISTS key|-key不存在:返回零说明不存在,hset新建当前线程属于自己的锁BY UUID:ThreadlD |-key存在:返回壹说明已经有锁,需进一步判断是不是当前线程自己的:HEXISTS key uuid:ThreadlD|-返回0说明不是自己的|-返回非0说明是自己的:自增1表示重入
                      1. 显示参数版本
                      if redis.call('exists','key') == 0 or redis.call('hexists','key','uuid:threadid') == 1 then  redis.call('hincrby','key','uuid:threadid',1)  redis.call('expire','key',30)  return 1else  return 0end
                      1. 参数替换版本
                      名称替换位置示例值
                      keyKEYS[1]testRedisLock
                      valueARGV[1]2f586ae740a94736894ab9d51880ed9d:1
                      过期时间值ARGV[2]30 秒
                      if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then   redis.call('hincrby',KEYS[1],ARGV[1],1)   redis.call('expire',KEYS[1],ARGV[2])   return 1 else  return 0end

                      工厂模式分布式锁

                      封装锁,使用工厂模式

                      @Componentpublic class RedisDistributedLock implements Lock{    private StringRedisTemplate stringRedisTemplate;    private String lockName;//KEYS[1]    private String uuidValue;//ARGV[1]    private long   expireTime;//ARGV[2]//注意这里    public RedisDistributedLock(StringRedisTemplate stringRedisTemplate, String lockName, String uuid)    {        this.stringRedisTemplate = stringRedisTemplate;        this.lockName = lockName;        this.uuidValue = uuid+":"+Thread.currentThread().getId();        this.expireTime = 30L;    }    @Override    public void lock()    {        tryLock();    }    @Override    public boolean tryLock()    {        try {tryLock(-1L,TimeUnit.SECONDS);} catch (InterruptedException e) {e.printStackTrace();}        return false;    }    @Override    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException    {        if(time == -1L)        {            String script =                    "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then    " +                            "redis.call('hincrby',KEYS[1],ARGV[1],1)    " +                            "redis.call('expire',KEYS[1],ARGV[2])    " +                            "return 1  " +                    "else   " +                            "return 0 " +                    "end";            System.out.println("lockName:"+lockName+"\t"+"uuidValue:"+uuidValue);            while(!stringRedisTemplate.execute(new DefaultRedisScript(script,Boolean.class), Arrays.asList(lockName), uuidValue,String.valueOf(expireTime)))            {                //暂停60毫秒                try { TimeUnit.MILLISECONDS.sleep(60); } catch (InterruptedException e) { e.printStackTrace(); }            }            //新建一个后台扫描程序,来坚持key目前的ttl,是否到我们规定的1/2 1/3来实现续期            renewExpire();            return true;        }        return false;    }    @Override    public void unlock()    {        System.out.println("unlock(): lockName:"+lockName+"\t"+"uuidValue:"+uuidValue);        String script =                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then    " +                        "return nil  " +                "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then    " +                        "return redis.call('del',KEYS[1])  " +                "else    " +                        "return 0 " +                "end";        // nil = false 1 = true 0 = false        Long flag = stringRedisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime));        if(null == flag)        {            throw new RuntimeException("this lock doesn't exists,o(╥﹏╥)o");        }    }    private void renewExpire()    {        String script =                "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then     " +                        "return redis.call('expire',KEYS[1],ARGV[2]) " +                "else     " +                        "return 0 " +                "end";        new Timer().schedule(new TimerTask()        {            @Override            public void run()            {                if (stringRedisTemplate.execute(new DefaultRedisScript(script, Boolean.class), Arrays.asList(lockName), uuidValue, String.valueOf(expireTime)))                {                    renewExpire();                }            }        },(this.expireTime * 1000)/3);    }    //暂时用不到    @Override    public void lockInterruptibly() throws InterruptedException    {    }    @Override    public Condition newCondition()    {        return null;    }}

                      分布式锁工厂

                      @Componentpublic class DistributedLockFactory{    @Autowired    private StringRedisTemplate stringRedisTemplate;    private String lockName;    private String uuid;    public DistributedLockFactory()    {        //uuid会变化,所以在类创建的时候就uuid放入内部,否则影响可重入性        this.uuid = IdUtil.simpleUUID();    }    public Lock getDistributedLock(String lockType)    {        if(lockType == null) {            return null;        }        if(lockType.equalsIgnoreCase("REDIS")){            this.lockName = "zzyyRedisLock";            return new RedisDistributedLock(stringRedisTemplate,lockName,uuid);        }else if(lockType.equalsIgnoreCase("ZOOKEEPER")){            this.lockName = "zzyyZookeeperLockNode";            //TODO zookeeper版本的分布式锁            return null;        }else if(lockType.equalsIgnoreCase("MYSQL")){            //TODO MYSQL版本的分布式锁            return null;        }        return null;    }}

                      使用工厂锁

                      public String sale7()    {        String retMessage = "";        Lock redisLock = distributedLockFactory.getDistributedLock("redis");        redisLock.lock();        try        {            //1 查询库存信息            String result = stringRedisTemplate.opsForValue().get("inventory001");            //2 判断库存是否足够            Integer inventoryNumber = result == null ? 0 : Integer.parseInt(result);            //3 扣减库存,每次减少一个            if(inventoryNumber > 0)            {                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));                retMessage = "成功卖出一个商品,库存剩余:"+inventoryNumber;                System.out.println(retMessage+"\t"+"服务端口号"+port);                testReEntry();            }else{                retMessage = "商品卖完了,o(╥﹏╥)o";            }        }finally {            redisLock.unlock();        }        return retMessage+"\t"+"服务端口号"+port;    }

                      自动续期

                      CAP再提起

                      CAP即:

                      1、Consistency(一致性):对于客户端的每次读操作,要么读到的是最新的数据,要么读取失败。换句话说,一致性是站在分布式系统的角度,对访问本系统的客户端的一种承诺:要么我给您返回一个错误,要么我给你返回绝对一致的最新数据,不难看出,其强调的是数据正确。

                      2、Availability(可用性):任何客户端的请求都能得到响应数据,不会出现响应错误。换句话说,可用性是站在分布式系统的角度,对访问本系统的客户的另一种承诺:我一定会给您返回数据,不会给你返回错误,但不保证数据最新,强调的是不出错。

                      3、Partition tolerance(分区容忍性):由于分布式系统通过网络进行通信,网络是不可靠的。当任意数量的消息丢失或延迟到达时,系统仍会继续提供服务,不会挂掉。换句话说,分区容忍性是站在分布式系统的角度,对访问本系统的客户端的再一种承诺:我会一直运行,不管我的内部出现何种数据同步问题,强调的是不挂掉。

                      Redis集群是AP

                      Zookeeper集群是CP

                      Eureka集群是AP

                      Nacos集群是AP

                      总结

                      nginx微服务单机锁出现问题:只能锁本服务

                      引入分布式锁

                      1. 只加了锁,没有释放锁,出异常的话,可能无法释放锁,必须要在代码层面fnallv释放锁
                      2. 宕机了,部署了微服务代码层面根本没有走到fnally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
                      3. redis的分布式锁key,增加过期时间此外,还必须要setnx+过期时间必须同一行
                        1. 必须规定只能自己删除自己的锁,不能把别人的锁删除了unlock变为Lua脚本保证
                        2. 锁重入,hset替代setnx+lock变为Lua脚本保证
                        3. 自动续期

上一篇:迷你硬盘服务器怎么设置?

下一篇:宝塔面板怎么调整性能?