网站搭建实操(三)后台管理-2-forum-core)

网站搭建实操(三)后台管理-2-forum-core

forum-core核心配置模块

forum-core 是微服务的核心配置模块,提供:

  • 统一配置:所有服务共享的配置(跨域、拦截器、过滤器)

  • AOP切面:日志、权限、限流等通用功能

  • 安全配置:JWT过滤器、认证配置

  • 异常处理:全局异常处理器

  • 数据填充:MyBatis-Plus自动填充

pom文件

java 复制代码
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>forum-backend</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>forum-core</artifactId>
    <packaging>jar</packaging>
    <description>核心配置模块 - 提供全局配置、拦截器、过滤器、AOP等核心功能</description>

    <dependencies>
        <!-- Common模块 -->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>forum-common</artifactId>
            <version>${project.version}</version>
        </dependency>

        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Spring Boot AOP -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- Spring Boot Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- Spring Boot Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- MyBatis Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

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

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- Jackson -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

        <!-- Validation -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
        </dependency>
    </dependencies>

    <build>
        <finalName>forum-core</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

web MVC配置解决跨域等问题

设计原因:

    1. 统一配置跨域,解决前后端分离的跨域问题
    1. 配置拦截器执行顺序:限流 -> 认证
    1. 统一JSON序列化格式,确保前后端日期格式一致
java 复制代码
package org.example.core.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.example.core.interceptor.AuthenticationInterceptor;
import org.example.core.interceptor.RateLimitInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

/**
 * Web MVC配置类
 *
 * 设计原因:
 * 1. 统一配置跨域,解决前后端分离的跨域问题
 * 2. 配置拦截器执行顺序:限流 -> 认证
 * 3. 统一JSON序列化格式,确保前后端日期格式一致
 */
@Configuration
@RequiredArgsConstructor
public class WebMvcConfig  implements WebMvcConfigurer {
    private final AuthenticationInterceptor authenticationInterceptor;
    private final RateLimitInterceptor rateLimitInterceptor;

    /**
     * 配置跨域请求
     * 为什么需要跨域?浏览器同源策略限制,前后端分离部署时域名/端口不同
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:8080") // 允许的源
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }

    /**
     * 配置拦截器
     * 执行顺序:限流拦截器 -> 认证拦截器
     * 为什么这个顺序?先限流后认证,减少无效请求的认证开销
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 限流拦截器(最先执行)
        registry.addInterceptor(rateLimitInterceptor)
                .addPathPatterns("/api/**")
                .order(1);

        // 认证拦截器
        registry.addInterceptor(authenticationInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns(
                        "/api/auth/login",
                        "/api/auth/register",
                        "/api/auth/captcha",
                        "/swagger-ui/**",
                        "/v3/api-docs/**",
                        "/doc.html"
                )
                .order(2);
    }

    /**
     * 配置消息转换器
     * 为什么需要自定义?统一日期格式,避免时区问题
     */
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0, mappingJackson2HttpMessageConverter());
    }

    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setObjectMapper(objectMapper());
        return converter;
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();

        // 忽略未知属性,防止前端传递多余字段导致反序列化失败
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 日期格式配置
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

        // Java 8时间模块配置
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        javaTimeModule.addDeserializer(LocalDateTime.class,
                new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

        objectMapper.registerModule(javaTimeModule);
        return objectMapper;
    }

}

认证拦截器

  • 设计原因:
    1. 在请求进入Controller前解析JWT Token
    1. 将用户信息存入请求属性,供后续使用
    1. 不在此处验证权限,只做解析,权限由Spring Security管理
java 复制代码
package org.example.core.interceptor;
import com.forum.common.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 认证拦截器
 *
 * 设计原因:
 * 1. 在请求进入Controller前解析JWT Token
 * 2. 将用户信息存入请求属性,供后续使用
 * 3. 不在此处验证权限,只做解析,权限由Spring Security管理
 *
 * 为什么用拦截器而不是过滤器?
 * - 拦截器可以访问Spring MVC的上下文
 * - 可以更细粒度地控制哪些路径需要拦截
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthenticationInterceptor implements HandlerInterceptor {
    private final JwtUtils jwtUtils;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = extractToken(request);

        if (token != null && jwtUtils.validateToken(token)) {
            Long userId = jwtUtils.getUserIdFromToken(token);
            String username = jwtUtils.getUsernameFromToken(token);

            // 存入请求属性,供Controller使用
            request.setAttribute("userId", userId);
            request.setAttribute("username", username);

            log.debug("认证成功 - userId: {}, uri: {}", userId, request.getRequestURI());
        }

        return true; // 始终放行,让Spring Security做权限控制
    }

    private String extractToken(HttpServletRequest request) {
        String header = request.getHeader(jwtUtils.getTokenHeader());
        return jwtUtils.extractTokenFromHeader(header);
    }

}

