【要点记录】基于spring-boot搭建的QA论坛项目

需求:

设计一个QA问答项目,记录一下项目搭建过程中的一些经验。

一、技术栈

jdk11 + springboot + mybatis + redis + knite4j

二、设计数据库

2.1 外键设计原则

一对多场景:遵循一方主键对多方外键,字段名相同

在这里插入图片描述

2.2 设计索引

Mysql引擎用的是innodb,默认使用主键索引,如果查询语句中用到了其他字段进行查询,考虑到覆盖索引来节省索引回调所浪费的时间,可以建立相应的索引关系,例如SELECTContent , time FROM answer WHERE Content = “你好” AND time = now time,只需要建立一个Content和time的联合索引,就可以在数据量比较大的时候(K数量级)加快查询的效率。

索引涉及思路可参考《阿里巴巴 Java 开发手册》。

2.3 增加逻辑删除字段

涉及一个“假删除”,使用update代替delete完成sql操作
Short deleteFlag;

三、实体类的配置

3.1 配置自动填充

自动填充设置的默认值需要实体类属性的类型完全一致

在这里插入图片描述
例如“deleteFlag”字段是Short类型,自动填充设置的默认值类型也需为Short类型,不可是其他整型

3.2 配置逻辑删除

逻辑删除时的默认值可以与实体类属性类型不一致(例如Integer和Short)

在这里插入图片描述

注解中提供了逻辑删除的默认值为整型,不与其实际的类型"Short"相冲突

3.3 时间字段的Json转换

把时间(日期)字段通过Json转换直接存入Redis中,会变成整型,可在字段上加Jackson或Fastjson的注解来解决

在这里插入图片描述

这里我使用了FastJson的注解来实现

四、Knife4j的配置

Knife4j访问网址:http://localhost:80/doc.html

pom.xml:

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-spring-boot-starter</artifactId>
    <version>2.0.7</version>
</dependency>

java注解:

@EnableSwagger2WebMvc

五、AOP配置Redis

分析:Service层查询方法的返回值有两种类型:Object和List<Object>,所以在AOP切面的时候需要根据这两种情况分别定义2个注解

2021年2月18日16:28:26更新:
可以通过逻辑判断将不同返回值类型的切面整合为一个注解,也可以通过execution(* com.*.*(..))等el表达式根据包名来命中包,代替注解

2021年2月19日18:39:36更新:
可以将@Pointcut的el表达式存放在properties属性文件中,动态配置AOP的切点,redis.pointcut = @annotation(com.cry.qa.annotation.RedisCachePageObject)

@Configuration
// 指定配置文件路径
@PropertySource("classpath:redis.properties")
public class RedisConfig {

    @Value("${redis.pointcut}")
    private String pointcut;

    public String getPointcut() {
        return this.pointcut;
    }

}

众所周知@Pointcut后的expression表达式上只能放常量,笔者用这种方法实现动态AOP会报"常量表达式”的错误,应该可以用拦截器来绕过@Pointcut,待思考。

5.1 自定义元注解

/**
 * 元注解 用来标识查询数据库的方法
 */
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCache {
    //    RedisCacheNamespace nameSpace();
}

5.2 定义Redis get/set类

在该类中定义操作Redis的工具方法,并实现Java对象和Json数据格式的相互转换

    /**
     * 保存数据到Redis
     *
     * @param redisKey
     */
    public String saveDataToRedis(String redisKey, Object obj) {

        Jedis jedis = jedisPool.getResource();
        String code = jedis.set(redisKey, JSON.toJSONString(obj));
        //设置key的过期时间 时间单位是秒(7天)
        jedis.expire(redisKey, 604800);
        return code;
    }

    /**
     * 删除redis中的数据
     *
     * @param key
     * @return
     */
    public Long delete(String key) {

        Jedis jedis = jedisPool.getResource();
        Long del = jedis.del(key);
        return del;
    }

    /**
     * 正则匹配所有符合条件的keys
     *
     * @param className
     * @return
     */
    public Set<String> getKeysByPattern(String className) {

        Jedis jedis = jedisPool.getResource();
        StringBuilder stringBuilder = new StringBuilder();
        // *QuestionServiceImpl*
        stringBuilder.append("*").append(className).append("*");
        Set<String> keys = jedis.keys(stringBuilder.toString());
        return keys;
    }

5.3 定义AOP增强类

通过在切点JoinPoint前后加上环绕增强,来实现自动的缓存功能,首先判断Redis中是否有对应键值的数据,如有从Redis中取;如没有,从MySQL中取完后,将数据同步到Redis中

5.4 保持Redis和MySQL中数据的最终一致性

在增、删、改操作时,需要同步对Redis中的旧缓存数据进行一个删除,防止脏数据,该功能通过redis的删除来实现,每次改变mysql数据库中数据类型时,先对redis里的内容进行一个删除

