Redis学习专题(六)分布式锁

目录

一、情景再现

二、分布式锁主流实现方案

三、实现Redis分布式锁

0、启动redis

[1、指令:setnx key value](#1、指令:setnx key value)

[2、指令:del key](#2、指令:del key)

[3、指令 :expire key seconds](#3、指令 :expire key seconds)

[4、指令:ttl key](#4、指令:ttl key)

[5、指令:set key value nx ex seconds](#5、指令:set key value nx ex seconds)

四、代码实现分布式锁

五、优化分布式锁

1、死锁

解决办法:设置过期时间

2、误删锁

解决办法:设置UUID

3、删除缺乏原子性

解决办法:LUA脚本保证删除原子性

思考:

核心区别:原子性​​


一、情景再现

  1. 单体单机部署的系统被演化成分布式集群系统后
  2. 由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效
  3. 单纯的Java API并不能提供分布式锁的能力
  4. 为了解决这个问题就需要一种跨JVM互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
  5. 示意图(说明:我们探讨的分布式锁是针对分布式项目/架构而言[...],和redis集群无关)

单体项目锁机制示意图:

分布式项目锁机制示意图:

二、分布式锁主流实现方案

1、基于数据库实现分布式锁

2、基于缓存Redis实现分布式锁

3、基于Zookeeper实现分布式锁

性能对比:

性能:redis最高

可靠性:Zookeeper最高

三、实现Redis分布式锁

0、启动redis

1、指令:setnx key value

setnx:上锁

key:锁的键

value:锁的值

在这个key删除之前,不能执行相同key的上锁指令

2、指令:del key

删除key,也就是释放锁

3、指令 :expire key seconds

给锁key设置过期时间,目的防止**死锁,**一直不释放锁就会造成死锁

4、指令:ttl key

查看某个锁key的过期时间

5、指令:set key value nx ex seconds

设置锁的同时,指定该锁的过期时间,防止死锁。

这个指令的原子性 的,防止setnx key value/setnx key seconds两条指令中间执行被打断。

到期自动删除

四、代码实现分布式锁

情景再现:

在单机redis下,用springboot+redis实现分布式锁的使用

示意图:

当index1、index2、index3都想去执行业务逻辑的时候,先要到redis中获得锁 (执行setnx指令成功),然后执行业务逻辑 ,必须释放锁,不然后续的请求进不来造成死锁

1、先初始化数据

2、执行以下代码:

复制代码
    @GetMapping("/lock")
    public void lock() {
        //第一步:获取锁
        Boolean lock=redisTemplate.opsForValue().setIfAbsent("lock", "ok");
        if (lock){
            //这个key为num的要先设置默认值,否则会出现问题
            Object value = redisTemplate.opsForValue().get("num");
            //1.判断返回的value是否有值
            if(value == null || !StringUtils.hasText(value.toString())){
                return;
            }
            //2.有值就转成int
            int num = Integer.parseInt(value.toString());
            //3.将num+1再转回去
            redisTemplate.opsForValue().set("num", num + 1);
            //4.释放锁
            redisTemplate.delete("lock");
        }else {
            //获取锁失败,休眠100ms再此获取
            try {
                Thread.sleep(100);
                lock();//递归重新执行
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }

3、保证Linux可以访问到springboot项目

4、用ab工具测试,指令:ab -n 1000 -c 100 http://ip:端口/api接口路径

请求1000次,每次按100请求

num就变成了1000

五、优化分布式锁

1、死锁

在前面的代码中,我们没有设置分布式锁的过期时间,如果因为业务异常没有释放锁,可能会导致死锁

解决办法:设置过期时间

2、误删锁

用户A业务操作因为卡顿,超过了设置的锁过期时间,于是自动释放了,此时用户B就获取到锁了,但是可能用户A的卡顿结束就会去释放锁,本次释放的锁就是用户B的锁了。

也就是步骤3释放了1的锁,步骤5释放了用户B在步骤4设置的锁

属于是释放锁时未验证当前锁是否仍属于自己

解决办法:设置UUID

1、给锁设置唯一的UUID

2、释放锁的时候查看是不是设置的同一把锁

3、造成这个问题的本质是缺乏原子性

3、删除缺乏原子性

用户A和用户B操作步骤如下:

1)比较uuid发现一样

2)准备删掉,但是还没有删除时,在1步获取的锁到了过期期间自动释放

3)同时因为高并发情况下,B用户执行了第4步,瞬间冲进来了,获取/设置了锁lock

4)这时del lock就是B用户的锁lock ,又出现了误删,也就是说A释放了B的锁

解决办法:LUA脚本保证删除原子性

解答:

用指令得到lock,这个值是当前请求的锁的值,ARGV是uuid值,如果相等就删除

执行

这个Array.asList("lock")会传递给script的KEYS[1],uuid会传给ARGV[1]

思考:

都是判断uuid和key的值是不是一样的,为什么直接比较uuid不行

核心区别:原子性​
​方案​ ​执行方式​ ​是否原子​ ​是否可能误删​
​Lua 脚本​ Redis 单线程执行(GET + DEL 一步完成) ✅ 是 ❌ 不会误删
​普通 Java 代码​ GET(Java端判断)→ 再 DEL(两步操作) ❌ 非原子 ✅ 可能误删

相关推荐
不会编程的阿成1 小时前
RabbitMQ概念
分布式·rabbitmq
Edingbrugh.南空2 小时前
Kafka 拦截器深度剖析:原理、配置与实践
分布式·kafka
jakeswang3 小时前
一致性框架:供应链分布式事务问题解决方案
分布式·后端·架构
笨手笨脚の6 小时前
系统性能优化-2 CPU
redis·nginx·性能优化·numa·系统调优·cpu对性能的影响
Edingbrugh.南空6 小时前
多维度剖析Kafka的高性能与高吞吐奥秘
分布式·kafka
Edingbrugh.南空6 小时前
Kafka数据写入流程源码深度剖析(客户端篇)
分布式·kafka
高冷小伙7 小时前
介绍下分布式ID的技术实现及应用场景
分布式
爱吃芝麻汤圆7 小时前
分布式——分布式系统设计策略一
分布式
码上库利南11 小时前
详细讲解Redis为什么被设计成单线程
数据库·redis·缓存
Edingbrugh.南空11 小时前
深入探究 Kafka Connect MQTT 连接器:从源码到应用
分布式·kafka