redis分布式锁+redisson框架

目录

🧂1.锁的类型

🌭2.基于redis实现分布式

🥓[3. 基于redisson实现分布式锁](#3. 基于redisson实现分布式锁)


1.锁的类型

  • 1.本地锁:synchronize、lock等,锁在当前进程内,集群部署下依旧存在问题
  • 2.分布式锁:redis、zookeeper等实现,虽然还是锁,但是多个进程共用的锁标记,可以用Redis、Zookeeper, Mysql等都可以

2.基于redis实现分布式锁

1.加锁 setnx key value

  • setnx 的含义就是 SET if Not Exists,有两个参数 setnx(key, value),该方法是原子性操作
  • 如果 key 不存在,则设置当前 key 成功,返回 1;
  • 如果当前 key 已经存在,则设置当前 key 失败,返回 0

2.解锁 del(key)

得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入,调用 del(key)

3.配置锁超时 expire(key,30)

客户端崩溃或者网络中断,资源将会永远被锁住,即死锁,因此需要给key配置过期时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放

4.代码

正常的加锁逻辑,但存在问题

java 复制代码
  public void coupon(){
        String key="coupon_id";
        if (setnx(key,1)==1){
            expire(key,30, TimeUnit.MILLISECONDS)
            try{
                //业务
            }finally {
                del(key);
            }
        }else {
            //睡眠,然后自旋调用
            coupon();
        }
    }

5.存在的问题

  • 1.问题一
    • 多个命令之间不是原子性操作,如setnx和expire之间,如果setnx成功,则这个资源就是死锁但是expire失败,且宕机了,则这个资源就是死锁。
  • 2.解决
    • 使用原子命令:同时设置和配置过期时间 setnx / setex
    • redisTemplate.opsForValue().setIfAbsent(key, v: "lock", l: 30,TimeUnit.SECoNDS);
  • 1.问题二
    • 业务超时,存在其他线程勿删,key 30秒过期,假如线程A执行很慢超过30秒,则key就被释放了,其他线程B就得到了锁,这个时候线程A执行完成,而B还没执行完成,结果就是线程A删除了线程B加的锁
  • 2.解决
    • 可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁,那 value 应该是存当前线程的标识或者uuid
  • 3.但是,删除锁时也不是原子性操作。
    • 当线程A获取到正常值时,返回带代码中判断期间锁过期了,线程B刚好重新设置了新值,线程A那边有判断value是自己的标识,然后调用del方法,结果就是删除了新设置的线程B的值
    • 核心还是判断和删除命令 不是原子性操作导致

6.使用lua脚本

  • 问题: 加锁使用setnx setex可以保证原子性,那解锁使用判断和删除怎么保证原子性 ????
  • 多个命令的原子性:采用lua脚本+redis,由于【判断和删除】是lua脚本执行,所以要么全成功,要么全失败
    lua脚本
Lua 复制代码
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
  • 使用redis+lua分布式锁,正常严谨的逻辑

redisTemplate.execute返回值不必须long 类型,但你必须确保 Redis 脚本返回的数据与你在 DefaultRedisScript 中指定的类型相匹配。

java 复制代码
public class Main {
    @Autowired
    StringRedisTemplate redisTemplate;

    /**
     * 分布式锁-redis
     */
    @Test
    public void test() {
        //根据业务,动态获取key
        String key = "coupon_id";
        //随机生成uuid,作为value值
        String uuid = CommonUtil.generateUUID();
        //设置key的同时,设置过期时间;获取锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, uuid, 30, TimeUnit.MILLISECONDS);
        //判断是否加锁成功
        if (lock) {
            //枷锁成功
            try {
                //执行业务
            } finally {
                //释放锁,使用lua脚本保持原子性
                String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
                //执行lua脚本
                Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(key), uuid);
            }
        } else {
            //睡眠一定时间
            //加锁失败,自旋
            test();
        }
    }
}
  • 锁的过期时间,如何实现锁的自动过期 或者 避免业务执行时间过长,锁过期了?
  • 答:一般把锁的过期时间设置久一点,比如10分钟时间

3. 基于redisson实现分布式锁

1.添加依赖

XML 复制代码
            <!--分布式锁-->
            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson</artifactId>
                <version>3.12.0</version>
            </dependency>

2.添加redissonClient

如果报错的话,查看java版本是否正确

java 复制代码
@Configuration
@Data
public class RedissonConfig {

    //从配置文件获取redis的相关信息
    @Value("${spring.redis.host}")
    private String redisHost;
    @Value("${spring.redis.port}")
    private String redisPort;

    /**
     * 使用redisson作为分布式锁
     * @return
     */
    @Bean
    public RedissonClient redissonClient() {
        //1.创建配置对象
        Config config = new Config();
        //2.连接redis
        config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
        //3,创建redissonClient对象
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

3.实现分布式锁

myLock.lock()如果不指定过期时间,则默认30s后过期

java 复制代码
  @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/lock")
    public JsonData test() {
        //1.获取锁
        RLock myLock = redissonClient.getLock("my_lock");
        //2.手动加锁
        myLock.lock();
        try {
            //3.业务实现
            System.out.println("加锁成功~" + Thread.currentThread().getName());
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //4.手动解锁
            myLock.unlock();
            System.out.println("解锁成功~" + Thread.currentThread().getName());
        }
        return JsonData.buildSuccess();
    }

4.看门狗机制

Redis锁的过期时间小于业务的执行时间该如何续期?

  • 为了避免这种情况的发生, Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。
  • Redisson中客户端一旦加锁成功,就会启动一个watch dog看门狗。watch dog是一个后台线程,会每隔10秒检查一下,如果客户端还持有锁key,那么就会不断的延长锁key的生存时间。
  • 1.指定加锁时间

myLock.lock(10, TimeUnit.SECONDS)

  • 指定加锁时间后,默认10秒后过期,并且没有看门狗机制
  • 2.不指定加锁时间

myLock.lock(10, TimeUnit.SECONDS)

  • 默认30秒后过期,存在看门狗机制
  • 只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔(【LockWatchingTimeOut看门狗默认时间】/3)这么长时间自动续期;
相关推荐
m0_571957581 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2343 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
Chrikk5 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*5 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue5 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man5 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang
测开小菜鸟5 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity6 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq