使用 Redisson 框架基于 Redis 实现分布式锁

分布式锁可以采用数据库、zookeeper、redis 三种方式实现。

采用数据库实现方式,主要采用表字段的唯一索引特性。数据库是非常昂贵的资源,非常不推荐,最致命就是性能,不要去增加不必要的负担。

采用 zookeeper 的实现方式,主要使用其为客户端创建临时有序节点的特性,在我之前的博客有介绍。虽然使用 Apache Curator 客户端框架可以简化操作,但是其底层实现比较复杂,总体而言性能相对较差,因为需要维护大量的 Zookeeper 状态,引起大量网络 IO 的开销。

采用 redis 的实现方式,主要采用其单线程执行相关命令的特性。实现原理非常简单,性能也是最好,尤其采用 Redisson 实现方案。

Redisson 是基于 Redis 实现的一个框架。充分的利用了 Redis 键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包,获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度,简化了分布式环境中程序相互之间的协作。Redisson已经内置提供了基于Redis的分布式锁实现,此种方式是我们推荐的分布式锁使用方式。

本篇博客介绍 redis 的两种分布式锁的实现方式:自己编码实现方式和采用 redisson 框架的实现方式,在博客的最后会提供源代码下载。

Redisson 的官网访问地址为:https://redisson.org


一、搭建工程

新建一个 springboot 工程,取名为 springboot_redisson,工程结构如下图所示:

image

首先查看一下 pom 文件的内容,主要是引入了 redis 和 redisson 的 starter 依赖包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.jobs</groupId>
    <artifactId>springboot_redisson</artifactId>
    <version>1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--引入 redis 的 starter 依赖包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--引入 redisson 的 starter 依赖包-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.30.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build>
</project>

然后查看一下 application.yml 的配置内容,主要是 redis 的连接信息配置

server:
  port: 8888
spring:
  redis:
    host: 192.168.136.128
    port: 6379
    password: root
    jedis:
      pool:
        # 最大连接数
        max-active: 10
        # 最大空闲连接数
        max-idle: 5
        # 最小空闲
        min-idle: 1
        # 连接超时时间(毫秒)
        max-wait: 3000

二、代码细节

如果使用 RedisTemplate 操作 redis 的话,需要编写一个配置类,主要是改变一下 key 的序列化方式,方便明文查看

package com.jobs.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        //默认的Key序列化器为:JdkSerializationRedisSerializer
        //这里只是将 key 采用 string 序列化,方便查看
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }
}

最后就是编写一个 controller 类,里面有 redis 和 redisson 两种实现分布式锁的方案

package com.jobs.controller;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/lock")
public class LockController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 使用 redis 的两个命令实现分布式加锁和解锁
     * 加锁:set lock_key lock_value NX PX 3000
     * 解锁:del lock_key
     * 该分布式锁方案,是由我们自己编写代码实现,可以简单的实现分布式锁的特性,
     * 主要的不足时:这里是非阻塞锁,没有重试机制,获取不到锁后,直接返回失败,不太符合大多数应用场景
     */
    @GetMapping("/redistest")
    public String stock1() {
        String result;
        //获取当前线程的id
        String threadId = String.valueOf(Thread.currentThread().getId());
        //尝试加锁,如果获取锁成功,则返回 true
        //为了防止死锁,获取到锁之后,给锁设置了一个 3 秒的有效期,过期自动释放锁
        //key 可以随便定义,value 是线程id
        Boolean locked = redisTemplate.opsForValue().setIfAbsent("mylock", threadId, 3, TimeUnit.SECONDS);
        if (locked) {
            try {
                //由于是 demo,这里就不从数据库中获取库存量了,以 redis 代替数据库获取库存量
                String temp = redisTemplate.opsForValue().get("stock");
                if (StringUtils.hasText(temp)) {
                    int stock = Integer.parseInt(temp);
                    if (stock > 0) {
                        stock--;
                        redisTemplate.opsForValue().set("stock", String.valueOf(stock));
                        result = "库存量扣减成功,剩余库存量:" + stock;
                        System.out.println(result);
                    } else {
                        result = "库存不足!!!";
                        System.out.println(result);
                    }
                } else {
                    result = "请提前在 redis 中设置好 stock 库存量的值";
                    System.out.println(result);
                }
            } catch (Exception ex) {
                result = ex.getMessage();
                System.out.println(result);
            } finally {
                //对比 redis 中的 value 值,如果是当前线程 id 才可以进行解锁
                String myValue = redisTemplate.opsForValue().get("mylock");
                if (threadId.equals(myValue)) {
                    redisTemplate.delete("mylock");
                }
            }
        } else {
            result = "没有获取到锁,不能扣减库存量!!!";
            System.out.println(result);
        }

        return result;
    }

    //--------------------------------------------------

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 使用 Redission 框架实现分布式锁功能,具有阻塞重试的特性,非常适合绝大多数应用场景。
     */
    @GetMapping("/redissontest")
    public String stock2() {
        String result;
        //获得分布式锁对象,这里还没有尝试去获取锁
        RLock lock = redissonClient.getLock("mylock");

        //尝试获取锁,如果获取成功,则后续程序继续执行;如果获取不成功则阻塞等待
        //如果获取锁成功,则锁的有效期是 3 秒,超时后自动解锁
        lock.lock(3, TimeUnit.SECONDS);

        try {
            //由于是 demo,这里就不从数据库中获取库存量了,以 redis 代替数据库获取库存量
            String temp = redisTemplate.opsForValue().get("stock");
            if (StringUtils.hasText(temp)) {
                int stock = Integer.parseInt(temp);
                if (stock > 0) {
                    stock--;
                    redisTemplate.opsForValue().set("stock", String.valueOf(stock));
                    result = "库存量扣减成功,剩余库存量:" + stock;
                    System.out.println(result);
                } else {
                    result = "库存不足!!!";
                    System.out.println(result);
                }
            } else {
                result = "请提前在 redis 中设置好 stock 库存量的值";
                System.out.println(result);
            }
        } catch (Exception ex) {
            result = ex.getMessage();
            System.out.println(result);
        } finally {
            //解锁
            lock.unlock();
        }

        return result;
    }
}

代码已经编写完毕,注释也比较详细,应该很容易理解。可以打包后部署多份,采用 nginx 进行负载均衡转发,然后采用 Jmeter 等压力测试工具,模拟出多线程进行访问请求,测试分布式锁的实现结果。这里就不进行展示了。


本篇博客的源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/springboot_redisson.zip

posted @ 2024-05-11 19:39  乔京飞  阅读(3385)  评论(0编辑  收藏  举报