5.5 给Redis中的键设置不同的过期时间

根据业务的需求设置,待实现

2021年2月19日18:45:38 已实现

在这里插入图片描述
这里笔者是分set + expire两步来为redis中的数据赋值过期时间,高并发情况下可以把这两步合二为一确保数据安全性。

5.6 记录一个FastJson反序列化操作Redis数据的坑

从Redis中反序列化Json数据 => Java实体类对象时,由于存储在Redis中的数据是标准Json带"",通过Jedis获取到数据后打印控制台可验证:
在这里插入图片描述
因此无法直接通过JSON.parseObject()方法来转换,需要通过正则或JSON.parse()消除转义字符""后再转换。

六、实现用户登录模块

// TODO: 待更新

~~ 2021/1/27 更新

在这里插入图片描述

数据库的持久化依赖redis-aop生成的token,可以考虑用消息队列实现任务的异步处理。

七、实现Message信息模块

在这里插入图片描述
注意点:

7.1 请求

多参数请求构建请求参数封装类,在Service业务处理逻辑中转换成Do对象,补充空缺参数;

7.2 返回

取出Do对象,在Service中转换成Vo对象,返回前端。add,update,delete没必要返回值,在Controller层加try-catch,不抛出异常就是正常执行。返回msg信息给前端。

7.3 DO

一对多,多方全部查询,可以用联合查询;一对多,多方只去特定其一,传入一方和多方具体对象直接获取属性。

主动发起的业务,需要先getLogin或登录;被动通知的业务,不需要。

7.4 优化:构建常量类

Type类型:例如站内私信、系统通知这种文字类型,可以定义一个常量类。
在这里插入图片描述

八、实现Question - Answer问答模块

在这里插入图片描述

8.1 数据库优化

读写分离 + 主从同步

详见笔者博客:【已测可行】配置mysql数据库一主两从多数据源 + 数据库主从同步的一种方案

8.2 token数据持久化

下游业务可以由上游业务发起一个消息,下游独立系统从中间件中取得消息确保任务的独立完成。

消息中间件详见笔者博客:https://blog.csdn.net/haohaoxuexiyai/article/details/111055575

2021年2月19日18:53:33 更新: 考虑到业务属性类似,生产中往往会放在同一系统中,故可不拆分

8.3 日志读写的AOP分离

为了打印Controller中的日志,需要写大量的// 获取当前类的日志 private final Logger logger = LoggerFactory.getLogger(UserController.class); 代码来获取日志,可将该功能单独抽象一层。

@Aspect
@Component
public class WebLogAspect {

    private final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

    //切入点描述: 这个是controller包的切入点
    @Pointcut("execution(public * com.cry.qa.controller..*.*(..))")

    //签名,可以理解成这个切入点的一个名称
    public void controllerLog() {

    }

    //在切入点的方法run之前要干的
    @Before("controllerLog()")
    public void logBeforeController(JoinPoint joinPoint) {

        //这个RequestContextHolder是Springmvc提供来获得请求的东西
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();

        // 记录下请求内容
        logger.info("################URL : " + request.getRequestURL().toString());
        logger.info("################HTTP_METHOD : " + request.getMethod());
        logger.info("################IP : " + request.getRemoteAddr());
        logger.info("################THE ARGS OF THE CONTROLLER : " + Arrays.toString(joinPoint.getArgs()));

        //下面这个getSignature().getDeclaringTypeName()是获取包+类名的   然后后面的joinPoint.getSignature.getName()获取了方法名
        logger.info("################CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        //logger.info("################TARGET: " + joinPoint.getTarget());//返回的是需要加强的目标类的对象
        //logger.info("################THIS: " + joinPoint.getThis());//返回的是经过加强后的代理类的对象

    }
}

8.4 日志文件的本地存储

开启debug级别logback的日志,并将其存储在本地文件
application.yml:

在这里插入图片描述

考虑到日志文件会越来越大,做一个动态的日志切分,每50M切分一次

logback-spring.xml

<configuration scan="true" scanPeriod="10 seconds">
    <include resource="org/springframework/boot/logging/logback/base.xml" />

    <appender name="INFO_FILE"
        class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${LOG_PATH}/info/info.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_PATH}/info-%d{yyyyMMdd}.log.%i</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy
                class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>500MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <maxHistory>2</maxHistory>
        </rollingPolicy>
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36}
                -%msg%n
            </Pattern>
        </layout>
    </appender>
    <root level="INFO">
        <appender-ref ref="INFO_FILE" />
    </root>

</configuration>

九、项目地址

https://github.com/chenruoyu0319/qa-springboot-mp-father

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 护眼 设计师:闪电赇 返回首页