使用 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,工程结构如下图所示:
首先查看一下 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