Redis分布式锁手动实现

Redis分布式锁手动实现

java中锁机制

在 Java 中,锁是用来同步并发访问共享资源的机制。它确保了在一个时间点,只有一个线程可以执行某个代码块或方法,从而防止了数据的不一致和竞态条件。Java 提供了多种锁机制,包括内置锁(synchronized 关键字)和显式锁(如 ReentrantLock)。

1. 内置锁(synchronized)

Java 的每个对象都有一个内置锁。当一个线程进入一个对象的 synchronized 方法或代码块时,它会自动获得该对象的锁,并在退出该方法或代码块时释放锁。其他尝试进入该对象的 synchronized 方法或代码块的线程将被阻塞,直到锁被释放。

使用示例:

public class Counter {  
    private int count = 0;  
  
    public synchronized void increment() {  
        count++;  
    }  
  
    public synchronized int getCount() {  
        return count;  
    }  
}

2. 显式锁(ReentrantLock)

ReentrantLock 是一个更灵活的锁机制,它提供了比内置锁更多的功能,如可中断的获取锁、尝试获取锁、定时获取锁等。与内置锁不同,ReentrantLock 必须显式地获取和释放。

示例代码:

import java.util.concurrent.locks.ReentrantLock;  
  
public class Counter {  
    private final ReentrantLock lock = new ReentrantLock();  
    private int count = 0;  
  
    public void increment() {  
        lock.lock();  
        try {  
            count++;  
        } finally {  
            lock.unlock();  
        }  
    }  
  
    public int getCount() {  
        lock.lock();  
        try {  
            return count;  
        } finally {  
            lock.unlock();  
        }  
    }  
}

3. 读写锁(ReadWriteLock)

ReadWriteLock 是一种特殊的锁,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这可以提高并发性能,因为读取操作通常不会修改数据,因此可以安全地并发执行。Java 的 java.util.concurrent.locks 包中提供了 ReadWriteLock 接口及其实现类 ReentrantReadWriteLock

基于redis的分布式锁

但是在微服务多个不同的进程之间这些标志位是不共享的,因此需要一个为分布式服务,存储共享锁标志。常见的分布式锁:redis分布式锁zookeeper分布式锁数据库的分布式锁等。

基于分布式锁现在已经有很多开源的实现,我们可以直接引用就行,基于redis的redission,基于zookeeper的 Curator框架,Spring框架也为此为我们提供了统一的分布式锁的定义接口。

基于上述框架的分布式锁机制,我们有机会再细聊。

接下来,我们一起来手动实现基于redis的分布式锁

1.创建一个Spring-boot项目

创建Spring-boot 项目,在pom.xml中导入以下依赖

<!--        redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
<!--junit-->
 		<dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
<!--spring-boot-test-->
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

在application.yml中配置你的redis链接

spring:
  redis:
    host: XXXX  #your host
    port: 6379  #default port
    password: XXXXX    #your password
    timeout: 60000		#redis client timeOut
    database: 0 	#default database 0

2.实现

2.1简单实现v1(×)

我们简单想一下锁的基本实现,锁的目的就是,对于公共资源,程序A持有资源,程序B在访问该资源并获取时,会获取不到。

基于redis存储的<key,value>形式的数据,我们的设计:

所有消费者程序可以持有公共的key,在程序A访问时,我们可以在redis中存储一条数据,当程序B 进行访问时,在redis中判断key,如果存在表示已经有人持有锁了,没有则我们放入这个key去获取锁,执行完业务逻辑将这个key删除。

原理大概就是这样,我们一起将其付诸于实践

对于key,我们可以自定义,在此我们使用key: lock:consumer

对于value,也可以自定义,在此我们使用value:“1”

LockDemoSimple1示例代码如下:

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2024/4/16 16:29
 * @注释 最简易的分布式锁实现 版本1
 */
@Component
public class LockDemoSimple1 {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /***
     * 尝试加锁
     */
    public boolean trySimpleLock1() {
        String lockKey = "lock:consumer";

        // 尝试获取锁
        String value = redisTemplate.opsForValue().get(lockKey);
        if (value == null) {
            // 加锁
            redisTemplate.opsForValue().set(lockKey, "1");
            return true;
        }
        return false;
    }

    /****
     * 释放锁
     */
    public void releaseSimpleLock1() {
        String lockKey = "lock:consumer";
        redisTemplate.delete(lockKey);
    }

编写测试类进行测试:

  //测试自定义锁
    @Test
    public void testSimpleLock1() throws Exception {
        for (int i = 0; i < 5; i++) {
            System.out.println("线程:" + i + "开始执行,尝试获取锁,获取结果为:" + lockDemoSimple1.trySimpleLock1());
        }
    }

运行结果:

请添加图片描述

可以看到,redis中0号数据库中看到了存入的数据

请添加图片描述

接下来我们执行一下释放锁的测试方法,会发现redis0号数据库中数据被删除了

 @Test
    public void testSimpleLock1Release() throws Exception {
        lockDemoSimple1.releaseSimpleLock1();
    }

请添加图片描述

我们模拟一下正式的运行环境,testSimpleLock1AtestSimpleLock1B两个测试方法分别代表分布式系统中的两个程序,优先运行程序A,然后运行程序B。

 //测试自定义锁v1
    @Test
    public void testSimpleLock1A() throws Exception {
        try {
            if (lockDemoSimple1.trySimpleLock1()) {
                System.out.println("程序A:执行业务逻辑,睡100秒钟");
                Thread.sleep(100000);
            } else {
                System.out.println("程序A:获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("程序A:释放锁");
            lockDemoSimple1.releaseSimpleLock1();
        }

    }
    @Test
    public void testSimpleLockB() throws Exception {
        try {
            if (lockDemoSimple1.trySimpleLock1()) {
                System.out.println("程序B:执行业务逻辑,睡100秒钟");
                Thread.sleep(100000);
            } else {
                System.out.println("程序B:获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("程序B:释放锁");
            lockDemoSimple1.releaseSimpleLock1();
        }

    }

运行后截图:

请添加图片描述

请添加图片描述

观察上面的两个图,你会发现他们使用同一个Key在没获取到锁的时候也会去释放锁,删除key,这样会使testSimpleLock1A在执行业务逻辑期间,它的锁被testSimpleLock1B获取失败后,释放掉了;如果后续还会有testSimpleLock1C程序启动,C程序又能去获取资源了,很明显这里设计是存在问题的。

因此,我们又想到了另一个方案:

我们需要一个标识来标记这个锁属于某个程序,如果不是它的,执行释放锁操作就不能进行操作。

2.2简单实现v2[预防非法释放](×)

那么怎么去创建标识呢,我这里想到了UUID ,在合理的概率下,全球范围内每个生成的 UUID 都是唯一的

简单介绍一下UUID:

UUID(Universally Unique Identifier,通用唯一识别码)是一种由标准算法生成的128位数字,用于唯一标识信息元素。UUID由以下几部分构成:

  1. 时间戳:通常使用当前时间或时钟序列作为UUID的第一个组成部分,以确保每个UUID的唯一性。这个时间戳是自1582年10月15日午夜(即格林威治标准时间0点)以来的纳秒数。
  2. 时钟序列号:表示当前计数器的值,当时间戳发生变化时,时钟序列号会重新开始计数。
  3. 全局唯一标识:通常为一个计算机名、网络地址或MAC地址等固定值,用于标识生成UUID的计算机或网络环境。
  4. 变量节点号:一般是当前计算机的MAC地址或其他唯一标识符,用于增加UUID的随机性和唯一性。
  5. 版本号:表明UUID的版本,是一个随机值。目前有四个版本的UUID生成算法。

UUID的长度为16字节,可以表示2^128个唯一的值,因此生成重复的UUID在理论上具有极低的概率。这使得UUID在需要唯一标识符的场景中非常有用。

那么我们一起实现一下第二版程序,代码如下:

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2024/4/16 16:29
 * @注释 最简易的分布式锁实现 版本2 预防非法释放锁
 */
@Component
public class LockDemoSimple2 {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    private String value;
    private String lockKey = "lock:consumer";

    /***
     * 尝试加锁
     */
    public boolean trySimpleLock1() {
        // 尝试获取锁
        String value = redisTemplate.opsForValue().get(lockKey);
        if (value == null) {
            // 加锁
            UUID uuid = UUID.randomUUID();
            this.value = uuid.toString();
            redisTemplate.opsForValue().set(lockKey, this.value);

            return true;
        }
        return false;
    }

    /****
     * 释放锁
     */
    public void releaseSimpleLock1() {

        if (value != null && value.equals(redisTemplate.opsForValue().get(lockKey))) {
            System.out.println("释放自己的锁");
            redisTemplate.delete(lockKey);
        } else {
            System.out.println("不是我自己的锁,我不释放");
        }
    }


}

依旧创建测试方法进行测试(代码没差,除了修改注入的🔒):

 //测试自定义锁v2
    @Test
    public void testSimpleLock1A() throws Exception {
        try {
            if (lockDemoSimple2.trySimpleLock1()) {
                System.out.println("程序A:执行业务逻辑,睡100秒钟");
                Thread.sleep(100000);
            } else {
                System.out.println("程序A:获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("程序A:释放锁");
            lockDemoSimple2.releaseSimpleLock1();
        }

    }
    @Test
    public void testSimpleLockB() throws Exception {
        try {
            if (lockDemoSimple2.trySimpleLock1()) {
                System.out.println("程序B:执行业务逻辑,睡100秒钟");
                Thread.sleep(100000);
            } else {
                System.out.println("程序B:获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println("程序B:释放锁");
            lockDemoSimple2.releaseSimpleLock1();
        }

    }

在程序A在持有资源,进行业务逻辑处理时,程序B获取不到锁,同时redis 0号数据库可以看到数据。

请添加图片描述

请添加图片描述

请添加图片描述

在程序A在持有资源,处理完业务逻辑处理,并释放自己的锁时,redis 0号数据库可以看到数据消失,此时重新启动程序B,B也能获取锁,进行业务逻辑处理。

请添加图片描述

请添加图片描述

此时我们解决了非法释放锁的问题,那么我们再看看加锁的这段逻辑,看看是否仍然存在一些问题。

/***
     * 尝试加锁
     */
    public boolean trySimpleLock1() {
        // 尝试获取锁
        String value = redisTemplate.opsForValue().get(lockKey);
        if (value == null) {
            // 加锁
            UUID uuid = UUID.randomUUID();
            this.value = uuid.toString();
            redisTemplate.opsForValue().set(lockKey, this.value);

            return true;
        }
        return false;
    }

虽然redis是单线程的,但是如果两个程序同时读到key为lock:consumer的没有设置值的情况,可能会出现以下覆盖值的情况

在这里插入图片描述

因此我们需要将查看redis的值是否存在设置值弄成一个不可分割的操作,类似于事务,而redis也为我们提供了这个命令setnx key value,只有在不存在的时候才会去设置值,存在就不设置值了。

在这里插入图片描述

2.3简单实现v3[保证原子性](×)

将判断是否存在和设置值的操作合并在一起,保证操作的原子性

**
 * @version 1.0
 * @Author jerryLau
 * @Date 2024/4/16 16:29
 * @注释 最简易的分布式锁实现 版本3 保证原子性
 */
@Component
public class LockDemoSimple3 {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    private String value;
    private String lockKey = "lock:consumer";

    /***
     * 尝试加锁
     */
    public boolean trySimpleLock1() {
        // 尝试获取锁
        String uuid = UUID.randomUUID().toString();
        // 原子性操作setNX
        if (redisTemplate.opsForValue().setIfAbsent(lockKey, uuid)) {
            // 加锁
            this.value = uuid;
            return true;
        }
        return false;
    }

    /****
     * 释放锁
     */
    public void releaseSimpleLock1() {

        if (value != null && value.equals(redisTemplate.opsForValue().get(lockKey))) {
            System.out.println("释放自己的锁");
            redisTemplate.delete(lockKey);
        } else {
            System.out.println("不是我自己的锁,我不释放");
        }
    }
}

这回看似肯定没问题了,但是分布式服务有个最大的特点就是防止单点灾难

如果你在加锁期间你的服务挂了咋办,你的key一直不会被释放,这样对于公共资源,大家一块都不能使用了;这在开发中肯定不行,redis也有设置键的过期命令set key value ex number nx 中number就是时间,nx表示不存在才会执行。

在这里插入图片描述

但是这儿过期时间怎么去把握,如果设置的时间过长,可能造成资源的浪费,如果设置的时间过短,可能会在程序执行过程中,释放锁。那么这个问题应该如何解决呢?

没准定时任务其周期刷新是个好的方法。如果我们设置一个定时任务去周期性的帮我们续费key的时间。如果这个线程一直在,就一直续费,这个想法感觉还可以。

2.4简单实现v4[ttl时间续费](×)

大体思路如下:

在获取锁成功,启动一个定时任务去周期设置key的失效时间,当然在key不存在或者此线程已经被销毁(也就是执行完业务之后),应该停止此定时任务。

创建一个间隔10s的定时任务,进行线程存活检测,参考代码如下:

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2024/4/16 16:29
 * @注释 最简易的分布式锁实现 版本4 定时器续费
 */
@Component
public class LockDemoSimple4 {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    private String value;
    private String lockKey = "lock:consumer";

    /***
     * 尝试加锁
     */
    public boolean trySimpleLock1() {
        // 尝试获取锁
        String uuid = UUID.randomUUID().toString();
        // 原子性操作setNX
        if (redisTemplate.opsForValue().setIfAbsent(lockKey, uuid)) {
            // 加锁
            this.value = uuid;
            renewKey(Thread.currentThread(), lockKey);
            return true;
        }
        return false;
    }

    /****
     * 释放锁
     */
    public void releaseSimpleLock1() {

        if (value != null && value.equals(redisTemplate.opsForValue().get(lockKey))) {
            System.out.println("释放自己的锁");
            redisTemplate.delete(lockKey);
        } else {
            System.out.println("不是我自己的锁,我不释放");
        }
    }


    /**
     * 定时续费
     * @param thread 线程
     * @param key
     */
    public void renewKey(Thread thread, String key) {
        ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);

        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                if (thread.isAlive() && redisTemplate.hasKey(key)) {
                    System.out.println("线程还在,给key续30秒");
                    redisTemplate.expire(key, 30, TimeUnit.SECONDS);
                } else {
                    System.out.println("线程已经不存在,终止定时任务");
                    throw new RuntimeException("终止定时任务");
                }
            }
        }, 10, 10, TimeUnit.SECONDS);
    }
}

编写测试类对上述代码进行测试,对于程序A,设置休眠时间为50s,那么在休眠期间会触发redis锁key的续费操作。
在这里插入图片描述

观察redis中key的存活时间,发现会被续费

在这里插入图片描述

如果程序A异常终止,根据redis中设置的key的过期时间,依然获释放🔒资源,程序A运行时手动停止来模拟程序A异常终止

在这里插入图片描述

在这里插入图片描述

至此,基于redis手动实现分布式锁基本实现,现在可以再将代码进行封装一下。

2.5简单实现v5[代码封装,优化接口](!)

改动代码让其更符合使用的逻辑,比如说key让用户传进来,让用户自己设置过期时间,阻塞获取锁,或者定时一段时间内去获取锁。

示例代码:

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2024/4/16 16:29
 * @注释 最简易的分布式锁实现 版本5 继续分装简化逻辑
 */
@Component
public class LockDemoSimple5 {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private String value;

    private ThreadLocal<String> keyMap = new ThreadLocal<String>();//保存线程内部的局部变量

    @Autowired
    private ScheduledExecutorService scheduledExecutorService;

    /***
     * 尝试加锁
     * @param key
     * @return
     */
    public boolean trySimpleLock(String key) {
        keyMap.set(key);

        // 尝试获取锁
        String uuid = UUID.randomUUID().toString();
        this.value = uuid;
        System.out.println(Thread.currentThread().getName() + "获取锁   " + key + "   " + uuid + "方法被调用");
        // 原子性操作setNX
        if (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {
            // 加锁
            renewKey(Thread.currentThread(), key);
            return true;
        }
        return false;
    }

    /***
     * 给定时间内尝试加锁
     * @param key
     * @param timeout 超时时间
     * @return
     */
    public boolean trySimpleLock(String key, int timeout) {
        keyMap.set(key);
        // 尝试获取锁
        String uuid = UUID.randomUUID().toString();

        //计算结束时间
        Instant endTime = Instant.now().plusSeconds(timeout);

        //时间比较
        while (Instant.now().getEpochSecond() < endTime.getEpochSecond()) {
            // 原子性操作setNX
            if (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {
                // 加锁
                this.value = uuid;
                renewKey(Thread.currentThread(), key);
                return true;
            }
        }
        return false;
    }

    /***
     * 尝试加锁(阻塞)
     * @param key
     * @param timeout
     * @return
     */
    public void Lock(String key, int timeout) {
        keyMap.set(key);
        // 尝试获取锁
        String uuid = UUID.randomUUID().toString();
        while (true) {
            if (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {
                // 加锁
                this.value = uuid;
                renewKey(Thread.currentThread(), key);
                break;
            }
        }
    }

    /****
     * 释放锁
     */
    public void releaseSimpleLock() {
        System.out.println(Thread.currentThread().getName() + "释放锁方法被调用");
        String key = keyMap.get();
        System.out.println(Thread.currentThread().getName() + "释放锁   " +  " VALUE保存的:  " + this.value);
        System.out.println(Thread.currentThread().getName() + "释放锁   " +  "value从redis获取的:   " + redisTemplate.opsForValue().get(key));

        if (value != null && value.equals(redisTemplate.opsForValue().get(key))) {
            System.out.println( Thread.currentThread().getName() + "释放自己的锁");
            redisTemplate.delete(key);
            keyMap.remove();
        } else {
            System.out.println("不是我自己的锁,我不释放");
        }
    }


    /**
     * 定时续费
     * @param thread
     * @param key
     */
    public void renewKey(Thread thread, String key) {
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                if (thread.isAlive() && redisTemplate.hasKey(key)) {
                    System.out.println("线程还在,给key续30秒");
                    redisTemplate.expire(key, 30, TimeUnit.SECONDS);
                } else {
                    System.out.println("线程已经不存在,终止定时任务");
                    throw new RuntimeException("终止定时任务");
                }
            }
        }, 10, 10, TimeUnit.SECONDS);
    }
}

抽取后的配置文件:

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2024/4/19 14:16
 * @注释
 */
@Configuration
public class LockDemoSimple5Conf {

    @Bean
    public ConcurrentHashMap<Thread, String> map() {
        return new ConcurrentHashMap<>();
    }

    /**
     * 使用线程池优化新性能
     *
     * @return
     */
    @Bean
    public ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() {
        return new ScheduledThreadPoolExecutor(10);
    }
}

编写测试代码进行上述v5版本的测试

//模仿实际场景,测试自定义锁
    @Test
    public void testSimpleLock2() throws InterruptedException {
        System.out.println("程序A:开始");

        try {
            if (lockDemoSimple5.trySimpleLock("Lock:key")) {
                System.out.println("程序A: 获取锁成功,开始执行业务逻辑,睡50秒");
                //模拟业务逻辑
                Thread.sleep(50000);
            } else
                System.out.println("程序A:获取锁失败,无法执行业务逻辑");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放锁
            System.out.println("程序A:释放锁");
            lockDemoSimple5.releaseSimpleLock();
        }
    }

    @Test
    public void testSimpleLock3() throws Exception {
        try {

            System.out.println("程序B:开始获取锁");
            lockDemoSimple5.Lock("Lock:key", 30);
            System.out.println("程序B:获取锁成功,开始执行业务逻辑,睡30秒");
            //模拟业务逻辑
            Thread.sleep(30000);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放锁
            System.out.println("程序B:释放锁");
            lockDemoSimple5.releaseSimpleLock();
        }
    }

程序A运行

在这里插入图片描述

程序 B先是阻塞,等到程序A执行结束释放后,程序B进行执行

在这里插入图片描述

再将程序B的测试方法修改一下,设置成获取不到锁直接返回,在程序A执行的过程中启动程序B

 @Test
    public void testSimpleLock3() throws Exception {
        try {

            System.out.println("程序B:开始获取锁");
            boolean b = lockDemoSimple5.trySimpleLock("Lock:key", 30);
            if (b) {
                System.out.println("程序B:获取锁成功,开始执行业务逻辑,睡30秒");
                //模拟业务逻辑
                Thread.sleep(30000);
            } else {
                System.out.println("程序B获取锁失败,无法执行业务");
            }


        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //释放锁
            System.out.println("程序B:释放锁");
            lockDemoSimple5.releaseSimpleLock();
        }
    }

可以看到经过30s后程序B仍然获取不到锁,然后直接返回了结果

在这里插入图片描述

至此呢,我们基本实现了简单的分布式锁。

对于分布式锁的特性,我们在百度一下。

分布式锁的特性:

多进程可见:多进程可见,否则就无法实现分布式效果

互斥(必须的):同一时刻,只能有一个进程获得锁,执行任务后释放锁

可重入(可选):同一个任务再次获取改锁不会被死锁

阻塞锁(可选):获取失败时,具备重试机制,尝试再次获取锁

性能好(可选):效率高,应对高并发场景

高可用:避免锁服务宕机或处理好宕机的补救措施

2.6简单实现v6[提供可重入,接口优化,通过redistemplete执行lua脚本](!)

可以使用redis基本数据类型hset哈希结构来存储锁的持有者信息。每个锁的持有者(可能是一个线程或者一个客户端)在哈希中以一个字段的形式存在,字段名为持有者的ID(threadId),字段值为持有的锁数量(这里可能是一个计数器)。当锁被释放时,持有者的计数将减少。

那么加锁和解锁的逻辑如下:

获取锁的步骤:

1.先判断key是否存在

2.如果存在,判断是否是自己的锁,使用唯一的uuid表示,如果是,给count +1,如果不是表示锁已经被别人占有,加锁失败

3.如果不存在,表示锁还没有被持有,则添加hash,key为分布式锁的标识,field为uuid,唯一的锁身份标识,标识是谁的锁,value设置为1表示进入了一次

释放锁的步骤:

1.先判断key是否存在

2.如果存在,则判断是不是自己的锁,通过唯一的身份标识uuid,如果是,count进行-1操作,-1之后如果值为0,则删除这个hash。如果不是自己的锁,则不做任何操作

3.如果不存在,不做任何操作

这个时候值得注意的一点是:大家如果都读取到那个能获取锁的时间,同时加锁咋整?虽然redis是单线程的,但是如果两个人读取Key是否存在刚好同时操作,就会出问题,为此我们需要将获取锁和释放锁以数据库的事务一样要么全部完成,要么都失败,但是很不幸redis的事务并不是数据库的事务,不过也相应的提供了lua脚本功能,你可以在脚本中,将执行的redis命令一次性执行完,对于redis而言他就是一条命令,能够保证原子性。

需要专门去学这东西吗,我个人感觉用处不大,用的时候直接复制过来就行,而且看起来也不是很难懂。
接下来我们对代码在进行封装:

初始化🔒的接口:

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2024/4/25 13:40
 * @注释 创建锁
 */
public interface LockObtainInterface {

    /***
     * 创建🔒
     * @return
     */
    public LockInterface obtainLock(String key);
}

初始化🔒的实现类:

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2024/4/16 16:29
 * @注释 最简易的分布式锁实现 获取锁
 */
public class LockObtain implements LockObtainInterface {
    //redis Template
    private StringRedisTemplate redisTemplate;

    //prefix
    private String prefix;


    public LockObtain(StringRedisTemplate redisTemplate, String prefix) {
        this.prefix = prefix;
        this.redisTemplate = redisTemplate;
    }

    @Override
    public LockInterface obtainLock(String key) {
        return new LockDemoSimple6(redisTemplate, prefix + ":" + key);
    }
}

🔒操作接口:

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2024/4/25 13:31
 * @注释 自定义锁接口 加锁等操作
 */
public interface LockInterface {


    /**
     * 尝试获取🔒资源,如果获取不到,就阻塞
     */
    public void lock();

    /**
     * 尝试获取🔒资源
     *
     * @return 获取到返回true, 如获取不到返回false
     */
    public boolean tryLock();

    /**
     * 尝试在指定时间内获取🔒资源
     *
     * @param time
     * @return获取指定时间内没有获取到返回true,如获取不到返回false
     */
    public boolean tryLock(long time);

    /**
     * 释放🔒
     */
    public int unlock();

}

🔒的配置类

/**
 * @version 1.0
 * @Author jerryLau
 * @Date 2024/4/25 13:35
 * @注释 适用于案例6的自定义配置类
 */
@Configuration
public class LockDemoSimple6Conf {
    @Bean
    public LockObtainInterface lockRegistry(StringRedisTemplate redisTemplate) {
        return new LockObtain(redisTemplate, "lock");
    }

}

🔒操作的实现类:

/***
 * 🔒操作的实现类
 */
public class LockDemoSimple6 implements LockInterface {
    private StringRedisTemplate redisTemplate;
    private String lockKey;
    private String lockKeyValue;
    private long DEFAULT_RELEASE_TIME = 30;
    private static final DefaultRedisScript<Long> LOCK_SCRIPT;
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    private ScheduledExecutorService scheduledExecutorService  = new ScheduledThreadPoolExecutor(1);

    static {
        // 加载释放锁的脚本
        LOCK_SCRIPT = new DefaultRedisScript<>();
        LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new
                ClassPathResource("lock.lua")));
        LOCK_SCRIPT.setResultType(Long.class);
        // 加载释放锁的脚本
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new
                ClassPathResource("unlock.lua")));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    public LockDemoSimple6(StringRedisTemplate redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey;
        this.lockKeyValue = UUID.randomUUID().toString();
    }



    @Override
    public boolean tryLock() {
        // 执行脚本
        Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),
                lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));
        // 判断结果
        return result != null && result.intValue() == 1;
    }

    @Override
    public boolean tryLock(long time) {
        Instant endTime = Instant.now().plusMillis(time);

        while(Instant.now().getEpochSecond() < endTime.getEpochSecond()) {
            Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),
                    lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));

            if (result != null && result.intValue() == 1) {
                renewKey(Thread.currentThread());

                return true;
            }
        }

        return false;
    }

    @Override
    public void lock() {

        while (true) {
            Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),
                    lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));

            if (result != null && result.intValue() == 1) {
                renewKey(Thread.currentThread());

                break;
            }
        }
    }

    @Override
    public int unlock() {
        // 执行脚本
        Long execute = redisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(lockKey),
                lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));
        System.out.println("execute:"+execute);

        return execute.intValue();
    }

    /**
     * 定时续费
     * @param thread
     */
    public void renewKey(Thread thread) {
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            if (thread.isAlive() && redisTemplate.hasKey(lockKey)) {
                redisTemplate.expire(lockKey, DEFAULT_RELEASE_TIME, TimeUnit.SECONDS);
            } else {
                throw new RuntimeException("终止定时任务");
            }
        }, 10, 10, TimeUnit.SECONDS);
    }
}

