前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从0到1带你实现一个分布式锁组件

从0到1带你实现一个分布式锁组件

原创
作者头像
第七人格
发布2024-07-01 13:40:33
780
发布2024-07-01 13:40:33
举报
文章被收录于专栏:中间件开发

✨这里是第七人格的博客✨小七,欢迎您的到来~✨

🍅系列专栏:中间件开发🍅

✈️本篇内容: 分布式锁组件✈️

🍱 本篇收录完整代码地址:https://gitee.com/diqirenge/seven-lock-starter 🍱

楔子

几年以前小七在面试的时候,和面试官探讨过一个重复造轮子的事情。比如今天的主角——分布式锁,市面上有很多成熟的解决方案,包括框架、组件等,其中Redisson关于锁的实现也非常的完美了。今天我们就基于Redisson封装一个我们自己的分布式锁starter组件。

需求背景

公司关于分布式锁的实现五花八门,虽然都是基于redis实现的,但是有的项目用的Redisson,有的项目用的RedisTemplate,有的项目两个都在混用,

且使用方式都是直接调用的框架的API,有些小伙伴面对各种各样的分布式锁实现方式,一脸懵逼,不知道如何下手,甚至使用错误的情况也不在少数。

为了解决这个问题,我们计划统一技术栈,使用Redisson实现分布式锁,并提供一个基于Redisson实现的分布式锁的框架封装。

方案设计

为了方便各个系统更好的接入新的分布式锁组件,我们计划如下:

1、基于Spring boot starter封装,接入方只需要引入依赖,替换调用方法即可

2、尽量少的配置改动,如果项目以前有使用Redisson,甚至不修改配置,也可以使用

兼容方案如图所示:

开发环境

名称

版本

JDK

1.8

Redis

7.2.0

redisson-spring-boot-starter

3.15.4

spring-boot-configuration-processor

2.3.5.RELEASE

spring-boot-autoconfigure-processor

2.3.5.RELEASE

分支名称

master

分支地址

https://gitee.com/diqirenge/seven-lock-starter

代码实现

1、添加Maven依赖

代码语言:xml
复制
<dependencies>
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.15.4</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <version>2.3.5.RELEASE</version>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure-processor</artifactId>
        <version>2.3.5.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>

2、新建一个类DistributedLockProperties,用于存储和获取分布式锁的超时时间配置。

代码语言:java
复制
/**
 * <pre>
 * 这个类是一个简单的Java Bean,主要用于存储和获取分布式锁的超时时间配置,也是给调用方一个修改默认加锁时间的机会。
 * 通过@ConfigurationProperties注解,Spring Boot可以自动将配置文件(如application.properties或application.yml)中前缀为"com.run2code.starter"的属性绑定到这个类的字段上。
 * 例如,如果在配置文件中有一个属性com.run2code.starter.lock-timeout=60,那么这个值就会自动设置到lockTimeout字段上。
 * <pre/>
 *
 * 关注公众号【奔跑的码畜】,一起进步不迷路
 *
 * @author 第七人格
 * @date 2024/06/17
 */
// 使用@ConfigurationProperties注解,这允许我们将配置文件中的属性绑定到这个类的字段上。
// prefix属性定义了配置文件中属性的前缀,即"com.run2code.starter"。
@ConfigurationProperties(
        prefix = "com.run2code.starter"
)
public class DistributedLockProperties {
    // 定义一个长整型字段lockTimeout,用来存储锁的超时时间,单位可能是秒或者其他时间单位,
    // 默认值被设置为30L。
    private long lockTimeout = 30L;

    // 类的默认构造函数,没有参数,也没有执行任何操作。
    public DistributedLockProperties() {
    }

    // 这是一个getter方法,用于获取lockTimeout字段的值。
    public long getLockTimeout() {
        return this.lockTimeout;
    }

    // 这是一个setter方法,用于设置lockTimeout字段的值。
    // 方法接受一个长整型参数lockTimeout,并将其值赋给类的lockTimeout字段。
    public void setLockTimeout(final long lockTimeout) {
        this.lockTimeout = lockTimeout;
    }
}

3、编写DistributedLockRedissonAutoConfiguration,该类继承了Redisson的相关配置,并且引入了上面我们自己的配置

代码语言:java
复制
/**
 * <pre>
 *     这个配置类的主要目的是自定义RedissonClient的配置,特别是锁的看门狗超时时间。
 *     同时,它也确保了只有在Redisson.class和RedisOperations.class存在于类路径中,且RedissonAutoConfiguration已经完成配置后,这个配置类才会被加载和配置。
 * <pre/>
 *
 * 关注公众号【奔跑的码畜】,一起进步不迷路
 *
 * @author 第七人格
 * @date 2024/06/17
 */
// 使用@Configuration注解,表示这是一个Spring Boot的配置类,用于定义Bean和配置信息
@Configuration
// @ConditionalOnClass注解表示仅当类路径中存在Redisson.class和RedisOperations.class时,此配置才会生效
@ConditionalOnClass({Redisson.class, RedisOperations.class})
// @AutoConfigureAfter注解表示此配置类应在RedissonAutoConfiguration配置类加载并配置完成后才进行加载配置
@AutoConfigureAfter({RedissonAutoConfiguration.class})
// @EnableConfigurationProperties注解启用DistributedLockProperties类的配置属性绑定
@EnableConfigurationProperties({DistributedLockProperties.class})
// @ComponentScan注解用于扫描并加载指定包下的组件,这里指定扫描包含DistributedLock.class的包
@ComponentScan(
        basePackageClasses = {DistributedLock.class}
)
public class DistributedLockRedissonAutoConfiguration {

    // 定义一个私有变量distributedLockProperties,其类型为DistributedLockProperties
    private final DistributedLockProperties distributedLockProperties;

    // 使用@Primary注解表示当存在多个同类型的Bean时,优先使用该Bean
    // 使用@Bean注解定义一个名为redissonClient的Bean,其类型为RedissonClient
    // destroyMethod属性指定在Bean销毁时调用的方法,这里指定为"shutdown"
    // @ConditionalOnBean注解表示仅当存在RedissonClient类型的Bean时,此Bean定义才会生效
    @Primary
    @Bean(
            name = {"redissonClient"},
            destroyMethod = "shutdown"
    )
    @ConditionalOnBean({RedissonClient.class})
    public RedissonClient redissonClient(RedissonClient redissonClient) {

        // 从传入的redissonClient中获取其配置
        Config config = redissonClient.getConfig();

        // 设置锁的看门狗超时时间,单位为毫秒,这里将distributedLockProperties中获取的锁超时时间(秒)转为毫秒
        config.setLockWatchdogTimeout(this.distributedLockProperties.getLockTimeout() * 1000L);

        // 关闭传入的redissonClient
        redissonClient.shutdown();

        // 使用更新后的配置创建一个新的RedissonClient实例并返回
        return Redisson.create(config);
    }

    // 构造函数,接收一个DistributedLockProperties类型的参数,用于初始化distributedLockProperties变量
    public DistributedLockRedissonAutoConfiguration(final DistributedLockProperties distributedLockProperties) {
        this.distributedLockProperties = distributedLockProperties;
    }
}

4、定义分布式锁接口

代码语言:java
复制
/**
 * 分布式锁接口
 * 关注公众号【奔跑的码畜】,一起进步不迷路
 *
 * @author 第七人格
 * @date 2024/06/17
 */
public interface DistributedLock {
    /**
     * 根据key加锁,默认锁的过期时间是30秒
     *
     * @param key 加锁key
     * @return boolean
     */
    boolean lock(String key);

    /**
     *  根据key加锁,并且设置等待时间和锁的过期时间
     *
     * @param key 加锁key
     * @param waitTime 等待时间
     * @param leaseTime 过期时间
     * @return boolean
     */
    boolean lock(String key, int waitTime, int leaseTime);

    /**
     * 根据key释放锁
     *
     * @param key 加锁key
     */
    void unlock(String key);
}

5、使用Redisson实现分布式锁接口

代码语言:java
复制
/**
 * Redisson分布式锁实现
 * 关注公众号【奔跑的码畜】,一起进步不迷路
 *
 * @author 第七人格
 * @date 2024/06/17
 */
// 使用@Component注解,表明这是一个Spring组件,Spring会自动检测并实例化这个类
@Component
// 使用@ConditionalOnClass注解,表明这个组件的创建依赖于Redisson.class和RedisOperations.class是否存在于类路径中
@ConditionalOnClass({Redisson.class, RedisOperations.class})
public class RedissonDistributedLock implements DistributedLock {
    // 创建一个Logger对象,用于记录日志,这个对象是针对RedissonDistributedLock类的
    private static final Logger log = LoggerFactory.getLogger(RedissonDistributedLock.class);
    // 定义一个RedissonClient类型的成员变量,这个变量将用于与Redis进行交互
    private final RedissonClient redissonClient;

    // 定义一个lock方法,接收一个String类型的key作为参数,尝试获取对应的锁,并立即返回获取结果
    public boolean lock(String key) {
        // 使用redissonClient获取对应的锁,并尝试加锁,如果加锁成功则返回true,否则返回false
        return this.redissonClient.getLock(key).tryLock();
    }

    /**
     * 接收key、等待时间和租约时间作为参数,尝试在指定的等待时间内获取锁,并在获取锁后设置指定的租约时间
     *
     * @param key 加锁key
     * @param waitTime 等待时间
     * @param leaseTime 过期时间
     * @return boolean
     */
    public boolean lock(String key, int waitTime, int leaseTime) {
        try {
            // 使用redissonClient获取对应的锁,尝试在指定的等待时间内加锁,并设置租约时间
            // 如果在等待时间内成功获取锁,则返回true,否则返回false
            return this.redissonClient.getLock(key).tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            // 如果在等待过程中线程被中断,则记录错误信息,并返回false表示获取锁失败
            log.error("获取锁失败 - {} - {}", new Object[]{key, waitTime, e});
            return false;
        }
    }

