首页 > 其他分享 >分布式锁实践

分布式锁实践

时间:2024-03-09 12:45:56浏览次数:35  
标签:... 解锁 redis 实践 token org import 分布式

分布式锁实践

安装工具

正常是需要在linux安装redis(官方推荐),为了方便在开发环境中,使用windows版本的redis

GitHub - redis-windows/redis-windows: Redis 6.0.20 6.2.14 7.0.15 for Windows

下载release版本,

根据readme,在服务中注册,并启动redis:redis-windows/README.zh_CN.md at main · redis-windows/redis-windows · GitHub

下载windows下的redis管理可视化工具Releases · qishibo/AnotherRedisDesktopManager · GitHub

image-20240307161346528

image-20240307161212936

image-20240307161300241

那么,开始代码环节

代码实践

导入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>

配置

工具类

因为是工具类,所以并不想将他强依赖于IOC,所以这里使用beanFactory来对他进行bean的加载。

package com.example.redisdemo.config;


import com.example.redisdemo.utils.RedisLockUtil;

import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class BeanFactory {

    @Bean
    public RedisLockUtil redisLockUtil(RedisTemplate<String, String> redisTemplate) {
        return new RedisLockUtil(redisTemplate);
    }
}
package com.example.redisdemo.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.stereotype.Component;

import java.nio.charset.Charset;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;

@Slf4j
public class RedisLockUtil {

    private RedisTemplate<String,String> redisTemplate;

    /**
     * 解锁脚本,原子操作
     */
    private static final String unlockScript =
            "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"
                    + "then\n"
                    + "    return redis.call(\"del\",KEYS[1])\n"
                    + "else\n"
                    + "    return 0\n"
                    + "end";

    public RedisLockUtil(RedisTemplate<String,String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public RedisLockUtil() {
    }


    /**
     * 加锁,有阻塞
     * @param name
     * @param expire
     * @param timeout
     * @return
     */
    public String lock(String name, long expire, long timeout){
        long startTime = System.currentTimeMillis();
        String token;
        do{
            token = tryLock(name, expire);
            if(token == null) {
                if((System.currentTimeMillis()-startTime) > (timeout-50))
                    break;
                try {
                    Thread.sleep(50); //try 50 per sec
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return null;
                }
            }
        }while(token==null);

        return token;
    }


    /**
     * 加锁,无阻塞
     * @param name
     * @param expire
     * @return
     */
    public String tryLock(String name, long expire) {
        String token = UUID.randomUUID().toString();
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection conn = factory.getConnection();
        try{
            Boolean result = conn.set(name.getBytes(Charset.forName("UTF-8")), token.getBytes(Charset.forName("UTF-8")),
                    Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);
            if(result!=null && result)
                return token;
        }finally {
            RedisConnectionUtils.releaseConnection(conn, factory);
        }
        return null;
    }

    /**
     * 解锁
     * @param name
     * @param token
     * @return
     */
    public boolean unlock(String name, String token) {
        byte[][] keysAndArgs = new byte[2][];
        keysAndArgs[0] = name.getBytes(Charset.forName("UTF-8"));
        keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8"));
        RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
        RedisConnection conn = factory.getConnection();
        try {
            Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keysAndArgs);
            if(result!=null && result>0)
                return true;
        }finally {
            RedisConnectionUtils.releaseConnection(conn, factory);
        }

        return false;
    }

}


尝试获取锁

    @Test
    void redisLockTest() {
        String token = redisLockUtil.tryLock("fuwu:123", 1000);
        System.out.println("拿到锁:"+token);
    }

image-20240307171805606

此时已有锁,尝试再次执行

image-20240307171838148

token返回为null,说明获取锁失败。使用10个线程进行等待尝试,看是否能够实现交替进入业务。

多线程竞争

package com.example.redisdemo;

import javax.annotation.Resource;

import com.example.redisdemo.utils.RedisLockUtil;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.ValueOperations;

@SpringBootTest
class RedisDemoApplicationTests {
    @Resource
    RedisLockUtil redisLockUtil;

    @Test
    void redisLockTest() throws InterruptedException {
        System.out.println();
        String name = "RedisLockTest";
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(()->{
                System.out.println("业务编号:" + finalI + "开始执行,尝试获取锁...");
                String token = redisLockUtil.lock(name, 60, 60 * 1000);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(finalI+" 拿到锁:"+token);
                System.out.println("业务执行中...");
                System.out.println("业务执行完成\n开始解锁中...");
                if (redisLockUtil.unlock(name, token)) {
                    System.out.println("token:"+ token +" 解锁成功");
                }
            }).start();
        }
        Thread.sleep(10000);
    }

}

输出