在其中用到了加锁以及解锁的lua脚本

加锁脚本:

local key = KEYS[1]
local threadId = ARGV[1]
local releaseTime = ARGV[2]

if(redis.call('exists', key) == 0)
then
    redis.call('hset', key, threadId, '1')
    redis.call('expire', key, releaseTime)
    return 1
end

if(redis.call('hexists', key, threadId) == 1)
then
    redis.call('hincrby', key, threadId, '1')
    redis.call('expire', key, releaseTime)
    return 1
end
return 0

解锁脚本:

local key = KEYS[1]
local threadId = ARGV[1]
local releaseTime = tonumber(ARGV[2])

-- 检查锁的持有者身份
if (redis.call('HEXISTS', key, threadId) == 0) then
    -- 释放失败,因为调用者不是锁的持有者
    return 0
end

-- 减少锁的持有者计数
local count = redis.call('HINCRBY', key, threadId, -1)

-- 如果计数大于0,重新设置过期时间
if (count > 0) then
    redis.call('EXPIRE', key, releaseTime)
    -- 释放成功,但锁仍然被其他持有者持有
    return 1
else
    -- 删除整个哈希键,因为没有任何持有者了
    redis.call('DEL', key)
    -- 释放成功,且锁已经完全释放
    return 1
end

构建简单的测试类继续此时,先测试阻塞获取锁,启动程序A,程序B,B先阻塞一直等到A执行完成后在进行获取执行:

运行截图就不贴了

在测试指定时间内获取锁

在这里插入图片描述

在这里插入图片描述

至此,关于手动实现redis的分布式锁基本完成,哈哈哈,还算是比较顺利的

相关代码请查看代码仓库:jerryLau-hua/spring-boot-redis


后面有时间,可以在研究使用redission 等第三方框架 实现redis分布式锁,喜欢该系列的同学们记得一键三连哈🎉🎉🎉🎉🎉

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/576878.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

全国各地级市财政收入支出明细统计数据2003-2022年

01、数据简介 全国各地级市财政统计主要是按地级市财政支出和财政收入两项统计&#xff0c;反映地区财政资金形成、分配以及使用情况的统计&#xff0c;​是由地区各地级市统计局统计公布&#xff0c;是加强财政资金管理使用的依据&#xff0c;研究国民收入分配和再分配的重要…

山东省2024年首版次测试报告具体的要求是什么?

山东省首版次测试报告的具体要求可能会根据每年的政策调整、行业变化以及申报的具体产品而有所不同。但一般而言&#xff0c;山东省首版次测试报告需要满足以下一些基本要求和标准&#xff1a; 1.完整性&#xff1a;测试报告应涵盖所有关键的测试环节&#xff0c;包括但不限于测…

张小泉签约实在智能,用实在Agent打造自动化高

