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) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
1 概述
- Redisson是一个在Redis的基础上实现的Java驻内存数据网格,封装很多功能,比如分布式锁,布隆过滤器,可以使用很简单方式实现这些功能
2 基本使用
1. 引入依赖
首先,我们需要在项目的pom.xml中添加Redisson和Nacos的依赖。以下是使用Maven的示例:
XML
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.3</version> <!-- 根据最新版本选择 -->
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-spring-context</artifactId>
<version>2.0.3</version> <!-- 根据最新版本选择 -->
</dependency>
2. 创建Nacos配置
在nacos中进行配置管理
XML
spring:
data:
redis:
host: 192.168.200.130
port: 6379
database: 0
timeout: 1800000
password:
jedis:
pool:
max-active: 20 #最大连接数
max-wait: -1 #最大阻塞等待时间(负数表示没限制)
max-idle: 5 #最大空闲
min-idle: 0 #最小空闲
3.创建配置类,初始redisson对象
java
package com.atguigu.tingshu.common.config.redssion;
import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.SingleServerConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
/**
* redisson配置信息
*/
@Data
@Configuration
@ConfigurationProperties("spring.data.redis")
public class RedissonConfig {
private String host;
private String password;
private String port;
private int timeout = 3000;
private static String ADDRESS_PREFIX = "redis://";
/**
* 自动装配
*
*/
@Bean
RedissonClient redissonSingle() {
Config config = new Config();
if(StringUtils.isEmpty(host)){
throw new RuntimeException("host is empty");
}
SingleServerConfig serverConfig = config.useSingleServer()
.setAddress(ADDRESS_PREFIX + this.host + ":" + port)
.setTimeout(this.timeout);
if(!StringUtils.isEmpty(this.password)) {
serverConfig.setPassword(this.password);
}
return Redisson.create(config);
}
}
整个流程的序列图如下:
4. 基本示例
java
// 阻塞式获取锁
RLock lock = redisson.getLock("myLock");
lock.lock(); //加锁
//执行需要锁保护的代码
//执行需要锁保护的代码
//执行需要锁保护的代码
//执行需要锁保护的代码
//执行需要锁保护的代码
lock.unlock(); //解锁
// 非阻塞式获取锁
RLock lock = redisson.getLock("myLock");
if (lock.tryLock(10, TimeUnit.SECONDS)) {
// 执行需要锁保护的代码
lock.unlock();
} else {
// 获取锁失败,执行其他操作
}
5. 测试代码
java
package com.atguigu.tingshu.album.service.impl;
import com.alibaba.cloud.commons.lang.StringUtils;
import com.atguigu.tingshu.album.service.TestService;
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.stereotype.Service;
@Service
public class TestServiceImpl implements TestService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
@Override
public void testLock() {
//0.1 创建锁对象
RLock lock = redissonClient.getLock("lock");
//0.2 获取锁
//0.2.1 一直等待到获取锁,如果获取锁成功,锁的有效时间为:30s,底层启动"看门狗"线程(如果业务有超时风险)可以延迟锁有效时间
lock.lock();
//0.2.2 一直等待到获取锁,如果获取锁成功,自定义锁有效时间
//lock.lock(10, TimeUnit.SECONDS);
//0.2.3 尝试获取锁 参数1:等待获取锁时间,超时则放弃获取 参数2:如果获取锁成功,锁的有效时间 参数3:时间单位
//boolean b = lock.tryLock(3, 10, TimeUnit.SECONDS);
try {
//1.从Redis缓存中获取key="num"的值 保证redis中存在"num"(手动提前在redis中创建key)
String value = stringRedisTemplate.opsForValue().get("num");
if (StringUtils.isBlank(value)) {
return;
}
//2.对获取到值进行+1操作
int num = Integer.parseInt(value);
stringRedisTemplate.opsForValue().set("num", String.valueOf(++num));
} catch (NumberFormatException e) {
e.printStackTrace();
} finally {
//3. 释放锁
lock.unlock();
}
}
}
整个业务,在查询一个专辑详情时,添加缓存和分布式锁代码
java
@Override
public AlbumInfo getAlbumInfoById(Long id) {
//生成数据key,包含专辑id
String albumKey = RedisConstant.ALBUM_INFO_PREFIX+ id;
//1 查询redis缓存
AlbumInfo albumInfo = (AlbumInfo)redisTemplate.opsForValue().get(albumKey);
//2 如果redis查询不到数据,查询mysql,把mysql数据放到redis里面
if(albumInfo == null) {
// 查询mysql添加分布式锁,使用Redisson实现
try {//获取锁对象
String albumLockKey = RedisConstant.ALBUM_INFO_PREFIX+id+ ":lock";
RLock rLock = redissonClient.getLock(albumLockKey);
//加锁
// rLock.lock(); 加锁,默认值
//rLock.lock(30,TimeUnit.SECONDS); 自己设置过期时间
// trylock三个参数:第一个等待时间,第二个参数过期时间
boolean tryLock = rLock.tryLock(10, 30, TimeUnit.SECONDS);
if(tryLock) { //加锁成功
try {
//查询mysql,
AlbumInfo albumInfoData = this.getAlbumInfoData(id);
if(albumInfoData == null) {
albumInfoData = new AlbumInfo();
//把mysql查询数据放到redis里面
redisTemplate.opsForValue().set(albumKey,albumInfoData,
RedisConstant.ALBUM_TIMEOUT,TimeUnit.SECONDS);
}
//把mysql查询数据放到redis里面
redisTemplate.opsForValue().set(albumKey,albumInfoData,
RedisConstant.ALBUM_TIMEOUT,TimeUnit.SECONDS);
return albumInfoData;
}finally {
//解锁
rLock.unlock();
}
} else {
// 没有获取到锁的线程,自旋
return getAlbumInfoById(id);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
//3 如果redis查询数据,直接返回
return albumInfo;
}
}
3 两个机制
第一个:重试机制
- 重试机制 是由于各种原因导致的锁竞争失败,Redisson会自动进行重试,直到获取到锁或者超时。这个机制可以有效地避免因为网络问题等不可控因素导致的锁竞争失败。
第二个:看门狗机制(WatchDog)
-
**看门狗机制(WatchDog)**是 指在获取锁之后,Redisson会启动一个守护线程来监控锁的情况,如果锁的过期时间即将到达,守护线程会自动续期。保证操作在有锁状态下执行
-
看门狗机制是Redission提供的一种自动延期机制,这个机制使得 Redission提供的分布式锁是可以自动续期的。
-
在Redission中想要启动看门狗机制,不要自己设置过期时间。如果自己定义了过期时间,无论是通过 lock 还是 tryLock 方法,都无法启用看门狗机制 。
4 使用优化
在项目中很多数据接口需要查询缓存,那么分布式锁的业务逻辑代码就会出现大量的重复。因此,我们可以借助Spring框架事务注解来实现简化代码。只要类上添加了一个注解,那么这个注解就会自带分布式锁的功能。
实现如下:
在模块中添加一个自定义注解
java
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface GuiguCache {
/**
* 缓存key的前缀
* @return
*/
String prefix() default "cache";
}
创建AOP切面类
java
@Aspect
@Component
public class GuiguCacheAspect {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedissonClient redissonClient;
//当方法上有GuiguCache时候,这个方法执行
@SneakyThrows
@Around("@annotation(com.atguigu.tingshu.common.cache.GuiguCache)")
public Object cacheAspect(ProceedingJoinPoint joinPoint) {
//1 获取业务方法上注解里面前缀值 album:Info @GuiguCache(prefix="album:Info")
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
GuiguCache guiguCache = signature.getMethod().getAnnotation(GuiguCache.class);
String prefix = guiguCache.prefix();
//2 获取业务方法里面参数值 id值 public AlbumInfo getAlbumInfoById(Long id)
Object[] args = joinPoint.getArgs();
//3 根据前缀值 + id值生成redis的key
String key = prefix+ Arrays.asList(args).toString();
//4 查询redis
Object o = redisTemplate.opsForValue().get(key);
if(o == null) {
try {
//5 如果redis查询不到数据,查询mysql,把mysql数据放到redis里面,返回数据
//5.1 添加分布式锁
//获取锁对象
RLock rLock = redissonClient.getLock(key + ":lock");
//加锁
boolean tryLock = rLock.tryLock(10, 30, TimeUnit.SECONDS);
if(tryLock) {//加锁成功
try {
//查询mysql
Object obj = joinPoint.proceed(args);
if (null == obj){
// 并把结果放入缓存
obj = new Object();
this.redisTemplate.opsForValue().set(key, obj, 1,TimeUnit.HOURS);
return obj;
}
redisTemplate.opsForValue().set(key,obj,1,TimeUnit.HOURS);
return obj;
} finally {
rLock.unlock();
}
} else {//加锁失败
return cacheAspect(joinPoint);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
//6 如果redis查询到数据,直接返回
return o;
}
}
}
这样只要在实现类中添加该注解,设置前缀,redis里面key名字前缀 ,即可使用分布式锁
例如:
java
@GuiguCache(prefix = RedisConstant.ALBUM_INFO_PREFIX)
@Override
public AlbumInfo getAlbumInfoById(Long id) {
return this.getAlbumInfoDB(id);
}
@Override
@GuiGuCache(prefix = "albumStat:")
public AlbumStatVo getAlbumStatVoByAlbumId(Long albumId) {
// 调用mapper 层方法
return albumInfoMapper.selectAlbumStat(albumId);
}
@GuiguCache(prefix = "category:")
@Override
public BaseCategoryView getCategoryViewByCategory3Id(Long category3Id) {
return baseCategoryViewMapper.selectById(category3Id);
}