Skip to content

Commit

Permalink
feat: Spring AOP + Redisson 实现分布式锁
Browse files Browse the repository at this point in the history
  • Loading branch information
x201206030 committed Jun 20, 2022
1 parent ac1628a commit 9bd95d3
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 22 deletions.
49 changes: 28 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,27 @@ novel 是一套基于时下**最新** Java 技术栈 Spring Boot 3 + Vue 3 开

## 后端技术选型

| 技术 | 版本 | 说明 | 官网 | 学习 |
|-------------------------------|:--------------:|---------------------| --------------------------------------- |:---------------------------------------------------------------------------------------:|
| Spring Boot | 3.0.0-SNAPSHOT | 容器 + MVC 框架 | https://spring.io/projects/spring-boot | [进入](https://youdoc.github.io/course/novel/11.html) |
| MyBatis | 3.5.9 | ORM 框架 | http://www.mybatis.org | [进入](https://mybatis.org/mybatis-3/zh/index.html) |
| MyBatis-Plus | 3.5.1 | MyBatis 增强工具 | https://baomidou.com/ | [进入](https://baomidou.com/pages/24112f/) |
| JJWT | 0.11.5 | JWT 登录支持 | https://github.com/jwtk/jjwt | - |
| Lombok | 1.18.24 | 简化对象封装工具 | https://github.com/projectlombok/lombok | [进入](https://projectlombok.org/features/all) |
| Caffeine | 3.1.0 | 本地缓存支持 | https://github.com/ben-manes/caffeine | [进入](https://github.com/ben-manes/caffeine/wiki/Home-zh-CN) |
| Redis | 7.0 | 分布式缓存支持 | https://redis.io | [进入](https://redis.io/docs) |
| MySQL | 8.0 | 数据库服务 | https://www.mysql.com | [进入](https://docs.oracle.com/en-us/iaas/mysql-database/doc/getting-started.html) |
| ShardingSphere-JDBC | 5.1.1 | 数据库分库分表支持 | https://shardingsphere.apache.org | [进入](https://shardingsphere.apache.org/document/5.1.1/cn/overview) |
| Elasticsearch | 8.2.0 | 搜索引擎服务 | https://www.elastic.co | [进入](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) |
| RabbitMQ | 3.10.2 | 开源消息中间件 | https://www.rabbitmq.com | [进入](https://www.rabbitmq.com/tutorials/tutorial-one-java.html) |
| XXL-JOB | 2.3.1 | 分布式任务调度平台 | https://www.xuxueli.com/xxl-job | [进入](https://www.xuxueli.com/xxl-job) |
| Sentinel | 1.8.4 | 流量控制组件 | https://github.com/alibaba/Sentinel | [进入](https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5) |
| Spring Boot Admin | 3.0.0-M1 | 应用管理和监控 | https://github.com/codecentric/spring-boot-admin | [进入](https://codecentric.github.io/spring-boot-admin/3.0.0-M1) |
| Undertow | 2.2.17.Final | Java 开发的高性能 Web 服务器 | https://undertow.io | [进入](https://undertow.io/documentation.html) |
| Docker | - | 应用容器引擎 | https://www.docker.com/ | - |
| Jenkins | - | 自动化部署工具 | https://github.com/jenkinsci/jenkins | - |
| Sonarqube | - | 代码质量控制 | https://www.sonarqube.org/ | - |
| 技术 | 版本 | 说明 | 官网 | 学习 |
|---------------------|:--------------:|---------------------| --------------------------------------- |:---------------------------------------------------------------------------------------:|
| Spring Boot | 3.0.0-SNAPSHOT | 容器 + MVC 框架 | https://spring.io/projects/spring-boot | [进入](https://youdoc.github.io/course/novel/11.html) |
| MyBatis | 3.5.9 | ORM 框架 | http://www.mybatis.org | [进入](https://mybatis.org/mybatis-3/zh/index.html) |
| MyBatis-Plus | 3.5.1 | MyBatis 增强工具 | https://baomidou.com/ | [进入](https://baomidou.com/pages/24112f/) |
| JJWT | 0.11.5 | JWT 登录支持 | https://github.com/jwtk/jjwt | - |
| Lombok | 1.18.24 | 简化对象封装工具 | https://github.com/projectlombok/lombok | [进入](https://projectlombok.org/features/all) |
| Caffeine | 3.1.0 | 本地缓存支持 | https://github.com/ben-manes/caffeine | [进入](https://github.com/ben-manes/caffeine/wiki/Home-zh-CN) |
| Redis | 7.0 | 分布式缓存支持 | https://redis.io | [进入](https://redis.io/docs) |
| MySQL | 8.0 | 数据库服务 | https://www.mysql.com | [进入](https://docs.oracle.com/en-us/iaas/mysql-database/doc/getting-started.html) |
| Redisson | 3.17.4 | 分布式锁实现 | https://github.com/redisson/redisson | [进入](https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95) |
| ShardingSphere-JDBC | 5.1.1 | 数据库分库分表支持 | https://shardingsphere.apache.org | [进入](https://shardingsphere.apache.org/document/5.1.1/cn/overview) |
| Elasticsearch | 8.2.0 | 搜索引擎服务 | https://www.elastic.co | [进入](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) |
| RabbitMQ | 3.10.2 | 开源消息中间件 | https://www.rabbitmq.com | [进入](https://www.rabbitmq.com/tutorials/tutorial-one-java.html) |
| XXL-JOB | 2.3.1 | 分布式任务调度平台 | https://www.xuxueli.com/xxl-job | [进入](https://www.xuxueli.com/xxl-job) |
| Sentinel | 1.8.4 | 流量控制组件 | https://github.com/alibaba/Sentinel | [进入](https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5) |
| Spring Boot Admin | 3.0.0-M1 | 应用管理和监控 | https://github.com/codecentric/spring-boot-admin | [进入](https://codecentric.github.io/spring-boot-admin/3.0.0-M1) |
| Undertow | 2.2.17.Final | Java 开发的高性能 Web 服务器 | https://undertow.io | [进入](https://undertow.io/documentation.html) |
| Docker | - | 应用容器引擎 | https://www.docker.com/ | - |
| Jenkins | - | 自动化部署工具 | https://github.com/jenkinsci/jenkins | - |
| Sonarqube | - | 代码质量控制 | https://www.sonarqube.org/ | - |