在不少老杭州人的童年记忆里&#xff0c;妈妈裁剪衣服、料理食材、修剪各种物品&#xff0c;用的都是张小泉刀剪。 近日&#xff0c;实在智能与“刀剪第一股”张小泉&#xff08;股票代码&#xff1a;301055.SZ&#xff09;正式达成合作&#xff0c;实在Agent数字员工助力张小…

AM解调 FPGA(寻找复刻电赛电赛D题的)

设计平台 Quartus II10.3mif产生工具modelsimSE &#xff08;仿真用&#xff09; DDS&#xff08;直接数字式频率合成器&#xff09; 从前面的内容可知&#xff0c;我们需要产生一个载波&#xff0c;并且在仿真时&#xff0c;我们还需要一个较低频率的正弦波信号来充当我们的…

划重点:用这个技巧,抖音粉丝涨不停!

在这个信息爆炸的时代&#xff0c;如何在抖音上脱颖而出&#xff0c;吸引大量粉丝&#xff0c;成为了每一个创作者心中的痛。你是否曾经在发布作品后焦急等待评论&#xff0c;期待着每一次互动&#xff1f;如果你有这样的困扰&#xff0c;那么这篇文章将为你打开一扇新的大门&a…

【Claude 3 Opus】Claude 3 Opus 模型正式上线抢先体验

文章目录 1. Claude 3 Opus介绍2. Claude 3 Opus 支持的应用场景3. 申请Claude 3 Opus访问4. Claude 3 Opus初体验5. 『云上探索实验室』Bedrock 体验又更新啦6. 参考链接 1. Claude 3 Opus介绍 近期&#xff0c;亚马逊云宣布 Anthropic 的 Claude 3 Opus 模型已在 Amazon Bed…

大数据分析与应用实验(黑龙江大学)

实验一 Hadoop伪分布式实验环境搭建与WordCount程序 一、实验目的 1、学习搭建Hadoop伪分布式实验环境 2、在伪分布式实验环境下运行WordCount程序 二、实验内容 1、搭建Hadoop伪分布式实验环境&#xff0c;并安装Eclipse。 2、在Eclipse环境下&#xff0c;编写并执行Wor…

【JVM】从i++到JVM栈帧

【JVM】从i到JVM栈帧 本篇博客将用两个代码例子&#xff0c;简单认识一下JVM与栈帧结构以及其作用 从i与i说起 先不急着看i和i&#xff0c;我们来看看JVM虚拟机&#xff08;请看VCR.JPG&#xff09; 我们初学JAVA的时候一定都听到过JAVA“跨平台”的特性&#xff0c;也就是…

