一.引入pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>redission</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redission</name>
<description>redission</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--日志-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<scope>compile</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
<!--aop切面-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
二.YAML文件配置集群模式(哨兵模式)
redisson.yaml
sentinelServersConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
failedSlaveReconnectionInterval: 3000
failedSlaveCheckInterval: 60000
password: "我是密码"
subscriptionsPerConnection: 5
clientName: "redisson"
loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> { }
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
slaveConnectionMinimumIdleSize: 24
slaveConnectionPoolSize: 64
masterConnectionMinimumIdleSize: 24
masterConnectionPoolSize: 64
readMode: "SLAVE"
subscriptionMode: "SLAVE"
sentinelAddresses:
- "redis://ip:port"
- "redis://ip:port"
- "redis://ip:port"
masterName: "masterName"
database: 0
scanInterval: 1000
pingConnectionInterval: 0
keepAlive: false
tcpNoDelay: false
threads: 16
nettyThreads: 32
codec: !<org.redisson.codec.JsonJacksonCodec> { }
transportMode: "NIO"
三.激活redisson.yaml
1.创建application-database.yaml
spring:
redis:
redisson:
file: classpath:redisson.yaml
2.application.yaml引入application-database.yaml
spring:
application:
name: redission
profiles:
include: database
server:
port: 8700
servlet:
context-path: /api-test
四.自定义redis分布式锁注解
package com.example.redission.core;
import java.lang.annotation.*;
/**
* redis锁注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Inherited
@Documented
public @interface RedisLock {
/**
* 锁的key
*
* @return {@link String}
*/
String key() default "";
/**
* 超时时间(秒,默认600秒)
*
* @return long
*/
long timeout() default 600L;
/**
* 加锁失败提示语
*
* @return {@link String}
*/
String message() default "请求已发起,请勿重复操作!";
/**
* 等待锁时长 5s
*
* @return long
*/
long waitTimeout() default 5;
}
五.封装RedissonClient服务
package com.example.redission.core;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @author 土
* @version 1.0
* @description redis lock by Redisson
*/
@Service
@RequiredArgsConstructor
@Slf4j
@AutoConfigureAfter(RedissonClient.class)
public class RedissonService {
private final RedissonClient redisson;
@PostConstruct
public void init() {
log.info("RedissonService started");
if (Objects.isNull(redisson)) {
log.warn("RedissonClient not initialized");
}
log.info("RedissonService started on database:{}", redisson.getConfig().useSentinelServers().getDatabase());
}
public RedissonClient redisson() {
return redisson;
}
public RLock getLock(String key) {
return redisson.getLock(key);
}
public boolean tryLock(RLock lock, long timeout, long waitTime) {
if (Objects.isNull(lock)) {
return false;
}
if (lock.isLocked()) {
return false;
}
boolean result = false;
try {
result = lock.tryLock(waitTime, timeout, TimeUnit.SECONDS);
log.info("lock acquired, {},{}, result={}", lock.getName(), lock.getHoldCount(), result);
} catch (InterruptedException e) {
log.error("TryLock timeout");
}
return result;
}
public void unlock(RLock lock) {
if (Objects.nonNull(lock)) {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("unlock {}", lock.getName());
}
}
}
}
六.拦截带有自定义注解的方法,并在方法执行前后加锁和解锁
package com.example.redission.core;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.redisson.api.RLock;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* redis锁拦截器实现
*/
@Aspect
@Order(3)
@Component
@Slf4j
@RequiredArgsConstructor
public class RedisLockAspect {
private final RedissonService redissonService;
@Pointcut("@annotation(com.example.redission.core.RedisLock) " +
"|| @within(com.example.redission.core.RedisLock)"
)
public void pointcut() {
}
/**
* 加锁->处理->解锁
*
* @param joinPoint 连接点
* @param redisLock redis锁
* @return {@link Object}
*/
@Around("pointcut() && @annotation(redisLock)")
public Object doAround(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
String key = SpelParser.parse(redisLock.key(), joinPoint);
long expire = redisLock.timeout();
long waitTimeout = redisLock.waitTimeout();
RLock lock = redissonService.getLock(key);
if (!redissonService.tryLock(lock, expire, waitTimeout)) {
throw new Exception();
}
try {
return joinPoint.proceed();//执行业务代码!
} finally {
redissonService.unlock(lock);
}
}
}
七.解析spel
package com.example.redission.core;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 解析spel
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class SpelParser {
private static SpelExpressionParser parser = new SpelExpressionParser();
private static BeanFactory beanFactory;
@Resource
public void setBeanFactoryResolver(BeanFactory beanFactory) {
SpelParser.beanFactory = beanFactory;
}
/**
* @param expressionString spel 表达式
* @param joinPoint aspectj切面 JoinPoint
* @return
*/
public static String parse(String expressionString, JoinPoint joinPoint) {
Object[] values = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
String[] parameterNames = nameDiscoverer.getParameterNames(method);
if (parameterNames == null || parameterNames.length == 0) {
return expressionString;
}
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
for (int i = 0; i < parameterNames.length; i++) {
evaluationContext.setVariable(parameterNames[i], values[i]);
}
// add custom bean to resolver
evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
try {
Expression expression = parser.parseExpression(expressionString);
Object expressionValue = expression.getValue(evaluationContext);
if (expressionValue != null && !"".equals(expressionValue.toString())) {
return expressionValue.toString();
} else {
return expressionString;
}
} catch (Exception e) {
log.error(e.getMessage(), e);
return expressionString;
}
}
}
八.controller测试
package com.example.redission.controller;
import com.example.redission.core.RedisLock;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 土
* @version 1.0
* @description controller
*/
@RestController
@RequestMapping("/hello")
@Slf4j
@RequiredArgsConstructor
public class ApiController {
@RedisLock(key = "'lock_' + #str")
@GetMapping(value = "say", produces = MediaType.APPLICATION_JSON_VALUE)
public String send(@RequestParam(name = "str") String str) {
return str;
}
}
九.postman发起请求
http://localhost:8700/api-test/hello/say?str=damn
-----------------------------------------------------------------------我是一个分割线-------------------------------------------------------------------------------------------
RRateLimiter限流
一.自定义注解
package com.example.redission.core;
import java.lang.annotation.*;
/**
* @version 1.0
* @description ratelimit by redisson
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RateLimit {
// 标识接口
String name();
// 错误信息
String message() default "";
// 时间窗口 单位秒
long interval() default 60;
// 限流次数
long count() default 60;
// 等待时长 单位秒
long timeout() default 5;
}
二.实现自定义注解切面
package com.example.redission.core;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.redisson.api.*;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @version 1.0
* @description RateLimitAspect
*/
@Slf4j
@Aspect
@Component
@Order(4)
@AutoConfigureAfter(RedissonClient.class)
@RequiredArgsConstructor
public class RateLimitAspect {
private final RedissonService redissonService;
@Pointcut("@annotation(com.example.redission.core.RateLimit) " +
"|| @within(com.example.redission.core.RateLimit)"
)
public void pointcut() {
}
/**
* 继续执行的情况:
* 1. 无注解
* 2. RedissonClient未初始化
* 3. 限流器获取失败
* <p></p>
* 限流的情况:
* 获取 permit失败 @throw {@link Exception}
* <p></p>
*
* @param rateLimit 注解
*/
@Before("pointcut() && @annotation(rateLimit)")
public void doBefore(RateLimit rateLimit) {
if (Objects.isNull(rateLimit)) {
return;
}
RRateLimiter limiter = getRedissonRateLimiter(rateLimit.name(), rateLimit.interval(), rateLimit.count());
if (Objects.isNull(limiter)) {
log.warn("RedissonRateLimiter is null, continue");
return;
}
if (!limiter.tryAcquire(rateLimit.timeout(), TimeUnit.SECONDS)) {
throw new RuntimeException("gg");
}
}
/**
* 突发流量数 count*2
* <p></p>
* 1. 限流次数 >= 1
* 2. 限流时间 >=1
*
* @param name
* @param interval
* @param count
* @return
*/
private RRateLimiter getRedissonRateLimiter(String name, long interval, long count) {
if (count < 1 || interval < 1 || Objects.isNull(redissonService.redisson())) {
return null;
}
RRateLimiter rateLimiter = redissonService.redisson().getRateLimiter(name);
// 创建一个新的限流器
if (!rateLimiter.isExists()) {
rateLimiter.trySetRate(RateType.OVERALL, count, interval, RateIntervalUnit.SECONDS);
return rateLimiter;
}
// 已存在的限流器
RateLimiterConfig rateLimiterConfig = rateLimiter.getConfig();
Long rateInterval = rateLimiterConfig.getRateInterval();
Long rate = rateLimiterConfig.getRate();
// 应用重启过并修改过配置,应用新规则
if (TimeUnit.MILLISECONDS.convert(interval, TimeUnit.SECONDS) != rateInterval || count != rate) {
rateLimiter.delete();
rateLimiter.trySetRate(RateType.OVERALL, count, interval, RateIntervalUnit.SECONDS);
}
return rateLimiter;
}
}