diff --git a/README.md b/README.md index 7bedbc3..ae93bcf 100644 --- a/README.md +++ b/README.md @@ -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/ | - | **注:更多热门新技术待集成。** ## 前端技术选型 @@ -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: @@ -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 中运行) ``` diff --git a/pom.xml b/pom.xml index 04b3c58..9b69ada 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ 2.3.1 1.8.4 5.1.1 + 3.17.4 @@ -160,6 +161,19 @@ spring-boot-starter-security + + + org.redisson + redisson + ${redisson.version} + + + + + org.springframework.boot + spring-boot-starter-aop + + mysql mysql-connector-java diff --git a/src/main/java/io/github/xxyopen/novel/core/annotation/Key.java b/src/main/java/io/github/xxyopen/novel/core/annotation/Key.java new file mode 100644 index 0000000..7a1e2a8 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/annotation/Key.java @@ -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 ""; +} diff --git a/src/main/java/io/github/xxyopen/novel/core/annotation/Lock.java b/src/main/java/io/github/xxyopen/novel/core/annotation/Lock.java new file mode 100644 index 0000000..a5e5be2 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/annotation/Lock.java @@ -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; + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/aspect/LockAspect.java b/src/main/java/io/github/xxyopen/novel/core/aspect/LockAspect.java new file mode 100644 index 0000000..a003edd --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/aspect/LockAspect.java @@ -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); + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/core/config/RedissonConfig.java b/src/main/java/io/github/xxyopen/novel/core/config/RedissonConfig.java new file mode 100644 index 0000000..68136d5 --- /dev/null +++ b/src/main/java/io/github/xxyopen/novel/core/config/RedissonConfig.java @@ -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); + } + +} diff --git a/src/main/java/io/github/xxyopen/novel/service/impl/BookServiceImpl.java b/src/main/java/io/github/xxyopen/novel/service/impl/BookServiceImpl.java index 31d07e2..dc0c996 100644 --- a/src/main/java/io/github/xxyopen/novel/service/impl/BookServiceImpl.java +++ b/src/main/java/io/github/xxyopen/novel/service/impl/BookServiceImpl.java @@ -3,6 +3,8 @@ package io.github.xxyopen.novel.service.impl; 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; @@ -200,8 +202,9 @@ public class BookServiceImpl implements BookService { return RestResp.ok(bookCategoryCacheManager.listCategory(workDirection)); } + @Lock(prefix = "userComment") @Override - public RestResp saveComment(UserCommentReqDto dto) { + public RestResp saveComment(@Key(expr = "#{userId + '::' + bookId}") UserCommentReqDto dto) { // 校验用户是否已发表评论 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq(DatabaseConsts.BookCommentTable.COLUMN_USER_ID, dto.getUserId()) diff --git a/src/main/resources/redisson.yml b/src/main/resources/redisson.yml new file mode 100644 index 0000000..bfe1a47 --- /dev/null +++ b/src/main/resources/redisson.yml @@ -0,0 +1,3 @@ +singleServerConfig: + address: "redis://127.0.0.1:6379" + password: 123456