西瓜书学习——线性判别分析

文章目录 定义LDA的具体步骤1. 计算类内散布矩阵&#xff08;Within-Class Scatter Matrix&#xff09;2. 计算类间散布矩阵&#xff08;Between-Class Scatter Matrix&#xff09;3. 求解最佳投影向量4. 数据投影5. 分类 定义 线性判别分析&#xff08;Linear Discriminant A…

安装svn网络有问题怎么办?

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

【C++进阶之路】C++11(下) —— 线程库

序言 本篇文章主要是填之前C11留下的坑以及了解与熟悉线程库&#xff0c;有读者感兴趣之前的内容的话可见「C进阶之路」专栏中标题为「C11」的内容&#xff0c;废话不多说&#xff0c;先来概括一下本文的内容&#xff0c;首先我们会从历史的角度分别谈及Linux以及Windows下的线…

JetBrains GoLand v2024.1 激活版 (Go语言集成开发IDE)

前言 JetBrains GoLand是一款专门为Go语言开发人员构建的跨平台的集成开发环境。动态错误检测和修复建议、快速安全重构、智能代码完成、无效代码检测和文档提示可以帮助新手和有经验的Go开发人员高效地创建可靠的代码。GoLand还支持JavaScript&#xff0c;TypeScript&#xf…