**注:更多热门新技术待集成。**
## 前端技术选型
Expand Down Expand Up @@ -203,7 +204,7 @@ git clone https://gitee.com/novel_dev_team/novel.git
password: test123456
```
2. 修改`src/resources/application.yml`配置文件中的`redis`连接配置
2. 修改`src/resources/application.yml` 和 `src/resources/redisson.yml` 配置文件中的`redis`连接配置
```
spring:
Expand All @@ -213,6 +214,12 @@ git clone https://gitee.com/novel_dev_team/novel.git
password: 123456
```
```
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: 123456
```
3. 项目根目录下运行如下命令来启动后端服务(有安装 IDE 的可以导入源码到 IDE 中运行)
```
Expand Down
14 changes: 14 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<xxl-job.version>2.3.1</xxl-job.version>
<sentinel.version>1.8.4</sentinel.version>
<shardingsphere-jdbc.version>5.1.1</shardingsphere-jdbc.version>
<redisson.version>3.17.4</redisson.version>
</properties>
<dependencies>
<dependency>
Expand Down Expand Up @@ -160,6 +161,19 @@
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Redisson 相关 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>${redisson.version}</version>
</dependency>

<!-- Aop 相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/io/github/xxyopen/novel/core/annotation/Key.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.github.xxyopen.novel.core.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* 分布式锁-Key 注解
*
* @author xiongxiaoyang
* @date 2022/6/20
*/
@Documented
@Retention(RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
public @interface Key {
String expr() default "";
}
31 changes: 31 additions & 0 deletions src/main/java/io/github/xxyopen/novel/core/annotation/Lock.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.github.xxyopen.novel.core.annotation;

import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* 分布式锁 注解
*
* @author xiongxiaoyang
* @date 2022/6/20
*/
@Documented
@Retention(RUNTIME)
@Target(METHOD)
public @interface Lock {

String prefix();

boolean isWait() default false;

long waitTime() default 3L;

ErrorCodeEnum failCode() default ErrorCodeEnum.OK;

}
81 changes: 81 additions & 0 deletions src/main/java/io/github/xxyopen/novel/core/aspect/LockAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package io.github.xxyopen.novel.core.aspect;

import io.github.xxyopen.novel.core.annotation.Lock;
import io.github.xxyopen.novel.core.common.exception.BusinessException;
import lombok.SneakyThrows;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
* 分布式锁 切面
*
* @author xiongxiaoyang
* @date 2022/6/20
*/
@Aspect
@Component
public record LockAspect(RedissonClient redissonClient) {

private static final String KEY_PREFIX = "Lock";

private static final String KEY_SEPARATOR = "::";

@Around(value = "@annotation(io.github.xxyopen.novel.core.annotation.Lock)")
@SneakyThrows
public Object doAround(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method targetMethod = methodSignature.getMethod();
Lock lock = targetMethod.getAnnotation(Lock.class);
String lockKey = KEY_PREFIX + buildLockKey(lock.prefix(), targetMethod,
joinPoint.getArgs());
RLock rLock = redissonClient.getLock(lockKey);
if (lock.isWait() ? rLock.tryLock(lock.waitTime(), TimeUnit.SECONDS) : rLock.tryLock()) {
try {
return joinPoint.proceed();
} finally {
rLock.unlock();
}
}
throw new BusinessException(lock.failCode());
}

private String buildLockKey(String prefix, Method method, Object[] args) {
StringBuilder builder = new StringBuilder();
if (Objects.nonNull(prefix) && !prefix.isEmpty()) {
builder.append(KEY_SEPARATOR).append(prefix);
}
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
builder.append(KEY_SEPARATOR);
if (parameters[i].isAnnotationPresent(io.github.xxyopen.novel.core.annotation.Key.class)) {
io.github.xxyopen.novel.core.annotation.Key key = parameters[i].getAnnotation(io.github.xxyopen.novel.core.annotation.Key.class);
builder.append(parseKeyExpr(key.expr(), args[i]));
}
}
return builder.toString();
}

private String parseKeyExpr(String expr, Object arg) {
if (Objects.isNull(expr) || expr.isEmpty()) {
return arg.toString();
}
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(expr, new TemplateParserContext());
return expression.getValue(arg, String.class);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.github.xxyopen.novel.core.config;

import lombok.SneakyThrows;
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 xiongxiaoyang
* @date 2022/6/20
*/
@Configuration
public class RedissonConfig {

@Bean
@SneakyThrows
public RedissonClient redissonClient(){
Config config = Config.fromYAML(getClass().getResource("/redisson.yml"));
return Redisson.create(config);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.github.xxyopen.novel.core.annotation.Key;
import io.github.xxyopen.novel.core.annotation.Lock;
import io.github.xxyopen.novel.core.auth.UserHolder;
import io.github.xxyopen.novel.core.common.constant.ErrorCodeEnum;
import io.github.xxyopen.novel.core.common.req.PageReqDto;
Expand Down Expand Up @@ -200,8 +202,9 @@ public RestResp<List<BookCategoryRespDto>> listCategory(Integer workDirection) {
return RestResp.ok(bookCategoryCacheManager.listCategory(workDirection));
}

@Lock(prefix = "userComment")
@Override
public RestResp<Void> saveComment(UserCommentReqDto dto) {
public RestResp<Void> saveComment(@Key(expr = "#{userId + '::' + bookId}") UserCommentReqDto dto) {
// 校验用户是否已发表评论
QueryWrapper<BookComment> queryWrapper = new QueryWrapper<>();
queryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID, dto.getUserId())
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/redisson.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: 123456

0 comments on commit 9bd95d3

Please sign in to comment.