Spring Boot 3.x + Redis 7.x,轻松掌握Redisson分布式锁实战技巧
大家好,我是袁庭新。
在分布式环境中,确保数据的一致性和正确性是至关重要的。对于需要高性能、高并发和分布式数据存储的应用程序来说,Redisson是一个很好的选择。同时,Redisson提供的分布式锁功能,在诸如互联网秒杀活动、抢优惠券操作以及接口幂等性校验等场景中发挥着重要作用,它有助于维护数据的一致性和正确性。
这篇文章将介绍Spring Boot 3.x集成Redis 7.x实现Redisson分布式锁的详解,提供了保姆级实战教程,超级详细~
1.基础环境搭建
1.创建Spring Boot项目。使用Spring Initializr方式创建一个名为redis-seckill-demo的Spring Boot项目,效果如下图所示。
2.引入相关依赖。在项目的pom.xml文件中添加Web模块中的Spring Web依赖、添加Spring Data Redis依赖启动器和Redisson依赖,以及Spring Boot单元测试依赖,示例代码如下。
4.0.0
org.springframework.boot
spring-boot-starter-parent
3.4.0
com.ytx
redis-seckill-demo
0.0.1-SNAPSHOT
redis-seckill-demo
redis-seckill-demo
21
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
org.redisson
redisson
3.40.2
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin
3.配置Redis数据库连接。这里首先需要先启动Redis服务;然后在项目的src/main/resources目录下创建application.yml全局配置文件,并在该文件中添加Redis数据库服务器的连接配置,示例代码如下。
spring:
data:
redis:
host: 192.168.230.131 # Redis服务器地址
port: 6379 # Redis服务器连接端口
password: 123456 # Redis服务器连接密码(如果Redis数据库没有设置密码,默认为空)
4.在项目的com.yx.controller包下创建一个名为
ProductFlashSaleController的控制器类。代码如下所示。
package com.ytx.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("product")
public class ProductFlashSaleController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("flash_sale")
public String flashSale() {
int phoneStock = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone_stock"));
if (phoneStock > 0) {
phoneStock--;
stringRedisTemplate.opsForValue().set("phone_stock", String.valueOf(phoneStock));
System.out.println("库存-1后,库存剩余:" + phoneStock);
} else {
System.out.println("库存不足");
}
return "当前库存:" + phoneStock;
}
}
4.在Redis数据库中添加一个名为phone_stock的键,用于存储商品的库存。
127.0.0.1:6379> set phone_stock 10
OK
2.单线程测试
使用Apache JMeter工具,在1秒内向服务器
http://192.168.200.44:8080/product/flash_sale发送100个请求,进行接口压力测试。
1.打开Apache JMeter工具,右键【Test Plan】选择【Add】-【Threads(Users)】-【Thread Group】选项,在打开的窗口中,将Number of Threads (users)选项设置为100,Ramp-up period (seconds)选项设置为1,如下图所示。
2.右键【Thread Group】选择【Add】-【Sampler】-【HTTP Request】选项,在打开的窗口中做如下图所示的配置。
3.右键【HTTP Request】选择【Add】-【Listener】-【View Results Tree】选项,打开如下图所示的窗口。
4.启动redis-seckill-demo项目,然后点击Apache JMeter窗口的Start按钮(绿色三角按钮),即执行在1秒内向
http://192.168.200.44:8080/product/flash_sale接口发送100个请求。
5.观察IDEA控制台的日志输出,发现会出现重复抢购的问题,如下图所示。该问题是由请求并发所导致的。
6.为了解决上述的请求并发问题,可以在请求处理方法flashSale()上通过synchronized关键字加锁。代码如下所示。
@RequestMapping("flash_sale")
public synchronized String flashSale() {
int phoneStock = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone_stock"));
if (phoneStock > 0) {
phoneStock--;
stringRedisTemplate.opsForValue().set("phone_stock", String.valueOf(phoneStock));
System.out.println("库存-1后,库存剩余:" + phoneStock);
} else {
System.out.println("库存不足");
}
return "当前库存:" + phoneStock;
}
7.重启redis-seckill-demo项目,再次使用Apache JMeter测试工具在1秒内向
http://192.168.200.44:8080/product/flash_sale接口发送100个请求,最后观察IDEA控制台打印输出,发现重复抢购问题不在产生。
3.Nginx下载与安装
3.1 Nginx环境配置
1.安装gcc。安装Nginx时需要先将从官网上下载下来的Nginx源码进行编译,编译依赖gcc环境,如果系统中未装有gcc则需要进行安装。
yum -y install gcc-c++
2.安装pcre和pcre-devel。PCRE(Perl Compatible Regular Expressions)是一个Perl库,包括perl兼容的正则表达式库,Nginx的HTTP模块需要使用pcre来解析正则表达式,所以需要在Linux上安装pcre库;pcre-devel是使用pcre开发的一个二次开发库,Nginx也需要此库。
yum -y install pcre pcre-devel
3.安装zlib。zlib库提供了很多种压缩和解压缩方式,Nginx使用zlib对HTTP包的内容进行gzip,所以需要安装zlib库。
yum -y install zlib zlib-devel
4.安装OpenSSL。OpenSSL 是一个强大的安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及SSL协议,并提供丰富的应用程序供测试或其它目的使用。Nginx不仅支持HTTP协议,还支持HTTPS(即在SSL协议上传输HTTP),所以需要安装OpenSSL库。
yum -y install openssl openssl-devel
3.2 Nginx下载安装
1.下载Nginx安装包。访问Nginx官网下载地址
https://nginx.org/en/download.html,在Nginx官网的【Stable version】选项下点击【nginx-1.26.2】选项即可下载Linux版的安装包,如下图所示。
2.将下载的nginx-1.26.2.tar.gz安装包上传至CentOS 7系统的/opt目录下。
scp /Users/yuanxin/Desktop/nginx-1.26.2.tar.gz root@192.168.230.130:/opt
3.直接使用tar命令解压nginx-1.26.2.tar.gz安装包。
tar -zxvf nginx-1.26.2.tar.gz
4.输入以下命令进行编译安装Nginx。
[root@localhost opt]# cd nginx-1.26.2
[root@localhost nginx-1.26.2]# ./configure
[root@localhost nginx-1.26.2]# make
[root@localhost nginx-1.26.2]# make install
5.可以通过whereis命令查找Nginx默认安装路径。
[root@localhost nginx-1.26.2]# whereis nginx
nginx: /usr/local/nginx
6.操作Nginx服务时,通常使用以下命令来启动、停止、重启或重新加载配置文件。具体命令可能会根据操作系统和服务管理工具的不同而有所变化。下面是一些常用的命令:
# 启动Nginx
./nginx
# 快速停止Nginx:这个命令会立即停止Nginx服务,但不保证保存所有相关的信息和数据
./nginx -s stop
# 完整有序的停止Nginx:这个命令会等待Nginx处理完所有正在处理的请求后再停止服务,因此是更优雅、更安全的关闭方式
./nginx -s quit
# 强制停止Nginx
kill -9 nginx
# 重载配置文件而不中断服务:直接通过Nginx的安装路径发送信号
./nginx -s reload
# 检查配置文件语法:在修改配置文件后,可以使用以下命令检查语法是否正确
./nginx -t
# 使用ps命令查找Nginx进程:如果Nginx已经成功关闭,将不会显示任何Nginx相关的进程
ps aux | grep nginx
7.启动Nginx成功后,我们就可以使用浏览器访问Nginx服务了(Nginx默认端口号是80),访问http://192.168.230.130:80地址,如果出现下图所示的页面,则说明Nginx启动成功。
4.高并发测试
4.1 Nginx配置
1.进入Nginx安装目录,编辑nginx.conf配置文件。
[root@localhost user]# cd /usr/local/nginx/conf
[root@localhost conf]# vim nginx.conf
2.在nginx.conf文件中添加如下的配置做负载均衡,具体配置信息见下。
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream ytx {
server 192.168.200.44:8080; # 3.项目运行对应主机的IP地址
server 192.168.200.44:8081;
}
server {
listen 80; # 1.监听Nginx安装所在主机的80端口的请求
server_name localhost;
location / {
proxy_pass http://ytx; # 2.Nginx安装所在主机80端口监听到的请求会给到代理类ytx
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
3.关闭Nginx服务器的防火墙。
# 1.查看防火墙运行状态。
systemctl status firewalld
# 2.关闭防火墙
systemctl stop firewalld
# 3.开机禁用防火墙
systemctl disable firewalld
4.启动Nginx服务器。
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
4.2 高并发测试
1.在IDEA窗口的右上角点击【Edit Configurations...】选项,如下图所示。
2.在打开的【Run/Debug Configurations】窗口中,先选中窗口左侧的【
RedisSeckillDemoApplication】启动类,接着点击左上角的复制按钮,然后在右侧的窗口中将复制出来的启动类命名为【
RedisSeckillDemoApplication2】,如下图所示。
3.打开上图后,我们会发现IDEA默认没有展示【VM options】和【Program arguments】选项,我们可以在上图中点击【Modify options】选项,然后在打开的新窗口中选择【Add VM options】和【Program arguments】选项。如下图所示。
4.在上面的窗口中,手动配置完毕后如下图所示。然后在【
RedisSeckillDemoApplication2】启动类的【VM options】选项中填写一个值"-Dserver.port=8081"。最后,我们在【Run/Debug Configurations】窗口中点击【Apply】按钮即可。
5.启动两台Tomcat服务。在IDEA窗口的右上角点击绿色三角按钮,分别启动这两个主类。
6.使用Nginx做负载均衡。通过测试工具JMeter在1秒内向Nginx服务器上的
http://192.168.230.130:80/product/flash_sale接口发送100个请求,配置信息如下图所示(修改IP和Port参数的值)。
7.查看IDEA控制台的日志打印输出,发现有重复抢购商品的问题,如下图所示。
5.分布式锁
5.1 分布式锁介绍
什么是分布式锁?分布式锁是指在分布式系统或集群环境下,能够实现多进程间互斥访问的一种锁机制,这种锁对于所有参与分布式系统的进程都是可见的。分布式锁的关键特性包括多进程可见性、互斥性、高可用性、高性能以及安全性。
关于如何实现分布式锁,其核心在于确保多进程间的互斥访问。为实现这一目标,有多种常见解决方案,以下列举三种主要方法。
特性 | MySQL | Zookeeper | Redis |
互斥 | 利用MySQL本身的互斥锁机制 | 利用节点的唯一性和有序性实现互斥 | 利用SETNX这样的互斥命令 |
高可用 | 好 | 好 | 好 |
高性能 | 一般 | 一般 | 好 |
安全性 | 断开连接,自动释放锁 | 临时节点,断开连接自动释放 | 利用锁超时时间,到期释放 |
在分布式系统中,实现分布式锁有多种方式,以下是三种常见的技术手段及其特点:
- MySQL:虽然MySQL具备自身的锁机制(如行级锁、表级锁),但其设计初衷并非针对分布式锁场景。由于数据库的读写操作相对内存操作较为缓慢,性能上难以满足高并发需求,因此,在实践中较少直接使用MySQL作为分布式锁的解决方案。
- Zookeeper:Zookeeper是一个专为分布式应用协调服务设计的工具,广泛应用于企业级开发中的分布式锁实现。它通过创建临时顺序节点来管理锁的获取和释放,确保了分布式环境中多个客户端之间的同步。尽管本教程不会深入讲解Zookeeper的工作原理及其实现分布式锁的具体方法,但在需要强一致性保证的场景下,Zookeeper是值得考虑的选择。
- Redis:Redis因其实现简单、性能高效而成为目前最流行的分布式锁解决方案之一。它利用SETNX(即Set if Not Exists)命令尝试设置一个键值对,只有当该键不存在时才会成功,以此逻辑判断是否获得了锁。同时结合EXPIRE或PX参数设定锁的有效期,防止死锁的发生。
综上所述,在选择分布式锁的实现方式时,应根据具体应用场景的需求权衡不同方案的优劣。对于追求速度和简易性的项目,Redis通常是首选;而对于那些对一致性和复杂协调有更高要求的应用,则可能更倾向于采用Zookeeper。
5.2 分布式锁设计
如果要求你利用Redis来设计并实现一个分布式锁,你的实现思路会是什么?,其核心在于确保锁的原子性、防止死锁以及应对高并发场景。以下是基于Redis实现分布式锁的思路,可总结为以下几个步骤:
1.Redis的单线程模型保证了其命令的原子性,这为分布式锁的实现提供了基础。我们可以使用SETNX命令(当且仅当键不存在时设置键的值)来尝试获取锁。
- 如果请求的key不存在,保存key(当前线程加锁),当业务执行完成后,删除key(表示释放锁)。
- 如果key已存在,表示锁已被其他线程持有,当前线程应进入等待或阻塞状态。
2.为了避免因异常或其他原因导致锁无法释放(死锁),可以为锁设置一个合理的过期时间。这样,即使持有锁的线程崩溃或未能显式释放锁,锁也会在过期后自动释放。在加锁成功的同时,使用EXPIRE命令为锁设置一个过期时间(如10秒)。
3.在高并发场景下,锁的持有时间可能因任务执行时间的不确定性而难以预估。简单的过期时间设置可能导致锁在任务完成前被误释放。
- 第一个线程,执行需要13秒。对key加锁并设置过期时间为10秒,执行到第10秒时,Redis的key自动过期了(释放锁)。
- 第二个线程,执行需要7秒。对key加锁并设置过期时间为10秒,执行到第3秒的时候,发现当前锁被释放了,为什么呢?是因为第一个线程执行完成,然后将锁释放了。
- 这就导致了连锁反应。当前线程刚加的锁,就被其他线程释放掉了,周而复始,导致锁会永久失效。
4.解决高并发下的锁失效问题。
- UUID唯一标识:为每个线程生成一个唯一的UUID作为锁的标识符,并在加锁时将其与锁的值关联。
- 释放锁时验证UUID:在释放锁时,检查当前线程的UUID是否与锁的值匹配。如果匹配,则删除锁;否则,不执行删除操作。
5.为了平衡锁的持有时间和资源利用效率,可以引入动态调整过期时间的机制。
- 定时器线程:启动一个定时器线程,定期检查锁的剩余过期时间。
- 延长过期时间:当锁的剩余过期时间低于某个阈值(如总过期时间的1/3)时,延长锁的过期时间。
通过上述步骤和策略,尽管手动实现分布式锁是可行的,但过程复杂且容易出错。因此,推荐使用现成的分布式锁框架,如Redisson。
6.Redission
Redission官网地址:https://redisson.org。
Redission的GitHub地址:
https://github.com/redisson/redisson。
6.1 Redission介绍
Redisson是一个基于Redis的Java客户端库,它提供了多种分布式数据结构和服务,用于构建高性能的并发应用程序。以下是对Redisson的详细介绍。
6.1.1 基本概述
Redisson是一个基于Redis的Java驻内存数据网格(In-Memory Data Grid),它不仅提供了对Redis的全面支持,还实现了许多高级功能和服务。通过将Redis与Java实用工具包中常用接口相结合,Redisson为开发者提供了一系列具有分布式特性的工具类和对象。这使得原本用于协调单机多线程并发程序的工具获得了在分布式环境中协调多机多线程并发系统的能力。
为了实现高效的网络通信,Redisson使用了非阻塞I/O框架Netty作为底层通信层。Netty不仅提供了强大的异步事件处理能力,还简化了网络编程模型,使Redisson能够轻松应对大规模并发请求。同时,Redisson支持多种Redis配置形式的连接,包括单节点、主从复制、哨兵模式以及集群模式等。
6.1.2 主要特性
分布式数据结构:Redisson支持多种分布式数据结构,如Bloom filters、Bit sets、Counting Bloom filters、Geospatial indexes、Hashes、HyperLogLogs、Lists、Maps、Nx sets、Read/Write locks、Scored sets、Sets、Sorted sets等。
分布式锁:Redisson提供了多种分布式锁实现,包括公平锁、联锁、红锁(RedLock)、可重入锁、读写锁等,用于确保分布式环境下的并发操作的正确性和一致性。
分布式服务:Redisson提供了Executor service、Scheduled executor service、Atomic long、Atomic double等分布式服务,用于构建分布式系统中的服务。
连接池管理:Redisson内置了连接池管理,优化了Redis连接的创建和销毁。
Lua脚本执行:支持执行Lua脚本来实现复杂的原子操作。
序列化和反序列化:支持自定义序列化和反序列化机制,以适应不同的数据存储需求。
事件监听:提供了事件监听机制,可以监听Redisson对象的变更事件。
Redis集群支持:Redisson支持Redis集群模式,能够自动处理节点故障和数据迁移。
灵活的配置选项:Redisson提供了灵活的配置选项,可以根据应用需求调整其行为。
6.1.3 使用场景
Redisson适用于需要高性能、高并发和分布式数据存储的应用程序。它的API设计简洁,易于使用,可以大大简化并发编程的复杂性。同时,Redisson也支持多种Java并发API,如CompletableFuture,使得异步编程更加方便。具体使用场景包括:
- 分布式锁:在分布式环境中实现互斥访问,确保数据的一致性和正确性。
- 缓存:利用Redis的高速缓存能力,提高应用程序的数据访问速度。
- 会话存储:在Web应用中存储用户会话信息,如登录状态、购物车内容等。
- 排行榜和计数器:实现实时排行榜、点赞数、访问计数等功能。
- 消息队列:作为消息队列系统,处理异步任务。
- 实时分析:用于用户行为分析、实时统计信息等。
- 发布/订阅:实现消息的发布和订阅,用于异步通信和事件驱动架构。
- 分布式限流:基于漏桶算法和令牌桶算法实现分布式限流,限制系统的访问速率。
- 数据共享:在微服务架构中,作为服务间共享数据的媒介。
综上所述,Redisson是一个功能强大、易于使用的Redis Java客户端库,它提供了丰富的分布式数据结构和服务,适用于各种高性能、高并发和分布式数据存储的应用程序场景。
6.2 Redisson使用
1.在项目的pom.xml文件中,添加Redisson的依赖。
org.redisson
redisson
3.40.2
2.在项目的com.ytx.config包下创建一个名为RedissonConfig的配置类,用于初始化RedissonClient客户端。
package com.ytx.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
/** Redis单机模式 */
@Bean
public RedissonClient singletonModeRedisson() {
// 创建Config实例(配置类)
Config config = new Config();
// 设置单机模式下的服务器地址
config.useSingleServer()
// 设置Redis服务器地址
.setAddress("redis://192.168.230.131:6379")
// 设置Redis服务器密码
.setPassword("123456")
// 设置Redis数据库索引,默认为0
.setDatabase(0);
// 创建RedissonClient实例
return Redisson.create(config);
}
/** Redis主从模式 */
/*
@Bean
public RedissonClient masterSlaveModeRedisson() {
Config config = new Config();
// 设置主从模式下的服务器地址
config.useMasterSlaveServers()
// 主服务器IP
.setMasterAddress("redis://192.168.230.131:6379")
// 从服务器IP
.addSlaveAddress("redis://192.168.230.132:6379", "redis://192.168.230.133:6379")
.setPassword("123456");
return Redisson.create(config);
}
*/
/** Redis哨兵模式 */
/*
@Bean
public RedissonClient sentinelModeRedisson() {
Config config = new Config();
// 设置哨兵模式下的服务器地址
config.useSentinelServers()
.setMasterName("myMaster")
.addSentinelAddress(
"redis://192.168.230.131:6379",
"redis://192.168.230.132:6379",
"redis://192.168.230.133:6379")
.setPassword("123456");
return Redisson.create(config);
}
*/
/** Redis集群模式 */
/*
@Bean
public RedissonClient clusterModeRedisson() {
Config config = new Config();
// 设置集群模式下的服务器地址
config.useClusterServers()
.addNodeAddress(
"redis://192.168.230.131:6379",
"redis://192.168.230.132:6379",
"redis://192.168.230.133:6379")
.setPassword("123456")
// 设置集群状态扫描间隔时间(单位为毫秒)
.setScanInterval(2000);
return Redisson.create(config);
}
*/
}
3.重构
ProductFlashSaleController类中的业务代码,通过Redisson来实现分布式锁,代码如下所示。
package com.ytx.controller;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("product")
public class ProductFlashSaleController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
@RequestMapping("flash_sale")
public String flashSale() {
// 1.获取锁对象
RLock redissonClientLock =redissonClient.getLock("phone_lock");
// 2.加锁
redissonClientLock.lock(30, TimeUnit.SECONDS);
int phoneStock = 0;
try {
// 3.执行业务逻辑
phoneStock = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone_stock"));
if (phoneStock > 0) {
phoneStock--;
stringRedisTemplate.opsForValue().set("phone_stock", String.valueOf(phoneStock));
System.out.println("库存-1后,库存剩余:" + phoneStock);
} else {
System.out.println("库存不足");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 4.释放锁
redissonClientLock.unlock();
}
return "当前库存:" + phoneStock;
}
}
4.启动两台Tomcat服务。在IDEA窗口的右上角点击绿色三角按钮,分别启动这两个主类。
5.使用Nginx做负载均衡。通过测试工具JMeter在1秒内向Nginx服务器上的
http://192.168.230.130:80/product/flash_sale接口发送100个请求,配置信息如下图所示。
6.查看IDEA控制台的日志打印输出,如下图所示。
通过上图IDEA控制台中的打印输出可以看到,发现之前出现的重复抢购的问题将不在产生。说明Redisson分布式锁生效了。
7.总结
本文介绍了分布式锁的应用场景、实现方式以及使用Redisson框架的具体实践。文章首先通过创建Spring Boot项目和配置Redis数据库连接,展示了基础环境的搭建。接着,通过Apache JMeter进行单线程测试,发现并发请求导致重复抢购问题。为了解决这一问题,文章介绍了使用synchronized关键字加锁的方法。随后,文章详细介绍了Nginx的安装与配置,并使用Nginx进行高并发测试,发现仍然存在重复抢购问题。文章进一步探讨了分布式锁的概念和实现方式,重点介绍了Redis作为分布式锁的解决方案,并详细阐述了使用Redisson框架实现分布式锁的步骤。最后,通过重构代码,使用Redisson分布式锁解决了重复抢购问题,验证了分布式锁的有效性。