SpringBoot集成redis(3)|(Redisson方式实现分布式锁)
章节
前言
本章节主要介绍SpringBoot项目集成Redis的一些相关知识,主要介绍的是基于Springboot的Redisson方式,实现在分布式场景下锁机制。
一、Redisson是什么?
Java 开程序中集成 Redis,必须使用 Redis 的第三方库。而 Redisson 就是用于在 Java 程序中操作 Redis 的库,它使得我们可以在程序中轻松地使用 Redis。Redisson 在 java.util 中常用接口的基础上,为我们提供了一系列具有分布式特性的工具类。
Redisson底层采用的是Netty 框架。支持Redis 2.8以上版本,支持Java1.6+以上版本
二、集成步骤
1.依赖引入
pom依赖如下,主要列出SpringBoot依赖版本以及Redis版本,其他需要依赖自行添加
<!-- Springboot 版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.7</version>
<relativePath/>
</parent>
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
<!--redis分布式锁redisson需要-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.6</version>
</dependency>
<!--springboot config配置需要-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2.文件配置
yml配置,配置了单机模式、哨兵模式、集群模式
# redisson配置
redisson:
redis:
password: redispassword
timeout: 30000
database: 1
#sentinel/cluster/single 配置类型
mode: single
#连接池配置
pool:
max-idle: 16
min-idle: 8
max-active: 8
max-wait: 3000
conn-timeout: 3000
so-timeout: 3000
size: 10
#单机配置
single:
address: 127.0.0.1:6482
#集群配置
cluster:
scan-interval: 1000
nodes: 127.0.0.1:6482
read-mode: SLAVE
retry-attempts: 3
failed-attempts: 3
slave-connection-pool-size: 64
master-connection-pool-size: 64
retry-interval: 1500
#哨兵配置
sentinel:
master: business-master
nodes: 127.0.0.1:6482
master-onlyWrite: true
fail-max: 3
3.配置类
RedisProperties配置类集合包含以下几种类型
RedisSingleProperties 单机版配置
RedisClusterProperties集群配置
RedisPoolProperties连接池配置
RedisSentinelProperties哨兵配置
import lombok.Data;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Created by Oak on 2022/10/8.
* Description:
*/
@ConfigurationProperties(prefix = "redisson.redis", ignoreUnknownFields = true)
@Data
@ToString
public class RedisProperties {
private int database;
/**
* 等待节点回复命令的时间。该时间从命令发送成功时开始计时
*/
private int timeout;
private String password;
private String mode;
/**
* 池配置
*/
private RedisPoolProperties pool;
/**
* 单机信息配置
*/
private RedisSingleProperties single;
/**
* 集群 信息配置
*/
private RedisClusterProperties cluster;
/**
* 哨兵配置
*/
private RedisSentinelProperties sentinel;
}
import lombok.Data;
import lombok.ToString;
/**
* Created by Oak on 2022/10/8.
* Description:
*/
@Data
@ToString
public class RedisSingleProperties {
private String address;
}
import lombok.Data;
import lombok.ToString;
/**
* Created by Oak on 2022/10/8.
* Description:
*/
@Data
@ToString
public class RedisClusterProperties {
/**
* 集群状态扫描间隔时间,单位是毫秒
*/
private int scanInterval;
/**
* 集群节点
*/
private String nodes;
/**
* 默认值: SLAVE(只在从服务节点里读取)设置读取操作选择节点的模式。 可用值为: SLAVE - 只在从服务节点里读取。
* MASTER - 只在主服务节点里读取。 MASTER_SLAVE - 在主从服务节点里都可以读取
*/
private String readMode;
/**
* (从节点连接池大小) 默认值:64
*/
private int slaveConnectionPoolSize;
/**
* 主节点连接池大小)默认值:64
*/
private int masterConnectionPoolSize;
/**
* (命令失败重试次数) 默认值:3
*/
private int retryAttempts;
/**
*命令重试发送时间间隔,单位:毫秒 默认值:1500
*/
private int retryInterval;
/**
* 执行失败最大次数默认值:3
*/
private int failedAttempts;
}
@Data
@ToString
public class RedisPoolProperties {
private int maxIdle;
private int minIdle;
private int maxActive;
private int maxWait;
private int connTimeout;
private int soTimeout;
/**
* 池大小
*/
private int size;
}
@Data
@ToString
public class RedisSentinelProperties {
/**
* 哨兵master 名称
*/
private String master;
/**
* 哨兵节点
*/
private String nodes;
/**
* 哨兵配置
*/
private boolean masterOnlyWrite;
/**
*
*/
private int failMax;
}
4.实例redissonClient
定义了单机连接池模式,集群模式,哨兵模式。通过配置redisson.redis.mode来设置需要使用哪种模式
import com.oak.net.config.redison.properity.RedisProperties;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Created by Oak on 2022/10/8.
* Description:
*/
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfiguration {
@Autowired
private RedisProperties redisProperties;
@Configuration
@ConditionalOnClass({Redisson.class})
@ConditionalOnExpression("'${redisson.redis.mode}'=='single'")
protected class RedissonSingleClientConfiguration {
/**
* 单机模式 redisson 客户端
*/
@Bean
@ConditionalOnProperty(name = "redisson.redis.mode", havingValue = "single")
RedissonClient redissonSingle() {
Config config = new Config();
String node = redisProperties.getSingle().getAddress();
node = node.startsWith("redis://") ? node : "redis://" + node;
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(node)
.setTimeout(redisProperties.getPool().getConnTimeout())
.setConnectionPoolSize(redisProperties.getPool().getSize())
.setConnectionMinimumIdleSize(redisProperties.getPool().getMinIdle());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
/**
* 集群模式的 redisson 客户端
*
* @return
*/
@Bean
@ConditionalOnProperty(name = "redisson.redis.mode", havingValue = "cluster")
RedissonClient redissonCluster() {
System.out.println("cluster redisProperties:" + redisProperties.getCluster());
Config config = new Config();
String[] nodes = redisProperties.getCluster().getNodes().split(",");
List<String> newNodes = new ArrayList(nodes.length);
Arrays.stream(nodes).forEach((index) -> newNodes.add(
index.startsWith("redis://") ? index : "redis://" + index));
ClusterServersConfig serverConfig = config.useClusterServers()
.addNodeAddress(newNodes.toArray(new String[0]))
.setScanInterval(
redisProperties.getCluster().getScanInterval())
.setIdleConnectionTimeout(
redisProperties.getPool().getSoTimeout())
.setConnectTimeout(
redisProperties.getPool().getConnTimeout())
// .setFailedAttempts(
// redisProperties.getCluster().getFailedAttempts())
.setRetryAttempts(
redisProperties.getCluster().getRetryAttempts())
.setRetryInterval(
redisProperties.getCluster().getRetryInterval())
.setMasterConnectionPoolSize(redisProperties.getCluster()
.getMasterConnectionPoolSize())
.setSlaveConnectionPoolSize(redisProperties.getCluster()
.getSlaveConnectionPoolSize())
.setTimeout(redisProperties.getTimeout());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
/**
* 哨兵模式 redisson 客户端
* @return
*/
@Bean
@ConditionalOnProperty(name = "redisson.redis.mode", havingValue = "sentinel")
RedissonClient redissonSentinel() {
System.out.println("sentinel redisProperties:" + redisProperties.getSentinel());
Config config = new Config();
String[] nodes = redisProperties.getSentinel().getNodes().split(",");
List<String> newNodes = new ArrayList(nodes.length);
Arrays.stream(nodes).forEach((index) -> newNodes.add(
index.startsWith("redis://") ? index : "redis://" + index));
SentinelServersConfig serverConfig = config.useSentinelServers()
.addSentinelAddress(newNodes.toArray(new String[0]))
.setMasterName(redisProperties.getSentinel().getMaster())
.setReadMode(ReadMode.SLAVE)
// .setFailedAttempts(redisProperties.getSentinel().getFailMax())
.setTimeout(redisProperties.getTimeout())
.setMasterConnectionPoolSize(redisProperties.getPool().getSize())
.setSlaveConnectionPoolSize(redisProperties.getPool().getSize());
if (StringUtils.isNotBlank(redisProperties.getPassword())) {
serverConfig.setPassword(redisProperties.getPassword());
}
return Redisson.create(config);
}
}
}
5.redis分布式锁实现
共享数据设置了商品的数量,现在需要对该商品进行秒杀,首先会获取redis锁,获取到了后就去减库存,没有这直接返回。减完库存后释放该锁。其他线程即可继续获取该锁来进行抢占商品。
lock是当获取锁失败时会阻塞当前进程,如果没有带参数设置过期时间则是30秒后自动解锁
tryLock则是当获取锁失败时,当超过设置的等待时间时返回false
import com.oak.net.redis.RedisKey;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
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.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* Created by Oak on 2022/9/20.
* Description:
*/
@Api(tags = {"redisson分布式锁接口"})
@RestController
@RequestMapping("/lock")
@Slf4j
public class RedisonLockCtrl {
@Autowired
private RedissonClient redisson;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@ApiOperation(value = "测试1", notes = "测试1")
@GetMapping(value = "/order1")
public void toReadLock() {
//获取 锁
RLock lock = redisson.getLock(RedisKey.GOODSLOCK);
boolean isLock = false;
try {
lock.lock();
LessQuantity();
} catch (Exception e) {
log.error("执行业务异常:{}", e.getMessage());
} finally {
lock.unlock();
}
}
@ApiOperation(value = "测试1", notes = "测试1")
@GetMapping(value = "/order")
public void toReadLock1() {
//获取 锁
RLock lock = redisson.getLock(RedisKey.GOODSLOCK);
boolean isLock = false;
try {
isLock = lock.tryLock(1000, 10000, TimeUnit.MILLISECONDS);
if (isLock) {
LessQuantity();
} else {
log.info("没有获取到锁");
}
} catch (Exception e) {
log.error("执行业务异常:{}", e.getMessage());
} finally {
if (isLock) {
lock.unlock();
}
}
}
/**
* 共享数据操作,确保同时只能有一个线程执行该操作
*
* @return
*/
private Integer LessQuantity() {
Object sum1 = redisTemplate.opsForValue().get(RedisKey.GOODS);
int sum = (int) sum1;
if (sum > 0) {
int realSum = sum - 1;
redisTemplate.opsForValue().set(RedisKey.GOODS, realSum);
System.out.println("A服务-下单成功,数量还剩:" + realSum);
return realSum;
} else if (sum == 0) {
System.out.println("A服务-下单失败");
return 0;
}
return null;
}
}
5.存在的缺点
问题:使用 Redisson 实现分布式锁方案最大的问题就是如果你对某个 Redis Master 实例完成了加锁,此时 Master 会异步复制给其对应的 slave 实例。但是这个过程中一旦 Master 宕机,主备切换,slave 变为了 Master。接着就会导致,客户端 2 来尝试加锁的时候,在新的 Master 上完成了加锁,而客户端 1 也以为自己成功加了锁,此时就会导致多个客户端对一个分布式锁完成了加锁,这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。所以这个就是 Redis Cluster 或者说是 Redis Master-Slave 架构的主从异步复制导致的 Redis 分布式锁的最大缺陷(在 Redis Master 实例宕机的时候,可能导致多个客户端同时完成加锁)(后面有Redission红锁可以解决)
总结
以上就是SpringBoot集成redisson实现分布式锁的代码,改方式实现可普通单机情况下的所有问题,广泛使用于分布式高并发场景下,存在的问题主要是特殊情况下,主机宕机造成锁异常问题,后面将介绍开源Redisson解决该问题的方式。