限流拦截器

设计原因:

    1. 防止恶意请求和DDoS攻击
    1. 保护后端服务,避免资源耗尽
    1. 使用Redis实现分布式限流
java 复制代码
package org.example.core.interceptor;
import com.forum.common.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.TimeUnit;

/**
 * 限流拦截器
 *
 * 设计原因:
 * 1. 防止恶意请求和DDoS攻击
 * 2. 保护后端服务,避免资源耗尽
 * 3. 使用Redis实现分布式限流
 *
 * 限流算法:计数器算法(滑动窗口)
 * 为什么选择计数器?实现简单,性能好,适合API限流
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class RateLimitInterceptor  implements HandlerInterceptor{
    private final RedisUtils redisUtils;

    @Value("${rate-limit.enabled:true}")
    private Boolean enabled;

    @Value("${rate-limit.default-limit:100}")
    private Long defaultLimit;

    @Value("${rate-limit.default-window:60}")
    private Long defaultWindow;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!enabled) {
            return true;
        }

        String uri = request.getRequestURI();
        String key = getRateLimitKey(request, uri);

        if (!checkRateLimit(key)) {
            log.warn("请求被限流 - uri: {}, key: {}", uri, key);
            throw new BusinessException(429, "请求过于频繁,请稍后再试");
        }

        return true;
    }

    private boolean checkRateLimit(String key) {
        Long current = redisUtils.increment(key, 1);

        if (current == 1) {
            redisUtils.expire(key, defaultWindow, TimeUnit.SECONDS);
        }

        return current <= defaultLimit;
    }

    private String getRateLimitKey(HttpServletRequest request, String uri) {
        String ip = getClientIp(request);
        return "rate:limit:ip:" + ip + ":" + uri;
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty()) {
            ip = request.getRemoteAddr();
        }
        return ip != null ? ip.split(",")[0].trim() : "unknown";
    }

}

日志切面

  • 设计原因:
    1. 统一记录接口调用日志,便于问题排查
    1. 记录请求参数和响应结果,追踪数据流向
    1. 记录接口执行时间,监控性能
java 复制代码
package org.example.core.aop;

import com.alibaba.fastjson.JSON;
import com.forum.common.annotation.LogAnnotation;
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.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 日志切面
 *
 * 设计原因:
 * 1. 统一记录接口调用日志,便于问题排查
 * 2. 记录请求参数和响应结果,追踪数据流向
 * 3. 记录接口执行时间,监控性能
 *
 * 为什么使用AOP?非侵入式,业务代码无需关注日志
 */
@Slf4j
@Aspect
@Component
public class LogAspect {
    @Pointcut("@annotation(com.forum.common.annotation.LogAnnotation)")
    public void logPointCut() {
    }

    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);

        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes != null ? attributes.getRequest() : null;

        Map<String, Object> logMap = new HashMap<>();
        logMap.put("module", logAnnotation.module());
        logMap.put("operation", logAnnotation.operation());
        logMap.put("method", method.getName());

        if (request != null) {
            logMap.put("uri", request.getRequestURI());
            logMap.put("ip", getClientIp(request));
        }

        // 记录请求参数
        if (logAnnotation.needParam()) {
            Object[] args = joinPoint.getArgs();
            if (args != null && args.length > 0) {
                String params = Arrays.stream(args)
                        .filter(arg -> !(arg instanceof HttpServletRequest))
                        .map(arg -> {
                            try {
                                return JSON.toJSONString(arg);
                            } catch (Exception e) {
                                return arg.toString();
                            }
                        })
                        .collect(Collectors.joining(", "));
                logMap.put("params", params);
            }
        }

        Object result = null;
        try {
            result = joinPoint.proceed();
            logMap.put("result", logAnnotation.needResult() ? JSON.toJSONString(result) : "success");
            logMap.put("status", "success");
        } catch (Exception e) {
            logMap.put("status", "error");
            logMap.put("error", e.getMessage());
            throw e;
        } finally {
            long costTime = System.currentTimeMillis() - startTime;
            logMap.put("costTime", costTime + "ms");

            // 根据日志级别记录
            switch (logAnnotation.level()) {
                case INFO:
                    log.info("接口日志: {}", JSON.toJSONString(logMap));
                    break;
                case WARN:
                    log.warn("接口日志: {}", JSON.toJSONString(logMap));
                    break;
                case ERROR:
                    log.error("接口日志: {}", JSON.toJSONString(logMap));
                    break;
                default:
                    log.debug("接口日志: {}", JSON.toJSONString(logMap));
            }
        }

        return result;
    }

    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty()) {
            ip = request.getRemoteAddr();
        }
        return ip != null ? ip.split(",")[0].trim() : "";
    }
}

MyBatis-Plus配置类

  • 设计原因:
    1. 分页插件:支持物理分页,性能好
    1. 乐观锁插件:防止并发更新问题
    1. 防全表更新插件:防止误操作
    1. 自动填充:统一处理创建时间、更新时间
java 复制代码
package org.example.core.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.LocalDateTime;

/**
 * MyBatis-Plus配置类
 */
@Slf4j
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 分页插件 - 支持物理分页,避免内存分页
        PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        paginationInterceptor.setOptimizeJoin(true);
        paginationInterceptor.setMaxLimit(1000L);
        interceptor.addInnerInterceptor(paginationInterceptor);

        // 乐观锁插件 - 解决并发更新问题
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

        // 防全表更新插件 - 防止误操作删除/更新所有数据
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());

        log.info("MyBatis-Plus拦截器配置完成");
        return interceptor;
    }

    /**
     * 自动填充处理器
     * 为什么需要自动填充?统一处理创建人、创建时间、更新人、更新时间
     * 避免每个业务代码都重复设置
     */
    @Bean
    public MetaObjectHandler metaObjectHandler() {
        return new MetaObjectHandler() {
            @Override
            public void insertFill(MetaObject metaObject) {
                // 创建时间
                this.strictInsertFill(metaObject, "createdTime", LocalDateTime.class, LocalDateTime.now());
                // 更新时间
                this.strictInsertFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
                // 逻辑删除默认值
                this.strictInsertFill(metaObject, "isDeleted", Integer.class, 0);
            }

            @Override
            public void updateFill(MetaObject metaObject) {
                // 更新时间
                this.strictUpdateFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
            }
        };
    }
}

JWT认证过滤器

  • 设计原因:
    1. 在Spring Security过滤器链中解析JWT Token
    1. 将用户信息设置到SecurityContext
    1. 支持无状态认证,适合微服务架构
java 复制代码
package org.example.core.security;

import com.forum.common.utils.JwtUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * JWT认证过滤器
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter  extends OncePerRequestFilter{
    private final JwtUtils jwtUtils;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        try {
            String token = extractToken(request);

            if (token != null && jwtUtils.validateToken(token)) {
                String username = jwtUtils.getUsernameFromToken(token);

                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    // 加载用户详情
                    UserDetails userDetails = userDetailsService.loadUserByUsername(username);

                    // 创建认证对象
                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(
                                    userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                    // 设置到SecurityContext
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        } catch (Exception e) {
            log.error("JWT认证失败: {}", e.getMessage());
        }

        filterChain.doFilter(request, response);
    }

    private String extractToken(HttpServletRequest request) {
        String header = request.getHeader(jwtUtils.getTokenHeader());
        return jwtUtils.extractTokenFromHeader(header);
    }
}

Spring Security配置类

  • 设计原因:
    1. 禁用Session,使用JWT无状态认证
    1. 配置放行路径(登录、注册、文档等)
    1. 添加JWT过滤器
java 复制代码
package org.example.core.security;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * Spring Security配置类
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 放行路径
                .antMatchers("/api/auth/**").permitAll()
                .antMatchers("/swagger-ui/**").permitAll()
                .antMatchers("/v3/api-docs/**").permitAll()
                .antMatchers("/doc.html").permitAll()
                .antMatchers("/actuator/**").permitAll()
                // 其他请求需要认证
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}
相关推荐
Memory_荒年2 小时前
Dubbo高级实战:从“能用”到“好用”的奇技淫巧
java·后端
Flittly2 小时前
【SpringAIAlibaba新手村系列】(4)流式输出与响应式编程
java·spring boot·spring·ai
yangyanping201082 小时前
广告系统设计二之RTA系统设计
java·spring·mybatis
皙然2 小时前
Redis 持久化机制超详细详解(RDB+AOF 双方案 + 生产实战)
数据库·redis·bootstrap
刘 大 望2 小时前
开发自定义MCP Server并部署
java·spring·ai·语言模型·aigc·信息与通信·ai编程
无籽西瓜a2 小时前
【西瓜带你学设计模式 | 第三期-工厂方法模式】工厂方法模式——定义、实现方式、优缺点与适用场景以及注意事项
java·后端·设计模式·工厂方法模式
Zzxy3 小时前
Spring Security + JWT 简单集成
java·spring boot
2401_827499993 小时前
python核心语法01-数据存储与运算
java·数据结构·python
镜花水月linyi3 小时前
Redis 为什么快?
redis·后端