    /**
     * 接收一个String类型的key作为参数,用于释放对应的锁
     *
     * @param key 加锁key
     */
    public void unlock(String key) {
        try {
            // 使用redissonClient获取对应的锁,并释放它
            this.redissonClient.getLock(key).unlock();
        } catch (Exception e) {
            // 如果在释放锁的过程中出现异常,则抛出一个DistributedLockException异常,将原始的异常作为其原因
            throw new DistributedLockException(key, e);
        }
    }

    // 构造函数,接收一个RedissonClient对象作为参数,并将其赋值给成员变量redissonClient
    public RedissonDistributedLock(final RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }
}

6、自定义分布式锁异常类

代码语言:java
复制
/**
 * <pre>
 *     这段代码定义了一个自定义的运行时异常类DistributedLockException,它扩展了Java标准库中的RuntimeException类。
 *     这个自定义异常类主要用于处理与分布式锁相关的异常情况。
 *     通过提供一个带有错误信息和原始异常原因的构造方法,它使得在抛出异常时能够提供更详细的上下文信息,便于问题的定位和解决。
 * <pre/>
 * 关注公众号【奔跑的码畜】,一起进步不迷路
 *
 * @author 第七人格
 * @date 2024/06/17
 */
// 声明一个名为DistributedLockException的公开类,它继承自RuntimeException
// RuntimeException是Java中表示运行时异常的一个类,通常用于程序逻辑错误或其他异常情况
public class DistributedLockException extends RuntimeException {

    // 定义一个构造方法,接收两个参数:一个字符串类型的message和一个Throwable类型的cause
    // message通常用于提供关于异常的描述信息
    // cause表示引发当前异常的原始异常,允许将异常链传递下去,便于后续的错误追踪和分析
    public DistributedLockException(String message, Throwable cause) {

        // 调用父类RuntimeException的构造方法,将message和cause传递给父类的构造器
        // 这样做是为了初始化RuntimeException对象,同时保留了原始异常的堆栈跟踪信息
        super(message, cause);
    }
}

7、在spring.factories中指明组件自动配置类

代码语言:properties
复制
# 在Spring Boot项目中,spring.factories文件是一个特殊的配置文件,用于配置Spring Boot的自动配置和其他Spring Boot特性。
# 这行代码的意义在于,它告诉Spring Boot在启动过程中自动加载并应用DistributedLockRedissonAutoConfiguration这个配置类。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.run2code.starter.lock.config.DistributedLockRedissonAutoConfiguration

8、执行Maven打包/上传命令

测试工程代码实现

1、引入依赖

代码语言:xml
复制
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.5.RELEASE</version>
    <relativePath/>
</parent>
代码语言:xml
复制
<dependencies>
    <dependency>
        <groupId>com.run2code.starter</groupId>
        <artifactId>seven-lock-starter</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2、加入配置

代码语言:properties
复制
spring.redis.host=localhost
#spring.redis.password=
spring.redis.port=16379
spring.redis.database=5

# com.run2code.starter.lock-timeout=10

3、编写测试用的Controller

代码语言:java
复制
@RestController
public class UserController {
    private Logger logger = LoggerFactory.getLogger(UserController.class);
    
    @Resource
    private DistributedLock distributedLock;

    @GetMapping(path = "/query")
    public String queryUserInfo(@RequestParam String userId) {
        logger.info("查询用户信息,userId:{}", userId);
        boolean hasLocked = distributedLock.lock("lockKey");
        if (!hasLocked) {
            // 没有获得锁的处理
            System.out.println("请求过于频繁,请稍候重试");
            return "请求过于频繁,请稍候重试";
        }
        try {
            // 获得锁后的操作
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            distributedLock.unlock("lockKey");
        }
        System.out.println("查询用户信息成功");
        return userId;
    }

}

4、编写启动类

代码语言:java
复制
@SpringBootApplication
public class ApplicationLock {

    public static void main(String[] args) {
        SpringApplication.run(ApplicationLock.class, args);
    }
}

5、测试

启动项目后先请求http://localhost:8080/query?userId=123,5秒内再次请求http://localhost:8080/query?userId=123,返回

请求过于频繁,请稍候重试

总结

本文通过详细的步骤和代码示例,展示了如何从零开始实现一个基于Redisson的分布式锁组件,并提供了封装和测试方案。读者可以直接用于生产。

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 楔子
  • 需求背景
  • 方案设计
  • 开发环境
  • 分支名称
  • 分支地址
  • 代码实现
  • 测试工程代码实现
  • 总结
相关产品与服务
云数据库 Redis®
腾讯云数据库 Redis®(TencentDB for Redis®)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档