AIX7环境上一次艰难的Oracle打补丁经历

系统环境 AIX &#xff1a;7200-05-03-2148 Oracle&#xff1a;11.2.0.4 PSU: 11.2.0.4.201020&#xff08;31718723&#xff09; perl:5.28 问题一&#xff1a;AUTO patch #/u01/app/11.2.0/grid/OPatch/opatch auto /tmp/31718723 错误信息如下&#xff1a;匹配mos 2516761.1…

K8s: 公有镜像中心和私有镜像中心的搭建

公有镜像中心的搭建和使用 1 &#xff09;在 官方docker镜像中心推送 在 hub.docker.com 上注册账号 (国内一般访问不了&#xff0c;原因不多说) 找到 Create Repository 按钮就行仓库的创建 这样就在官方创建了一个仓库&#xff0c;比如地址为: xx/y-y xx 是我的账户名y-y 是…

MATLAB - 机器人动力学 - 质心(Center of Mass)

系列文章目录 前言 一、用法 com centerOfMass(robot) com centerOfMass(robot,configuration) [com,comJac] centerOfMass(robot,configuration) 二、说明 com centerOfMass(robot) 计算机器人模型在原点构型处相对于基础坐标系的质心位置。com centerOfMass(robot,conf…

TCP关闭连接时的一些思考

TCP协议是TCP/IP栈中最复杂的协议&#xff0c;它最大的优点是传输的可靠性&#xff0c;这通过面向连接、按序传输、超时重传、流量控制等机制保证其传输的可靠性。但这并不是我们今天要讨论的重点&#xff01; TCP通信的过程分别是三个阶段&#xff1a;建立连接、传输数据、关…

VMware ESXi虚拟机备份的方法和步骤

关于虚拟机备份 VMware ESXi 是 VMware vSphere 企业虚拟化套件的核心组件。在版本4.1之前&#xff0c;它一直被称为ESX。ESXi是一种裸机管理程序&#xff0c;直接安装在物理服务器上&#xff0c;它提供对底层资源的直接访问和控制&#xff0c;允许您在单个物理主机上创建和运…

私有化部署 Llama3 大模型, 支持 API 访问

私有化部署 Llama3 大模型, 支持 API 访问 视频 https://www.bilibili.com/video/BV1wD421n75p/ 前言 原文 https://ducafecat.com/blog/llama3-model-api-local 通过 ollama 本地运行 Llama3 大模型其实对我们开发来说很有意义&#xff0c;你可以私有化放服务上了。 然后通…

练习题(2024/4/26)

1所有可能的路径 给你一个有 n 个节点的 有向无环图&#xff08;DAG&#xff09;&#xff0c;请你找出所有从节点 0 到节点 n-1 的路径并输出&#xff08;不要求按特定顺序&#xff09; graph[i] 是一个从节点 i 可以访问的所有节点的列表&#xff08;即从节点 i 到节点 graph…

科普童话新课堂杂志社科普童话新课堂编辑部2024年第16期目录

作品选 封2,封3-封4 探索新知《科普童话》投稿&#xff1a;cn7kantougao163.com 泱泱国之风 悠悠诗之情 沈灿宇1-3 试论"文化意识"视角下的高中英语阅读教学 董娜4-6 立足小组合作探究优化写作能力培养 时同祥7-9 以"导"促学:全面提升学生的文学核心素养 吴…