业务编号:0开始执行,尝试获取锁...
业务编号:8开始执行,尝试获取锁...
业务编号:3开始执行,尝试获取锁...
业务编号:4开始执行,尝试获取锁...
业务编号:2开始执行,尝试获取锁...
业务编号:5开始执行,尝试获取锁...
业务编号:6开始执行,尝试获取锁...
业务编号:1开始执行,尝试获取锁...
业务编号:7开始执行,尝试获取锁...
业务编号:9开始执行,尝试获取锁...
9 拿到锁:a3059978-efff-4f06-847f-a21a0fd6a6dc
业务执行中...
业务执行完成
开始解锁中...
token:a3059978-efff-4f06-847f-a21a0fd6a6dc 解锁成功
1 拿到锁:7aba4b40-77f1-448f-bbc3-60b0991d8b8d
业务执行中...
业务执行完成
开始解锁中...
token:7aba4b40-77f1-448f-bbc3-60b0991d8b8d 解锁成功
3 拿到锁:df582f8a-3791-431c-8732-d64cb897dd47
业务执行中...
业务执行完成
开始解锁中...
token:df582f8a-3791-431c-8732-d64cb897dd47 解锁成功
7 拿到锁:c771321a-707a-4b1d-8023-732b991cf9b1
业务执行中...
业务执行完成
开始解锁中...
token:c771321a-707a-4b1d-8023-732b991cf9b1 解锁成功
0 拿到锁:ba041359-e7ef-4ed7-a59c-8d95e2b5b31d
业务执行中...
业务执行完成
开始解锁中...
token:ba041359-e7ef-4ed7-a59c-8d95e2b5b31d 解锁成功
5 拿到锁:78f5eee0-e182-48fa-be70-09f1ec0853e4
业务执行中...
业务执行完成
开始解锁中...
token:78f5eee0-e182-48fa-be70-09f1ec0853e4 解锁成功
4 拿到锁:3f821c1e-8c23-4062-8290-d184bd8b09ee
业务执行中...
业务执行完成
开始解锁中...
token:3f821c1e-8c23-4062-8290-d184bd8b09ee 解锁成功
2 拿到锁:f861db53-f7cf-48e8-95ec-b08e86b26273
业务执行中...
业务执行完成
开始解锁中...
token:f861db53-f7cf-48e8-95ec-b08e86b26273 解锁成功
8 拿到锁:0b50aa0b-3a72-47be-b62a-bb1ac50693b7
业务执行中...
业务执行完成
开始解锁中...
token:0b50aa0b-3a72-47be-b62a-bb1ac50693b7 解锁成功
6 拿到锁:5524fe2a-1967-453e-a86e-bf35a49de5d9
业务执行中...
业务执行完成
开始解锁中...
token:5524fe2a-1967-453e-a86e-bf35a49de5d9 解锁成功

可以看到,锁的获取都是串行的,说明该锁在多线程的情况下,能保持作用

使用setNX + lua脚本解锁作为分布式锁解决方案的小结

首先,代码简单,代码简单意味着可维护性高,并且出了问题十分好定位。

但是其实仅实现了较为简单的功能,仅使用lua脚本解决了在解锁的时候,获取锁的值跟目前线程持有锁的值对比,然后对比成功再删除,且该操作是原子性的,防止A线程在获取自己的锁的value的时候刚好超时,B线程进来获取到锁了,然后A线程把B线程的锁给嘎了这种场景。

但是,A线程在正常运行的情况下,真的有必要释放锁吗?虽然我们设置了过期时间是为了防止A线程因为宕机,或者业务太长执行了太长时间了,导致一系列问题。但是如果是A线程跟B线程一起执行的话,势必会导致各种问题呢?那么A线程就得保持住自己的锁,不让B进来,为了防止宕机问题,那么过期时间必然是要设置的,所以此时需要续命了,如果A线程还在执行,那么给他续个时间。这种系统的复杂度还是较高的,为了防止各种奇奇怪怪的BUG,我们这引入redisson来实现,redisson提供了看门狗机制,10s检查一下A线程是否还活着,如果活着,给它续一次命,看门狗是单独的线程。

因此,下面开始编写redisson来实现分布式锁的写法。

Redisson实现分布式锁

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>2.7.0</version>
        </dependency>
    @Bean(name = "Redisson")
    public Redisson redisson() {
        Config config = new Config();
        config.useSingleServer()
                .setAddress("localhost:6379");
//        config.useClusterServers()
//                // 集群状态扫描间隔时间,单位是毫秒
//                .setScanInterval(2000)
//                //cluster方式至少6个节点(3主3从,3主做sharding,3从用来保证主宕机后可以高可用)
//                .addNodeAddress("redis://127.0.0.1:6379" )
//                .addNodeAddress("redis://127.0.0.1:6380")
//                .addNodeAddress("redis://127.0.0.1:6381")
//                .addNodeAddress("redis://127.0.0.1:6382")
//                .addNodeAddress("redis://127.0.0.1:6383")
//                .addNodeAddress("redis://127.0.0.1:6384");
        return (Redisson) Redisson.create(config);
    }

    @Bean(name = "RedisLockUtilForRedisson")
    public RedisLockUtilForRedisson redisLockUtil(Redisson redisson) {
        return new RedisLockUtilForRedisson(redisson);
    }
package com.example.redisdemo.utils;

