分布式進階:Springboot自定義注解優(yōu)雅的實現(xiàn)Redisson分布式鎖
一、前言
在這個微服務多節(jié)點、多線程的環(huán)境中,多個任務可能會同時競爭訪問共享資源,從而導致數(shù)據(jù)錯誤和不一致。一般的JVM層面的加鎖顯然無法滿足多個節(jié)點的情況!分布式鎖就出現(xiàn)了,在redis官網推薦Java使用Redisson去實現(xiàn)分布式鎖!
這是基本api調用,今天我們使用自定義注解來完成,一勞永逸,減少出錯!
二、Redisson簡介
Redisson是一個用于Java應用程序的開源的、基于Redis的分布式和高性能數(shù)據(jù)結構服務庫。它提供了一系列的分布式對象和服務,幫助開發(fā)人員更輕松地在分布式環(huán)境中使用Java編程語言。Redisson通過封裝Redis的功能,使得開發(fā)者能夠更方便地利用分布式特性,同時提供了許多額外的功能和工具。
比setnx簡單的加鎖機制,Redisson會提供更完善的加鎖機制,比如:
「到期方法沒有執(zhí)行完成,引入看門狗機制自動續(xù)期,內部使用Lua腳本保證原子性!」
「提供眾多的鎖:」
- 可重入鎖(Reentrant Lock)
- 公平鎖(Fair Lock)
- 聯(lián)鎖(MultiLock)
- 紅鎖(RedLock)
- 讀寫鎖(ReadWriteLock)
對于今天的注解形式,只能實現(xiàn)可重入鎖、公平鎖兩種形式,不過也滿足大部分業(yè)務場景!
今天以實戰(zhàn)為主,這些信息可以去官網看一下詳細的文檔:
Redisson文檔:https://github.com/redisson/redisson/wiki/1.-Overview。
三、實戰(zhàn)
1、導入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
2、配置文件
server:
port: 8087
spring:
redis:
password: 123456
# 一定要加redis://
address: redis://127.0.0.1:6379
datasource:
#使用阿里的Druid
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?serverTimeznotallow=UTC
username: root
password:
3、RedissonClient配置
/**
* @author wangzhenjun
* @date 2022/2/9 9:57
*/
@Configuration
public class MyRedissonConfig {
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.address}")
private String address;
/**
* 所有對redisson的使用都是通過RedissonClient來操作的
* @return
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redissonClient(){
// 1. 創(chuàng)建配置
Config config = new Config();
// 一定要加redis://
config.useSingleServer().setAddress(address);
config.useSingleServer().setPassword(password);
// 2. 根據(jù)config創(chuàng)建出redissonClient實例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
4、Redis序列化配置
/**
* @author wangzhenjun
* @date 2022/11/17 15:20
*/
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
// 使用StringRedisSerializer來序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
5、自定義注解
我們自定義注解,key支持el表達式!這里的參數(shù)可以再加一個key的前綴或者鎖的類型,根據(jù)類型判斷:可重入鎖(RLock getLock(String name))、公平鎖(RLock getFairLock(String name);)這兩種的加鎖!等待鎖超時時間、自動解鎖時間、時間單位這是可選擇的,大家按需,需要看門狗的有的就不需要,現(xiàn)在是有兩種加鎖機制,后面也是看大家的選擇!
/**
* @author wangzhenjun
* @date 2023/8/30 10:45
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisLock {
/**
* 分布式鎖的 key,必須:請保持唯一性,支持 spring el表達式
*
*/
String value();
/**
* 等待鎖超時時間,默認30
*
*/
long waitTime() default 30;
/**
* 自動解鎖時間,自動解鎖時間一定得大于方法執(zhí)行時間,否則會導致鎖提前釋放,默認100(根據(jù)場景配置)
* 對時間沒有把握可以使用默認的看門狗會自動續(xù)期
*/
long leaseTime() default 100;
/**
* 時間單位,默認為秒
*
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
6、定義切片
現(xiàn)在有兩種加鎖方式,我們來詳細說一下區(qū)別,大家按需選擇:
「lock.tryLock():」
這是一個非阻塞的方法。如果獲取鎖成功,會立即返回 true,如果獲取鎖失敗,會立即返回 false。
當然你可以添加等待時間,超過這個時間仍然沒有獲取到鎖才會返回false。tryLock(long time, TimeUnit unit)tryLock(long waitTime, long leaseTime, TimeUnit unit)
如果你想嘗試獲取鎖,但「不希望在獲取失敗時被阻塞」,可以使用這個方法。
這個方法通常用于獲取鎖后執(zhí)行一個短時間的任務,避免長時間的等待。
「lock.lock():」
這是一個阻塞的方法,如果獲取鎖失敗,它會阻塞當前線程,直到獲取到鎖或超時。因此要確保你的鎖的使用不會導致長時間的等待,避免影響系統(tǒng)性能。
也可以添加鎖的過期時間,一旦獲取鎖成功,鎖會在指定的時間后自動釋放。如果在這段時間內任務未完成,鎖會自動釋放,避免長時間的占用。這個時間要考慮清除,如果執(zhí)行時間不可控建議還是不要傳過期時間,默認會有看門狗來自動續(xù)期,防止方法執(zhí)行中鎖被釋放了!
lock(long leaseTime, TimeUnit unit)
如果你希望一定能夠獲取鎖,而且「不希望在獲取失敗時立即返回」,可以使用這個方法。
這個方法通常用于獲取鎖后需要執(zhí)行一個相對耗時的任務,以及希望避免鎖被長時間占用而引發(fā)的問題。
小編這里建議使用第一種,有鎖正在執(zhí)行,應該返回信息給用戶,不應該讓用戶長時間等待造成不好的影響!
如果是第一個方法,我們需要判斷返回值,加鎖失敗返回給用戶!異常大家可以專門定義一個加鎖失敗異常,小編這里就使用業(yè)務異常了!
/**
* 分布式鎖切片
* @author wangzhenjun
* @date 2023/8/31 9:28
*/
@Slf4j
@Aspect
@RequiredArgsConstructor
@Component
public class RedisLockAspect {
private final RedissonClient redissonClient;
private final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
/**
* 環(huán)繞切片
*/
@Around("@annotation(redisLock)")
public Object aroundRedisLock(ProceedingJoinPoint point, RedisLock redisLock) throws Throwable {
log.info("=====請求來排隊嘗試獲取鎖=====");
String value = redisLock.value();
Assert.hasText(value, "@RedisLock key不能為空!");
boolean el = redisLock.isEl();
String lockKey;
if (el) {
lockKey = evaluateExpression(value, point);
} else {
lockKey = value;
}
log.info("========解析后的lockKey :{}", lockKey);
long waitTime = redisLock.waitTime();
long leaseTime = redisLock.leaseTime();
TimeUnit timeUnit = redisLock.timeUnit();
RLock lock = redissonClient.getLock(lockKey);
// lock.tryLock(waitTime, leaseTime, timeUnit);
// lock.lock(leaseTime, timeUnit);
// lock.lock();
boolean tryLock = lock.tryLock();
if (!tryLock) {
throw new ServiceException("鎖被占用,請稍后提交!");
}
try {
return point.proceed();
} catch (Throwable throwable) {
log.error("方法執(zhí)行失敗:", throwable.getMessage());
throw throwable;
} finally {
lock.unlock();
}
}
/**
* 解析el表達式
* @param expression
* @param point
* @return
*/
private String evaluateExpression(String expression, ProceedingJoinPoint point) {
// 獲取目標對象
Object target = point.getTarget();
// 獲取方法參數(shù)
Object[] args = point.getArgs();
MethodSignature methodSignature = (MethodSignature) point.getSignature();
Method method = methodSignature.getMethod();
EvaluationContext context = new MethodBasedEvaluationContext(target, method, args, parameterNameDiscoverer);
Expression exp = spelExpressionParser.parseExpression(expression);
return exp.getValue(context, String.class);
}
}
7、測試
我們測試一個el表達式的,模擬方法執(zhí)行15s,方便我們測試!
@SneakyThrows
@RedisLock("#id")
@GetMapping("/listTest")
public Result listTest(@RequestParam("id") Long id){
System.out.println("=====方法執(zhí)行中");
Thread.sleep(150000);
System.out.println("=====方法執(zhí)行完成");
return Result.success("成功");
}
我們調用兩次這個方法,看到控制臺有報錯信息,返回結果也是沒有問題的!
四、總結
在本篇博客中,我們深入探討了如何在Spring Boot應用中借助自定義注解來實現(xiàn)分布式鎖,為分布式環(huán)境下的并發(fā)問題提供了優(yōu)雅且高效的解決方案。通過自定義注解,我們成功地將分布式鎖的復雜邏輯進行了封裝,使得在業(yè)務代碼中只需簡單地使用注解,便能實現(xiàn)分布式鎖的獲取和釋放。這不僅讓代碼更具可讀性,還提升了開發(fā)效率,讓開發(fā)人員能夠更專注于業(yè)務邏輯的實現(xiàn)。
相信大家已經能夠對Spring Boot中使用自定義注解實現(xiàn)分布式鎖有一個清晰的理解,加鎖的方式大家可以按需選擇!