目录
Redisson 分布式 Redis 客户端
分布式锁需求分析 与 主流实现方式
Redisson 分布式锁快速入门
Redisson 分布式锁常用 API
自定义 Redisson 配置选项
YML 文件方式配置(推荐方式)
1、Redisson 是一个在 Redis 的基础上实现的 Java 驻内存数据网格(In-Memory Data Grid),它不仅提供了一系列的分布式的 Java 常用对象,还提供了许多分布式服务,如 (BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) 。
2、Redisson 底层采用的是 Netty 框架,支持 Redis 2.8 以上版本,支持 Java1.6+ 以上版本。Redis 命令和 Redisson 对象匹配列表。
3、个人理解:Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 更侧重对 Reids 数据库的 CRUD(增删改查),而 Redisson API 侧重于分布式开发,比如它的分布式锁。Spring Boot 为 Lettuce 和 Jedis 客户端库提供基本的自动配置,且默认使用 Lettuce 作为客户端,对于现在微服务开发,项目通常都是分布式多实例部署,分布式锁通常都会用到,实现分布式锁的方式有很多,Redisson 解决方案就是其中一种,此时只需要添加 Redisson 依赖即可轻松使用,不用担心与 Jedis 或 Lettuce 冲突。
4、通过 Redisson 官方文档可以发现 Redisson 有很多分布式功能,本文暂时以分布式锁进行练习。
Redisson github 开源地址:https://github.com/redisson/redisson/ Redisson 官方中文文档: 目录 · redisson/redisson Wiki · GitHub Redisson 官方示例:https://github.com/redisson/redisson-examples |
1、当两个用户同时请求,落在同一个系统的不同节点上时,如果使用 Java 原生的锁机制(synchronized或ReentrantLock ),则是无效的,因为图中的两个 A 系统节点,运行在两个不同的 JVM 中,加的锁只对属于自己 JVM 里面的线程有效。
2、此时,解决办法是使用分布式锁,分布式锁的思路是:为整个系统提供一个全局、唯一的锁。比较常用的是 Redission,以及 基于 zookeeper 实现分布式锁。本文主要介绍前者。
3、Redisson 分布式锁不仅使用非常简单,而且可靠性高:
redisson 所有指令都通过 lua 脚本执行,redis 支持 lua 脚本原子性执行 |
redisson 设置 key 的默认过期时间为 30s,当某个客户端持有锁超过了30s时怎么办呢?redisson 的 |
redisson 的“看门狗”逻辑保证了没有死锁发生,比如机器宕机了,看门狗也就没了,此时自然也就不会延长 key 的过期时间,到了 30s 之后就会自动过期了,其他线程可以获取到锁。 |
分布式锁主流实现方式 | |
1、基于数据库记录,进入时写数据,退出时删记录 | |
2、数据库行锁,比如分布式 quartz,它是一把排它锁 | |
3、基于 Redis,自己直接使用 redis,或者使用第三方的框架,如 Redisson | |
4、基于 zookeeper |
|
5、redis 获取锁是轮训机制,客户端每隔一段时间去获取一下,锁释放后会有多个调用者争抢;zk 是监听机制,有变动会接到通知,除了非公平锁,也可以实现公平锁。 |
1、以如下的示例进行验收分布式锁,当没有锁的情况下,用户重复请求时,后台重复执行业务代码。
2、第一步:项目中导入 Redisson 依赖。Redission 官网分布式锁和同步器
org.redisson
redisson
3.13.4
3、第二步:配置 RedissonClient 实例
无论 Reids 是单机部署、还是主从复制、哨兵模式、云托管、集群部署等等,Redisson 都提供了相应的[配置方法]。本节以 redis 服务器单机部署为例。
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;
/**
* Redisson 配置类
*
* @author wangMaoXiong
* @version 1.0
* @date 2020/9/24 19:28
*/
@Configuration
public class RedissonConfig {
//redis 服务器单机部署时,创建 RedissonClient 实例,交由 Spring 容器管理
@Bean
public RedissonClient redissonClient() {
/**
* Config:Redisson 配置基类,SingleServerConfig:单机部署配置类,MasterSlaveServersConfig:主从复制部署配置
* SentinelServersConfig:哨兵模式配置,ClusterServersConfig:集群部署配置类。
* useSingleServer():初始化 redis 单服务器配置。即 redis 服务器单机部署
* setAddress(String address):设置 redis 服务器地址。格式 -- 主机:端口,不写时,默认为 127.0.0.1:6379
* setDatabase(int database): 设置连接的 redis 数据库,默认为 0
* setPassword(String password):设置 redis 服务器认证密码,没有时设置为 null,默认为 null
* RedissonClient create(Config config): 使用提供的配置创建同步/异步 Redisson 实例
* Redisson 类实现了 RedissonClient 接口,真正需要使用的就是这两个 API
*/
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setDatabase(2)
.setPassword(null);
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
Config 以及 XxxServerConfig 配置类中提供了许多的配置项,不过通常大部分默认即可,为了演示方便,才写死在代码中,实际中应该是由配置文件配置。
4、第四步:分布式锁使用
加锁之后,对于同一个支付订单,当用户重复请求时,因为执行业务前已经上锁了,所以后续请求必须等待前面的请求执行完成,释放锁之后,才能继续执行。(只是为了演示方便,才在控制层加锁,实际中应该在业务层操作)
/**
* RedissonClient.getLock(String name):可重入锁
* boolean tryLock(long waitTime, long leaseTime, TimeUnit unit):尝试获取锁
* 1、waitTime:获取锁时的等待时间,超时自动放弃,线程不再继续阻塞,方法返回 false
* 2、leaseTime:获取到锁后,指定加锁的时间,超时后自动解锁
* 3、如果成功获取锁,则返回 true,否则返回 false。
*
* http://localhost:8080/redisson/payment3?orderNumber=8856767
*
* @param orderNumber
* @return
*/
@GetMapping("redisson/payment3")
public String payment3(@RequestParam Integer orderNumber) {
String result = "订单【" + orderNumber + "】支付成功.";
logger.info("用户请求支付订单【" + orderNumber + "】.");
String key = "com.wmx.wmxredis.controller.RedissonController.payment3_" + orderNumber;
/**
* getLock(String name):按名称返回锁实例,实现了一个非公平的可重入锁,因此不能保证线程获得顺序
* lock():获取锁,如果锁不可用,则当前线程将处于休眠状态,直到获得锁为止
*/
RLock lock = redissonClient.getLock(key);
boolean tryLock = false;
try {
tryLock = lock.tryLock(30, 180, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!tryLock) {
return "订单【" + orderNumber + "】正在支付中,请耐心等待!";
}
try {
logger.info("查询支付状态");
TimeUnit.SECONDS.sleep(40);
logger.info("开始支付订单【" + orderNumber + "】");
TimeUnit.SECONDS.sleep(40);
lock.unlock();
} catch (Exception e) {
e.printStackTrace();
result = "订单【" + orderNumber + "】支付失败:" + e.getMessage();
} finally {
logger.info("结束支付订单【" + orderNumber + "】");
/**
* boolean isLocked():检查锁是否被任何线程锁定,被锁定时返回 true,否则返回 false.
* unlock():释放锁, Lock 接口的实现类通常会对线程释放锁(通常只有锁的持有者才能释放锁)施加限制,
* 如果违反了限制,则可能会抛出(未检查的)异常。如果锁已经被释放,重复释放时,会抛出异常。
*/
if (lock.isLocked()) {
lock.unlock();
}
}
return result;
}
在线演示源码:src/main/java/com/wmx/wmxredis/redisson/RedissonController.java · 汪少棠/wmx-redis - Gitee.com
更多详细信息参考官网:分布式锁和同步器
Redisson 类实现 RedissonClient 接口 | |
RLock lock = redisson.getLock("key"); |
可重入锁(Reentrant Lock) 按名称返回锁实例,实现了一个非公平的可重入锁,因此不能保证线程获得顺序 可重复锁是指可以多次对同一个 key 的进行 lock,加锁几次,同样就得解锁(unlock)几次。 |
RLock fairLock = redisson.getFairLock("key"); | 可重入公平锁(Fair Lock) 保证了当多个 Redisson 客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson 会等待5秒后继续下一个线程 |
联锁(MultiLock):将多个 RLock 对象关联为一个联锁,每个 RLock 对象实例可以来自于不同的 Redisson 实例。 RLock lock1 = redissonInstance1.getLock("lock1"); RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3); |
|
红锁(RedLock):也可以用来将多个 RLock 对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例,在大部分节点上加锁成功就算成功。 RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3); // 为加锁等待100秒时间,并在加锁成功10秒钟后自动解开 |
|
读写锁(ReadWriteLock):分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。 RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock"); |
|
RLock 接口常用 API | |
void lock.lock(); | 获取锁,如果锁不可用,则当前线程将处于休眠状态,直到获得锁为止 |
void lock(long var1, TimeUnit var3) | 获取锁,如果锁不可用,则当前线程将处于休眠状态,直到获得锁为止。 加锁以后在指定的时间后自动解锁,可以无需再调用 unlock 方法手动解锁 |
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) | 尝试获取锁,如果成功获取锁,则返回 true,否则返回 false。 waitTime:获取锁时的等待时间,超时则放弃,线程不再继续阻塞,返回 false leaseTime:获取到锁后,指定加锁的时间,超时后自动解锁 |
void unlock(); | 释放锁, Lock 接口的实现类通常会对线程释放锁(通常只有锁的持有者才能释放锁)施加限制,如果违反了限制,则可能会抛出(未检查的)异常。 注意:如果锁已经被释放了,重复释放时,会抛出异常. |
boolean forceUnlock(); | 强制释放锁,即使锁已经被释放时,重复强制释放也不会抛出异常! |
boolean isLocked() | 检查锁是否被任何线程锁定,被锁定时返回 true,否则返回 false. |
String getName() | 获取锁的名称,即锁的 key. |
boolean isHeldByThread(long threadId) | 检查锁是否由指定线程Id的线程持有,是则返回 true,否则返回 false |
boolean isHeldByCurrentThread() | 检查锁是否由当前线程持有,是则返回 true,否则返回 false |
long remainTimeToLive(); | 获取锁的过期时间(毫秒),如果锁不存在,则返回 -2,如果锁存在但没有指定失效时间,则返回 -1。 |
上面的例子中将参数写死在代码中只是为了演示方便,现在优化一下,采用配置文件进行配置。
一:自定义配置属性 src/main/java/com/wmx/wmxredis/redisson/RedssionProperties.java · 汪少棠/wmx-redis - Gitee.com
1、Config 以及 XxxServerConfig 配置类中提供了许多的配置项,不过通常大部分默认即可,可以抽取其中比较常用的选项自定义一个配置类。这些属性可以参考 {@link SingleServerConfig}、{@link BaseConfig}、{@link Config}。
2、这些属性也可以参考 Redssion 官网配置方法。
import org.redisson.config.BaseConfig;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author wangMaoXiong
*
* 1、自定义 Redssion 配置属性,这些属性可以参考 {@link SingleServerConfig}、{@link BaseConfig}、{@link Config},根据需要添加或者减少
* 2、Redssion 官网配置方法:https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95
*/
@ConfigurationProperties(prefix = "redisson")
public class RedssionProperties {
//Redis 服务器地址
private String address;
//用于Redis连接的数据库索引
private int database = 0;
//Redis身份验证的密码,如果不需要,则应为null
private String password;
//Redis最小空闲连接量
private int connectionMinimumIdleSize = 24;
//Redis连接最大池大小
private int connectionPoolSize = 64;
//Redis 服务器响应超时时间,Redis 命令成功发送后开始倒计时(毫秒)
private int timeout = 3000;
//连接到 Redis 服务器时超时时间(毫秒)
private int connectTimeout = 10000;
//省略 getter、setter 方法未粘贴
}
二:自定义配置类 src/main/java/com/wmx/wmxredis/redisson/RedissonConfig.java · 汪少棠/wmx-redis - Gitee.com
@Configuration
@EnableConfigurationProperties(RedssionProperties.class)
public class RedissonConfig {
private final RedssionProperties redssionProperties;
/**
* 通过构造器从 Spring 容器中获取 {@link RedssionProperties}实例
*
* @param redssionProperties
*/
public RedissonConfig(RedssionProperties redssionProperties) {
this.redssionProperties = redssionProperties;
}
/**
* redis 服务器单机部署时,创建 RedissonClient 实例,交由 Spring 容器管理
* 只有当配置了 redisson.type=stand-alone 时,才继续生成 RedissonClient 实例并交由 Spring 容器管理
*
* @return
*/
@Bean
@ConditionalOnProperty(prefix = "redisson", name = "type", havingValue = "stand-alone")
public RedissonClient redissonClient() {
/**
* Config:Redisson 配置基类,SingleServerConfig:单机部署配置类,MasterSlaveServersConfig:主从复制部署配置
* SentinelServersConfig:哨兵模式配置,ClusterServersConfig:集群部署配置类。
* useSingleServer():初始化 redis 单服务器配置。即 redis 服务器单机部署
* setAddress(String address):设置 redis 服务器地址。格式 -- redis://主机:端口,不写时,默认为 redis://127.0.0.1:6379
* setDatabase(int database): 设置连接的 redis 数据库,默认为 0
* setPassword(String password):设置 redis 服务器认证密码,没有时设置为 null,默认为 null
* RedissonClient create(Config config): 使用提供的配置创建同步/异步 Redisson 实例
* Redisson 类实现了 RedissonClient 接口,真正需要使用的就是这两个 API
*/
Config config = new Config();
config.useSingleServer()
.setAddress(redssionProperties.getAddress())
.setDatabase(redssionProperties.getDatabase())
.setPassword(redssionProperties.getPassword())
.setConnectionPoolSize(redssionProperties.getConnectionPoolSize())
.setConnectionMinimumIdleSize(redssionProperties.getConnectionMinimumIdleSize())
.setTimeout(redssionProperties.getTimeout())
.setConnectTimeout(redssionProperties.getConnectTimeout());
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
三:全局 src/main/resources/application.yml · 汪少棠/wmx-redis - Gitee.com
#自定义分布式 Redis 客户端 Redisson 配置
redisson:
type: stand-alone #redis服务器部署类型,stand-alone:单机部署、cluster:机器部署.
address: redis://127.0.0.1:6379 #redis服务器地址,单机时必须是redis://开头.
本文虽然只是演示了 Redis 单机部署模式,但是其他 Redis 模式部署时也是同理,都可以参考 Redisson 配置方法 修改可得。
1、将配置信息写死在程序代码里显然是不合适的,上面的自定义配置虽然也是一种很好的解决方式,但是官方提供了更好的办法,可以直接从 yml 配置文件中读取配置(推荐方式)。
2、先提供一个 yml 文件,比如 redisson-config.yml,文件名称随意,下面是单机版配置,其它方式配置可以参考官网,比如 集群模式。
# org.redisson.Config类的配置参数,适用于所有Redis组态模式(单机,集群和哨兵)
threads: 0
nettyThreads: 0
codec: ! {}
transportMode: NIO
# Redis 单机部署时 Redisson 文件方式配置
singleServerConfig:
address: "redis://127.0.0.1:6379" #节点地址
password: null #密码,默认null
database: 15 #数据库编号,默认0
idleConnectionTimeout: 10000 #连接空闲超时,单位毫秒,默认10000
connectTimeout: 10000 #连接超时,单位毫秒,默认10000
timeout: 3000 #命令等待超时,单位毫秒,默认3000
retryAttempts: 3 #命令失败重试次数,默认3
retryInterval: 1500 #命令重试发送时间间隔,单位毫秒,默认1500
subscriptionsPerConnection: 5 #单个连接最大订阅数量,默认5
clientName: null #客户端名称,默认null,
subscriptionConnectionMinimumIdleSize: 1 #发布和订阅连接的最小空闲连接数,默认1
subscriptionConnectionPoolSize: 50 #发布和订阅连接池大小,默认50
connectionMinimumIdleSize: 32 #最小空闲连接数,默认32
connectionPoolSize: 64 #连接池大小,默认64
dnsMonitoringInterval: 5000 #DNS监测时间间隔,单位毫秒,默认5000
sslEnableEndpointIdentification: true #启用SSL终端识别,默认true
sslProvider: JDK #SSL实现方式,默认JDK
sslTruststore: null #SSL信任证书库路径,默认null
sslTruststorePassword: null #SSL信任证书库密码,默认null
sslKeystore: null #SSL钥匙库路径,默认null
sslKeystorePassword: null #SSL钥匙库密码,默认null
src/main/resources/redisson-config.yml · 汪少棠/wmx-redis - Gitee.com
3、然后在配置类中直接读取即可,这样无论 redis 是怎样部署,只需要修改配置文件即可,完全不用修改任何代码,推荐方式。
/**
* Config fromYAML :从 yaml 文件读取 redisson 配置对象
* String toYAML() :将当前配置转换为YAML格式
*/
@Bean
public RedissonClient redissonClient() throws IOException {
URL resource = RedissonConfig2.class.getClassLoader().getResource("redisson-config.yml");
Config config = Config.fromYAML(resource);
RedissonClient redissonClient = Redisson.create(config);
log.info("Redisson 配置:{}", config.toYAML());
return redissonClient;
}
src/main/java/com/wmx/wmxredis/redisson/RedissonConfig2.java · 汪少棠/wmx-redis - Gitee.com
4、原生 org.redisson.redisson 依赖就已经提供了 fromYAML 的功能。