【黑马点评 - 实战篇01】Redis项目实战(Windows安装Redis6.2.6 + 发送验证码 + 短信验证码登录注册 + 拦截器链 - 登录校验)

目录

一、安装Redis

1、卸载旧版本Redis

2、Windows安装6.2.6版本Redis

二、导入黑马点评项目

1、导入数据库

2、导入后端代码

3、配置maven

4、修改application.yml和config配置

5、启动后端

6、启动前端

三、基于Session实现短信登录

1、业务流程概览

(1)session机制介绍

[2、发送短信验证码 - POST接口](#2、发送短信验证码 - POST接口)

(1)需求分析

(2)代码开发

[3、短信验证码登陆、注册 - POST接口](#3、短信验证码登陆、注册 - POST接口)

(1)需求分析

(2)代码开发

[4、登录校验 - 拦截器 LoginInterceptor](#4、登录校验 - 拦截器 LoginInterceptor)

(1)需求分析

(2)代码开发

四、基于Redis实现短信校验

1、session共享问题

2、Redis实现短信登录业务流程

(1)发送短信验证码流程

(2)短信验证码登录、注册流程

(3)登录校验流程

3、代码优化

[(1)发送短信验证码 - 优化](#(1)发送短信验证码 - 优化)

[(2)短信验证码登录、注册 - 优化](#(2)短信验证码登录、注册 - 优化)

[(3)登录校验 - 优化](#(3)登录校验 - 优化)

[4、解决状态登录刷新问题 - 拦截器链](#4、解决状态登录刷新问题 - 拦截器链)

(1)刷新Token拦截器

(2)登录校验拦截器

(3)MvcConfig配置类

[五、复习回顾 - Redis实现短信登录](#五、复习回顾 - Redis实现短信登录)

[1、发送验证码 - redis流程](#1、发送验证码 - redis流程)

[2、短信验证码登录、注册 - redis流程](#2、短信验证码登录、注册 - redis流程)

[3、登录校验 - 拦截器链流程](#3、登录校验 - 拦截器链流程)


一、安装Redis

1、卸载旧版本Redis

如果你的redis版本太低,先进入Redis安装目录,输入cmd启动控制台,然后输入下面命令进行卸载

然后再把redis文件夹删除就OK了

2、Windows安装6.2.6版本Redis

参考教程https://yonghongtech.csdn.net/681d632ea5baf817cf49a9ed.html?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7Ebaidujs_utm_term%7Eactivity-1-142692413-blog-140069813.235%5Ev43%5Epc_blog_bottom_relevance_base2&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7Ebaidujs_utm_term%7Eactivity-1-142692413-blog-140069813.235%5Ev43%5Epc_blog_bottom_relevance_base2&utm_relevant_index=1

https://link.csdn.net/?target=https%3A%2F%2Fgithub.com%2Fbinghe021%2Fredis-setup%2Freleases%3Flogin%3Dfrom_csdn

上面是下载链接,也可以通过置顶的资源进行下载

解压后双击运行,一直下一步即可

如果要修改密码,参考上面的教程链接,新手不建议设置密码,我这里就先不设置了

在redis安装路径文件夹输入cmd启动控制台

接着输入下面的指令(注意:先粘贴前面一段,按空格,然后再粘贴后面一段,最后按回车

XML 复制代码
redis-server.exe redis.windows.conf

如果出现以上界面说明安装成功,接下来测试一下

重新启动一个控制框

如果出现上面内容说明成功!

二、导入黑马点评项目

1、导入数据库

2、导入后端代码

3、配置maven

下载依赖如果报错 :Failure to transfer org.springframework.boot:spring-boot-starter-data-redis:jar:2.3.12.RELEASE from http://maven.aliyun.com/nexus/content/groups/public/ was cached in the local repository, resolution will not be reattempted until the update interval of alimaven has elapsed or updates are forced.这个错误表明阿里云仓库连接超时

解决方法:在C盘User -> .m2 -> settings.xml中加入下面的代码(更换镜像),然后回到IDE重新下载maven就可以了

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<settings>
    <mirrors>
        <mirror>
            <id>huaweicloud</id>
            <mirrorOf>*</mirrorOf>
            <name>华为云</name>
            <url>https://repo.huaweicloud.com/repository/maven/</url>
        </mirror>
    </mirrors>
</settings>

如果你的User-.m2文件夹下没有setting.xml,那就创建一个,在.m2文件夹下新建文本文件,然后重命名为【settings.xml】,用记事本方式打开,再粘贴上面的代码,最后回到IDE重新下载maven就好了

4、修改application.yml和config配置

然后修改RedissonConfig

5、启动后端

http://localhost:8081/shop-type/list

如果能获取到网页数据,说明后端配置成功

6、启动前端

把资料包中的nginx解压到一个全英文路径的文件夹下

在nginx所在目录下打开cmd,输入下列命令

XML 复制代码
start nginx.exe

最后在刚刚的页面中,按F12调出检查框,然后点击右上角的小手机图标,切换成手机模式

把网址端口改为8080,如果能成功显示,说明前端配置成功

三、基于Session实现短信登录

1、业务流程概览

(1)session机制介绍

  • Session是服务器端的用户状态管理机制。
  • 每个用户首次访问时,服务器会创建唯一的Session ID并通过Cookie返回给浏览器。
  • 后续请求浏览器自动携带此ID,服务器据此找到对应的Session对象。
  • Session数据存储在服务器内存中,包含用户登录状态、验证码等个人信息,各用户的Session完全隔离,有效保障数据安全和会话独立性。

2、发送短信验证码 - POST接口

(1)需求分析

前端页面点击【我的】,则会弹出登陆界面,如果点击【发送验证码】按钮,则前端会向后端发送一个请求,后端负责接收该请求并做相应处理

(2)代码开发

【1】controller层

java 复制代码
    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // 发送短信验证码并保存验证码
        return userService.sendCode(phone, session);
    }

【2】service层

  • 验证手机号格式是否正确
  • 若错误,返回错误信息
  • 若正确,生成验证码
  • 将验证码存入session,用于后续和用户输入的验证码比对
  • 发送验证码(模拟发送,不作为重点)
  • 返回成功信息
java 复制代码
    /**
     * 发送验证码
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.验证手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            //2.如果手机号无效,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //3.如果有效,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4.将验证码存入session
        session.setAttribute("code",code);
        //5.发送验证码(模拟)
        log.debug("您的验证码是:{}",code);
        //6.返回成功
        return Result.ok();
    }

3、短信验证码登陆、注册 - POST接口

(1)需求分析

(2)代码开发

【1】controller层

java 复制代码
    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // 实现登录功能
        return userService.login(loginForm, session);
    }

【2】service层

  • 校验手机号
  • 经验验证码
  • 若不一致,返回错误信息
  • 若一致,通过手机号查询数据库中是否存在该用户
  • 如果不存在,创建一个新用户保存到数据库中
  • 将用户保存到session中
  • 返回成功信息
java 复制代码
    /**
     * 短信实现登陆、注册
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机格式错误!");
        }

        //2.校验验证码
        String code = loginForm.getCode();
        Object cacheCode = session.getAttribute("code");

        if(cacheCode == null || !cacheCode.equals(code)){
            //3.不一致,返回错误信息
            return Result.fail("验证码错误!");
        }

        //4.一致,查询数据库是否存在用户
        User user = query().eq("phone", phone).one(); //mybatis-plus实现用手机号查询用户信息

        //5.用户不存在,创建新用户
        if(user == null){
            user = createUserWithPhone(phone);
        }
        //6.将用户保存进session
        //每一个用户的Session都是分开的,不是公共的
        //每个用户首次访问时,服务器生成唯一的Session ID
        session.setAttribute("user",user);

        return Result.ok();
    }

    private User createUserWithPhone(String phone) {
        // 1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        // 2.保存用户
        save(user);
        return user;
    }

4、登录校验 - 拦截器 LoginInterceptor

(1)需求分析

  • 如果没有拦截器,每个控制器方法都需重复编写登录验证、权限检查、日志记录等通用逻辑
  • 例如:用户权限校验代码会在订单查询、个人中心等数十个接口中重复出现,导致代码冗余、维护困难。
  • 拦截器通过统一预处理将这些横切关注点集中管理,极大提升代码复用性和系统可维护性。

(2)代码开发

【1】controller层

java 复制代码
    @GetMapping("/me")
    public Result me(){
        // 获取当前登录的用户并返回
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }

【2】拦截器

  • 获取session

  • 获取session中的用户

  • 判断用户是否存在,若不存在则报错401,不放行

  • 若存在,保存用户到ThreadLocal(用UserHolder方法实现)

  • 放行

preHandle 负责进门前的检查和准备,而afterCompletion 负责离开后的打扫和清理,两者共同构成了一个完整且安全的请求处理闭环

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取session
        HttpSession session = request.getSession();

        //2.获取session中的用户
        Object userDTO = session.getAttribute("user");

        //3.判断用户是否存在
        if(userDTO == null){
            //4.不放行
            response.setStatus(401);
            return false;
        }
        //5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser((UserDTO) userDTO);

        //6.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除用户
        UserHolder.removeUser();
    }
}

【3】Mvc配置文件

编写好拦截器后要在配置文件中添加拦截器,并规定【无需拦截的接口】

java 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns( //排除不需要拦截的接口
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
    }
}

四、基于Redis实现短信校验

1、session共享问题

  • 多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时会导致数据丢失
  • 为了解决共享问题,我们采用独立于多个Tomcat之外的Redis存储器

2、Redis实现短信登录业务流程

(1)发送短信验证码流程

  • 由【保存验证码到session】改变为【保存验证码到Redis】,Redis数据结构采用【String】比较直观【key:phone | value:code】

(2)短信验证码登录、注册流程

  • 校验验证码时,以手机号为key,从Redis中获取验证码
  • 由【保存用户信息到session】改变为【保存用户信息到Redis】,Redis数据结构采用【hash】比较合适【key:token | value:name:lisi】
  • 最后要把token返回给客户端,用于登录校验

(3)登录校验流程

  • 由【请求并携带token】改变为【请求并携带token】,用token为key,在Redis中获取用户信息
  • 登录校验时,为什么用token作为key从redis中获取用户信息,而不用手机号作为key?
  • 答:

3、代码优化

(1)发送短信验证码 - 优化

java 复制代码
    /**
     * 发送验证码 - session优化
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.验证手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            //2.如果手机号无效,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        //3.如果有效,生成验证码
        String code = RandomUtil.randomNumbers(6);

        //【4.将验证码存入redis】新!!
        //采用String存储,【key:业务前缀+phone  value:code  过期时间:2min】
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone,code,LOGIN_CODE_TTL,TimeUnit.MINUTES);

        //5.发送验证码(模拟)
        log.debug("您的验证码是:{}",code);
        //6.返回成功
        return Result.ok();
    }

(2)短信验证码登录、注册 - 优化

问题1:将用户信息保存进Redis

  • 随机生成token作为登录令牌,也作为用户信息的key
  • 将【User对象】转换为【UserDTO】再转换为【hashmap】,转换为hashmap是方便Redis的hash格式存储(key | value:field:value)
  • 存入Redis的hash结构中,用putAll可以一次性把用户所有属性存入(适用于传入map结构)
  • 设置token有效期(因为用户会源源不断地登录,如果不把不活跃的用户token清理,redis就会承受不住)

问题2:为什么要把UserDTO转换为String类型的Map?下面这段复杂代码是干什么的?

java 复制代码
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
  • 因为Redis Hash 更适合存储字符串值,而UserDTO中的id为Long类型,为了更加适配Redis Hash,这里需要自定义字段值编辑器,把所有属性类型转换为String
java 复制代码
    /**
     * 短信实现登陆、注册
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机格式错误!");
        }

        //【2.从redis中获取验证码并校验】新!!
        String code = loginForm.getCode();
        Object cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);

        if(cacheCode == null || !cacheCode.equals(code)){
            //3.不一致,返回错误信息
            return Result.fail("验证码错误!");
        }

        //4.一致,查询数据库是否存在用户
        User user = query().eq("phone", phone).one(); //mybatis-plus实现用手机号查询用户信息

        //5.用户不存在,创建新用户
        if(user == null){
            user = createUserWithPhone(phone);
        }

        //【6.将用户保存进redis】新!!
        //6.1 随机生成token作为登录令牌
        String token = UUID.randomUUID().toString(true);

        //6.2 将User对象转换为hashMap
        UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);
        //将UserDTO对象安全地转换为String类型的Map,忽略空值字段,确保数据格式统一,便于存储到Redis Hash结构中。
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));

        //6.3 存储 用putAll可以一次性把用户所有属性传进去
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token,userMap);
        //6.4 设置token有效期(30min)
        stringRedisTemplate.expire(LOGIN_USER_KEY + token,LOGIN_USER_TTL,TimeUnit.MINUTES);

        //【7.返回token】新!!
        return Result.ok(token);
    }

(3)登录校验 - 优化

问题一:为什么要在拦截器刷新token有效期?

  • 因为拦截器能拦截所有需要登录的请求,确保用户任何有效操作都会触发续期。
  • 用户每次操作都刷新过期时间,保持活跃会话,避免中途退出,同时自动清理不活跃的会话以释放资源。
java 复制代码
public class LoginInterceptor implements HandlerInterceptor {

    //不能使用 @Autowired 是因为拦截器实例是在配置类中手动创建的
    // Spring 无法对手动 new 的对象进行依赖注入。
    // 构造函数注入是更明确、更安全的依赖管理方式。
    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //【1.获取请求头中的token】新!!
        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){
            //不放行
            response.setStatus(401);
            return false;
        }

        String key = RedisConstants.LOGIN_USER_KEY + token;

        //【2.基于token获取redis中的用户】新!!
        //entries() 方法是用于获取 Redis Hash 结构中的所有字段和值
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);

        //3.判断用户是否存在
        if(userMap.isEmpty()){
            //4.不放行
            response.setStatus(401);
            return false;
        }

        //【5.将查询到的hash数据转换为DTO】新!!
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        //6.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);

        //7.刷新token有效期
        //为什么要在拦截器刷新token有效期?
        //答:因为拦截器能拦截所有需要登录的请求,确保用户任何有效操作都会触发续期。
        // 用户每次操作都刷新过期时间,保持活跃会话,避免中途退出,同时自动清理不活跃的会话以释放资源。
        stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        //8.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除用户
        UserHolder.removeUser();
    }
}

4、解决状态登录刷新问题 - 拦截器链

原方案中,Token的刷新依赖于登录校验拦截器。这意味着只有当用户访问需要登录的接口时,Token有效期才会被续期。如果用户长时间只访问公开接口(如首页、商品列表),即使他处于活跃状态,Token也可能因过期而失效,导致其在后续操作中意外退出。

优化点:

  • 我们解耦了【身份校验】和【Token刷新】这两个关注点,新增一个独立拦截器,并将其置于拦截器链的首位,以拦截所有路径。
  • 新拦截器职责:拦截所有路径,仅负责检查请求是否携带Token,以Token为key查询用户信息是否在Redis中。若Token有效且在Redis中,则将用户信息保存到ThreadLocal线程中,并刷新其有效期,无论该接口是否需要登录。
  • 原拦截器职责:拦截需要登录的路径,仅判断线程ThreadLocal中是否存在该用户,如果存在则放行,否则拦截。

改进优点

  • 此设计确保了只要用户在与应用进行任何交互,其登录状态就能得以保持,从而提供了连贯的用户体验,避免了活跃会话的中断。

(1)刷新Token拦截器

  • 从redis中获取token
    • 如果token不存在 → 放行(这时候用户信息并没有在线程ThreadLocal中,会被登录校验拦截器拦截)
  • 如果token存在,从Redis中获取用户信息
    • 如果用户不存在 → 放行(这时候用户信息并没有在线程ThreadLocal中,会被登录校验拦截器拦截)
  • 如果用户存在,将用户存入线程ThreadLocal
  • 刷新该用户的token
  • 放行
java 复制代码
public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true; //这里先放行,交给下一个拦截器判断
        }

        // 2.基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);

        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true; //这里先放行,交给下一个拦截器判断
        }

        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal(只有在redis中查到用户信息的才可以保存到ThreadLocal,其他的都会被第二个拦截器拦截)
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

(2)登录校验拦截器

  • 看线程ThreadLocal中是否存在该用户
    • 如果不存在 → 报错401,拦截
    • 如果存在 → 放行
java 复制代码
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断是否要拦截【ThreadLocal中是否有用户信息】
        if (UserHolder.getUser() == null) {
            //不存在该用户,拦截
            response.setStatus(401);
            return false;
        }
        //存在该用户,放行
        return true;
    }
}

(3)MvcConfig配置类

用于添加拦截器

java 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 添加拦截器
        //第一个拦截器:负责拦截需要【登录】的路径,做出是否拦截判断
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns( //排除不需要拦截的接口
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        //第二个拦截器:负责拦截【所有】路径,负责将存在redis的用户存入ThreadLocal,并刷新token
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

五、复习回顾 - Redis实现短信登录

这个课程相比起苍穹外卖真的很烧脑,所以把今天学习的重点【Redis实现短信登录】功能总结回顾一下,主要梳理【发送验证码】【短信验证码登录、注册】【登录校验】三个模块的开发业务逻辑。

1、发送验证码 - redis流程

2、短信验证码登录、注册 - redis流程

3、登录校验 - 拦截器链流程

相关推荐
xqqxqxxq2 分钟前
MySQL 数据类型笔记
数据库·笔记·mysql
知彼解己13 分钟前
Java 版本演进
java·开发语言·spring boot
Logintern0925 分钟前
理解MySQL数据库的“事务与锁”
数据库·mysql
朱容zr3331331 小时前
为什么推荐使用自增主键?使用UUID作为主键的优缺点是什么?
java·运维·数据库·后端·mysql·面试·性能优化
麻瓜code1 小时前
【Redis 】数据类型、持久化与过期删除
数据库·redis·缓存
空杆推不起1 小时前
穿透 Flink CDC 表层用法:数据库日志捕获机制、Flink Source 运行时、端到端一致性底层原理详解
大数据·数据库·flink
朱容zr3331331 小时前
请解释“回表”的概念。
java·前端·数据库
☆凡尘清心☆2 小时前
CentOS Stream 9 编译安装 Redis 7.2.7 完整版详细步骤
linux·redis·centos
大黄说说2 小时前
EF Core 避坑指南:查询慢、循环查询、并发更新问题如何解决
java·服务器·数据库
城管不管2 小时前
重生——第五次面试2026.8.1一面
java·数据库·后端·ai·面试·职场和发展·agent