Redis分布式锁手动实现

news2024/12/28 20:12:27

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.coloradmin.cn/o/1626943.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springBoot集成seata实现分布式事务

背景 日常开发离不开分布式系统&#xff0c;自然避免不了分布式事务问题&#xff0c;seata 是一款阿里开源的主流分布式事务解决方案&#xff0c;但实际工作引入seata感觉有点重&#xff0c;本人之前在商业银行做开发对数据一致性要求很高&#xff0c;但很少团队使用。几年前曾…

PDF加密了无法编辑?解密方法来了!

一下午都在捣鼓各种格式问题&#xff0c;首先是需要合并几个 PDF&#xff0c;然而有一个文件加密了无法操作&#xff0c;碰到加密不能编辑就很头痛&#xff0c;终于让我找到一个可行的方法了&#xff0c; 首先就这个加密文件右键选择打开方式-Google Chrome>>打开>>…

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

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

【服务器部署篇】Jenkins配置后端工程自动化部署

作者介绍&#xff1a;本人笔名姑苏老陈&#xff0c;从事JAVA开发工作十多年了&#xff0c;带过刚毕业的实习生&#xff0c;也带过技术团队。最近有个朋友的表弟&#xff0c;马上要大学毕业了&#xff0c;想从事JAVA开发工作&#xff0c;但不知道从何处入手。于是&#xff0c;产…

计算机服务器中了mkp勒索病毒怎么办,mkp勒索病毒解密数据恢复流程

网络技术的不断应用与发展&#xff0c;为企业的生产运营带来了极大便利&#xff0c;越来越多的企业依赖网络开展各项工作业务&#xff0c;网络也大大提升了企业的生产运营效率&#xff0c;但网络是一把双刃剑&#xff0c;在为企业提供便利的同时&#xff0c;也为企业的数据安全…

IDEA生成测试类

方法一 具体流程: 选中要生成的测试类------------>选择code选项------------>选择Generate选项---------->选择test选项---------->选择要生成的方法 第一步: 光标选中需要生成测试类的类 找到code选项 选中Generate选项 选中test选项 选中你要生成的测试…

【目标检测】基于深度学习的酒瓶表面瑕疵缺陷检测(yolov5算法,6个类别,附代码和数据集)

写在前面: 首先感谢兄弟们的关注和订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。(专栏订阅用户订阅专栏后免费提供数据集和源码一份,超级VIP用户不在服务范围之内) 路虽远,行则将至;事虽难,做…

运行游戏提示dll文件丢失,分享多种有效的解决方法

在我们日常频繁地利用电脑进行娱乐活动&#xff0c;特别是畅玩各类精彩纷呈的电子游戏时&#xff0c;常常会遭遇一个令人困扰的问题。当我们满怀期待地双击图标启动心仪的游戏程序&#xff0c;准备全身心投入虚拟世界时&#xff0c;屏幕上却赫然弹出一条醒目的错误提示信息&…

springboot+vue生鲜超市销售管理系统

项目效果演示视频: springbootvue的生鲜商城的设计与实现 生鲜超市销售管理系统springbootvue 基于JAVAVueSpringBootMySQL的生鲜超市销售系统&#xff0c;包含了商品档案、商品进货、商品销售、供应商、活动管理、消息通知模块&#xff0c;还包含系统自带的用户管理、部门管理…

多输入多输出 | Matlab实现WOA-LSSVM鲸鱼算法优化最小二乘支持向量机多输入多输出预测

多输入多输出 | Matlab实现WOA-LSSVM鲸鱼算法优化最小二乘支持向量机多输入多输出预测 目录 多输入多输出 | Matlab实现WOA-LSSVM鲸鱼算法优化最小二乘支持向量机多输入多输出预测预测效果基本介绍程序设计往期精彩参考资料 预测效果 基本介绍 Matlab实现WOA-LSSVM鲸鱼算法优化…

Linux系统----信号(万字文章超级详细并且简单易学附有实操shell指令图及注释!)

绪论​ “Do one thing at a time, and do well.”&#xff0c;本章开始Linux系统其中信号是学习操作系统的基本下面将会讲到什么是信号、信号的多种产生方式、信号如何保存的、信号如何处理的、以及一些信号的细节。话不多说安全带系好&#xff0c;发车啦&#xff08;建议电脑…

AS-V1000 视频监控平台,如何实现设备上线、下线时产生告警,及时通知管理人员

目录 一、客户需求 &#xff08;一&#xff09;客户需求 &#xff08;二&#xff09;掌握设备状况的意义 1、实时故障检测与预警 2、提升系统可靠性 3、优化资源配置 4、增强安全保障 5、提升管理效率 二、产品介绍 三、系统配置 &#xff08;一&#xff09;实现告警…

[网络安全] apt攻击是什么?

什么是APT攻击&#xff1a;APT攻击的主要特征包括&#xff1a;APT攻击的防御措施&#xff1a;零基础入门学习路线视频配套资料&国内外网安书籍、文档网络安全面试题 什么是APT攻击&#xff1a; APT&#xff08;Advanced Persistent Threat&#xff0c;高级持续性威胁&…

大小写不规范引起的LVS问题

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 往期文章链接: LVS常见问题解析 综合网表不规范,大小写混用常导致LVS问题,比如两个端口clk和CLK只有大小写区别,PR工具是可以识别为两个端口的,只不过Calibre LVS默认不区分大小写,会报错。 …

mac安装java

在 macOS 上配置 Java 环境变量是相对简单的。你需要做的是设置 JAVA_HOME 环境变量&#xff0c;并将 bin 目录添加到 PATH 变量中。本篇是最详细的教程&#xff0c;细化每个步骤过程&#xff0c;保姆级的教程&#xff01; 1. 下载JDK安装包 到oracle官网下载适合的JDK安装包…

外包干了4个月,技术退步明显

先说情况&#xff0c;大专毕业&#xff0c;18年通过校招进入湖南某软件公司&#xff0c;干了接近6年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落&#xff01; 而我已经在一个企业干了四年的功能…

关于前后端一体项目SpringSecurity框架登陆失效,HTTPS重定向登陆页面异常的问题

现有环境是基于SpringBoot 2.6.8&#xff0c;然后是前后台一体化的项目。 安全框架使用的是内置版本的SpringSecurity。 场景&#xff1a;用户登陆&#xff0c;系统重启导致用户的session失效。但前端并没有跳转到对应的登录页&#xff0c;在HTTP的环境下可以正常跳转&#x…

Linux——DNS的配置和使用

一、DNS 域名服务器&#xff0c;实现IP和域名的转换 DNS 协议运行在 UDP 协议之上&#xff0c;使用端口号 53 2.结构 DNS 的命名空间的结构如下&#xff1a; 1. 根域名&#xff08; Root Domain &#xff09;&#xff1a; 根域名位于 DNS 命名空间的顶部&#xff0c;它表示…

[leetcode] 58. 最后一个单词的长度

文章目录 题目描述解题方法倒序遍历java代码复杂度分析 题目描述 给你一个字符串 s&#xff0c;由若干单词组成&#xff0c;单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。 单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。 示例 1&#xff1a…

OpenHarmony实战开发—进程间通讯

版本&#xff1a;v3.2 Beta5 进程模型 OpenHarmony的进程模型如下图所示&#xff1a; 应用中&#xff08;同一包名&#xff09;的所有UIAbility、ServiceExtensionAbility、DataShareExtensionAbility运行在同一个独立进程中&#xff0c;即图中绿色部分的“Main Process”。…