import java.util.concurrent.TimeUnit;

import jodd.datetime.TimeUtil;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.spring.cache.RedissonSpringCacheManager;

public class RedisLockUtilForRedisson {
    public RedisLockUtilForRedisson() {
    }

    private Redisson redisson;

    public RedisLockUtilForRedisson(Redisson redisson) {
        this.redisson = redisson;
    }

    public boolean acquire(String key) {
        RLock lock = redisson.getLock(key);
        lock.lock(1,TimeUnit.MINUTES);
        return true;
    }

}

如果指定了过期时间,那么不会进入到scheduleExpirationRenewal,不会续命,使用默认时间30s会自动续命,续命的检查时间为 30s / 3 = 10s

image-20240307232352180

其他unlock、tryLock等自行看源码即可,较于自己实现,仅免去了写lua脚本,实现原理类似。

参考

SpringBoot实现Redis分布式锁 - 简书 (jianshu.com)

标签:...,解锁,redis,实践,token,org,import,分布式
From: https://www.cnblogs.com/jy00/p/18062533

相关文章

  • 理解Saga模式:分布式事务的优雅解决方案
    理解Saga模式:分布式事务的优雅解决方案在微服务架构中,系统通常被拆分成多个独立的服务,每个服务管理着自己的数据和逻辑。这种拆分带来了灵活性和可扩展性,但同时也引入了分布式事务管理的挑战。传统的事务管理方法,如数据库的ACID(原子性、一致性、隔离性、持久性)事务,不再适用于跨多......
  • Jmeter性能测试:高并发分布式性能测试
    一、为什么要进行分布式性能测试当进行高并发性能测试的时候,受限于Jmeter工具本身和电脑硬件的原因,无法满足我们对大并发性能测试的要求。基于这种场景下,我们就需要采用分布式的方式来实现我们高并发的性能测试要求。二、分布式性能测试原理要进行分布式性能测试,我们首先要一......
  • 【教程】HBuilderX开发实践:隐私合规检测问题解决方案
    文章目录摘要引言正文1、违规收集个人信息2、APP强制、频繁、过度索取权限知识点补充总结 摘要本篇博客介绍了在使用HBuilderX进行开发过程中,常遇到的隐私合规问题,并提供了相应的解决方案。主要包括违规收集个人信息和APP强制、频繁、过度索取权限两方面。......
  • 开源.NET8.0小项目伪微服务框架(分布式、EFCore、Redis、RabbitMQ、Mysql等)
    1、前言为什么说是伪微服务框架,常见微服务框架可能还包括服务容错、服务间的通信、服务追踪和监控、服务注册和发现等等,而我这里为了在使用中的更简单,将很多东西进行了简化或者省略了。年前到现在在开发一个新的小项目,刚好项目最初的很多功能是比较通用的,所以就想着将这些功能抽......
  • 分布式锁实现方案
    一基于Redis实现分布式锁如何基于Redis实现一个最简易的分布式锁?不论是本地锁还是分布式锁,核心都在于“互斥”。在Redis中,SETNX命令是可以帮助我们实现互斥。SETNX即SETifNoteXists(对应Java中的setIfAbsent方法),如果key不存在的话,才会设置key的值。如......
  • 长连接网关技术专题(十):百度基于Go的千万级统一长连接服务架构实践
    本文由百度技术团队分享,引用自百度Geek说,原题“千万级高性能长连接Go服务架构实践”,为了阅读便利,本文进行了排版优化等。1、引言移动互联网时代,长连接服务成为了提升应用实时性和互动性的基础服务。本文将介绍百度基于golang实现的统一长连接服务,从统一长连接功能实现和性能......
  • 从原理到实践,大咖带你拆解人工智能的神秘面纱
    在当今数字化时代,人工智能(AI)技术已经成为我们生活中不可或缺的一部分。然而,要构建出可信赖的AI系统并非易事。这需要我们不仅深入理解人工智能的核心原理,还需要将这些理论知识应用到实际场景中。为了帮助大家系统地掌握人工智能的核心原理及在实际中的应用场景,我们特别推出了如何......
  • three.js简单实践
    1.引入yarnaddthree2.vue页面引入 <divid="container"></div>import*asTHREEfrom'three'import{OrbitControls}from'three/examples/jsm/controls/OrbitControls.js'import{createMultiMaterialObject}from&......
  • 在.NET程序中整合微软的Playwright,使用 Playwright 的最佳实践和技巧
    Playwright是一个由Microsoft开发的开源工具,用于自动化Web浏览器的测试和操作。它提供了一种跨浏览器、跨平台的自动化解决方案,可以在Chromium、Firefox和WebKit(Safari)等多种浏览器上进行测试和操作。本篇随笔介绍Playwright的一些特点,以及能够完成的工作,并总结一些使用P......
  • 分布式事务解决方案详解
    1:分布式事务简介大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务(LocalTransaction)。本地事务的ACID特性是数据库直接提供支持。本地事务应用架构如下所示:但是在微服务架构中,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉......