SaaS短链接系统-新手从零学习 2

eve2333 发布于 2025-05-14 23 次阅读


 用户敏感信息接口返回脱敏

我们看一下12306是如何实现这个的,我们在进入订单提交页的时候,就是在你选好车次的时候选择乘车人的时候他的证件号码里面是加星号的,然后如果说证件号码没有加*,如果有人截断你的流量,并且会解包,敏感信息就泄露了,更不能用前端处理的方式,前端是给不懂的人玩的,post打一下接口就看出了。

所以,就是说我们从后端返回数据的时候,就要把这个数据是已经脱敏后的,常见的这种脱敏的这种信息类别的话,手机号身份证家庭住址;UserController改成下面即可

@GetMapping("/api/shortlink/v1/user/{username}")
public Result<UserRespDTO> getUserByUsername(@PathVariable("username")String username) {
    return Results.success(userService.getUserByUsername(username));
}

当我们apifox去get一下时,返回的json里面phone个手机号算是一个敏感信息,一般网上有两三种解决方案:

我们可以在Controller加一个@XXX的自定义注解,相当于用AOP的式扫描这个注解,上面带着这个注解的话,就从你的返回去给你一层层的去反射递归直到拿到你的这个上面DTO,我们在UserRespDTO里面比如给phone去@XXX(xxx=手机号类型),他就会去标记说你这个是什么类型,比如他拿到这个手机号类型做透明展示,但是这种太复杂; 

还可以JSON默认序列化的方式,给他进行一个额外的处理,然后返回的话就是正常脱敏后的。就是我们UserController  这里明明返回的是对象,怎么到了前端这里返回的请求这里为什么是json?因为springboot在web请求里面,默认通过jackson框架序列化的方式。这样就可以利用jackson框架了

在common下创建serialize包,新建PhoneDesensitizationSerializer.java,第一个值是手机号本身的一个值,第二个它是对象本身的一个通用的虚拟化器,第三个就是SerializerProvider(序列化生产值,没有用到)用序列化的写入器写到我们的一个目标的一个对象里面去

package com.nageoffer.shortlink.admin.common.serialize;
import cn.hutool.core.util.DesensitizedUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;

/**
 * 手机号脱敏反序列化
 */
public class PhoneDesensitizationSerializer extends JsonSerializer<String> {

    @Override
    public void serialize(String phone, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        String phoneDesensitization = DesensitizedUtil.mobilePhone(phone);
        jsonGenerator.writeString(phoneDesensitization);
    }
}

我们只需要在对应的包UserRespDTO ,phone的部分上面加对应的监听的序列化器即可

/**
 * 手机号
 */
@JsonSerialize(using  = PhoneDesensitizationSerializer.class)
private String phone;

这样可以通过Jackson去读取出来你这个字段给它进行一些泛解析,,如果是一定需要手机号字段呢?可以给他再出一个新的接口,controller里面写如下:(BeanUtil.toBean 方法只是复制属性值,并不会触发 @JsonSerialize 注解指定的序列化器,所以 phone 属性的值不会被脱敏处理。)

@GetMapping("/api/shortlink/v1/actual/user/{username}")
public Result<UserActualRespDTO> getActualUserByUsername(@PathVariable("username")String username) {
    return Results.success(BeanUtil.toBean(userService.getUserByUsername(username), UserActualRespDTO.class));
}

Resp里面新建类 UserActualRespDTO.java

package com.nageoffer.shortlink.admin.dto.resq;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName
public class UserActualRespDTO {
    /**
     * id
     */
    private Long id;
    /**
     * 用户名
     */
    private String username;
    /**
     真实姓名
     */
    private String realName;
    /**
     * 手机号
     */

    private String phone;
    /**
     * 邮箱
     */
    private String mail;
}

检查用户名是否存在功能

用户名全局为唯一的功能,事实上,这个是给用户自己取了名字,真正的用户名应该后台默认依次取一下id,类似于游戏uid,抖音号等;还有一种是什么英文+数字+符号(下划线)等这个会当作你用户的一个唯一标识,最好是填写的时候就小div告诉你已经注册

为了实现这个功能,需要实现接口hasUsername,impl如下,controller,service部分省略

    @Override
    public Boolean hasUsername(String username) {
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
                .eq(UserDO::getUsername, username);
        UserDO userDO = baseMapper.selectOne(queryWrapper);
        return userDO == null;
    }
    /**
     * 查询用户名是否存在
     */
    @GetMapping("/api/shortlink/v1/user/has-user-name")
    public Result<Boolean> hasUserName(@RequestParam("username")String username) {
        return Results.success(userService.hasUsername(username));
    }

这样就是用户管理的直接模型

存在什么问题?

  • 海量用户如果说查询的用户名存在或不存在,全部请求数据库,会将数据库直接打满。

 如何防止检查用户名缓存穿透(理论篇)

1. 用户名加载缓存

第一版解决方案,将数据库已有的用户名全部放到缓存里。

流程图:

该方案问题:

  • 是否要设置数据的有效期?只能设置为无无有效期,也就是永久数据。
  • 如果是永久不过期数据,占用 Redis 内存太高,设了也没用。

2. 布隆过滤器

第二版解决方案,使用布隆过滤器。

流程图:

2.1. 什么是布隆过滤器

布隆过滤器是一种数据结构,用于快速判断一个元素是否存在于一个集合中。具体来说,布隆过滤器包含一个位数组和一组哈希函数。位数组的初始值全部置为 0。在插入一个元素时,将该元素经过多个哈希函数映射到位数组上的多个位置,并将这些位置的值置为 1。

1字节(Byte)=8位(Bit)

在查询一个元素是否存在时,会将该元素经过多个哈希函数映射到位数组上的多个位置,如果所有位置的值都为 1,则认为元素存在;如果存在任一位置的值为 0,则认为元素不存在。

2.2. 优缺点

优点:

  • 高效地判断一个元素是否属于一个大规模集合。
  • 节省内存。

缺点:

  • 可能存在一定的误判。
2.3. 布隆过滤器误判理解
  • 布隆过滤器要设置初始容量。容量设置越大,冲突几率越低。
  • 布隆过滤器会设置预期的误判值。
2.4. 误判能否接受

布隆过滤器的误判是否能够接受?

答:可以容忍。为什么?因为用户名不是特别重要的数据,如果说我设置用户名为 aaa,系统返回我不可用,那我大可以在 aaa 的基础上再加一个a,也就是 aaaa。

2.5. 布隆过滤器流程图

初始化流程图:

执行流程图:

布隆过滤器的核心特性决定了其在不同场景下的使用方式,针对问题和回复的分析如下:


一、布隆过滤器的特性

  1. 无假阴性(False Negative)
    若布隆过滤器返回「不存在」,则数据必然不在底层存储(如数据库)中。
  2. 存在假阳性(False Positive)
    若返回「存在」,实际可能不存在(哈希碰撞导致),需二次验证。

二、原问题与回复的正确性分析

1. 用户的核心疑问

当布隆过滤器返回「存在」时,能否通过二次验证(如查询缓存)减少误判影响?

答案:可行但需权衡

  • 逻辑有效性
    布隆过滤器仅保证「不存在」的准确性,但对「存在」的判断需结合其他机制验证。用户提出的「布隆过滤器→缓存→数据库」链式查询,本质是用缓存作为二次验证层,可降低误判导致的数据库压力。
  • 性能权衡
    每次请求增加一次缓存查询(如Redis),会略微增加延迟,但相比数据库查询仍高效。若误判率较低,额外的缓存查询成本可接受。

2. 回复观点辨析

  • 乌拉呀哈氏(2024-06-16)
    ✔️ 正确。布隆过滤器设计初衷是快速排除不存在的请求,存在时需其他验证。
  • 乌拉呀哈氏(2024-06-21)
    ❌ 部分错误。误判在技术上不可消除,但某些场景(如容忍少量误判)可能接受。若业务要求严格一致性(如支付系统),误判不可接受。
  • Venom(2024-10-09)
    ✔️ 部分正确。哈希碰撞是误判根源,但用户方案通过缓存验证可缓解误判影响,逻辑合理。
  • 皮蛋瘦肉粥(2024-10-27)
    ✔️ 正确。误判率与哈希函数数量、位数组大小相关。降低误判率需增加哈希函数或位数组空间,可能牺牲性能。
  • 老汉堡(2024-12-10)
    ❌ 错误。布隆过滤器的「存在」判断本就需要二次验证,这是其设计特性,而非缺陷。严格场景下(如金融),需结合持久化存储验证,但非所有场景都需如此。

三、优化方案的可行性

用户提出的「布隆过滤器 + 缓存验证」方案是否合理?

1. 优点

  • 降低数据库压力 :布隆过滤器拦截无效请求,误判时缓存验证避免直接查库。
  • 减少误判影响 :缓存未命中时才触发数据库查询,误判不会直接导致数据库负载升高。

2. 潜在问题

  • 缓存穿透风险 :若布隆过滤器误判率高,可能导致大量无效缓存未命中,增加数据库查询。
  • 缓存雪崩风险 :若缓存过期时间相同,可能引发数据库瞬时压力激增。
  • 空间与复杂度 :需维护缓存与布隆过滤器的一致性,增加系统复杂度。

3. 改进建议

  • 设置低误判率的布隆过滤器 :通过调整哈希函数数量和位数组大小,将误判率控制在极低水平(如0.1%),减少二次验证的频率。
  • 缓存空值(Negative Caching) :对数据库查询结果为空的请求,缓存短时间的空值,防止重复穿透。
  • 异步更新布隆过滤器 :当数据库新增数据时,主动更新布隆过滤器,避免因异步延迟导致的误判。

四、结论

  • 布隆过滤器的定位 :仅用于快速排除「不存在」的请求,存在时需依赖其他存储(缓存/数据库)验证。
  • 用户方案的合理性 :在性能可接受的前提下,通过缓存验证减少误判影响是合理的优化,但需结合缓存过期策略和低误判率参数设置。
  • 回复的正确性
    • 乌拉呀哈氏和皮蛋瘦肉粥的观点正确,指出了布隆过滤器的核心特性。
    • Venom和老汉堡的表述有偏差,未准确理解用户方案的目标和布隆过滤器的适用范围。

最终结论 :用户的思路正确,布隆过滤器的「存在」判断需二次验证,但需根据业务场景权衡实现复杂度和性能。

3. 代码中使用布隆过滤器

 导入redis配置yaml和pom文件

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
server:
  port: 8002

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/link?characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      connection-test-query: select 1
      connection-timeout: 20000
      idle-timeout: 300000
      maximum-pool-size: 5
      minimum-idle: 5
  data:
    redis:
      host: 192.168.111.130
      password: 123321
      port: 6379

 在admin新建软件包config,里面新建java文件

package com.nageoffer.shortlink.admin.config;

import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 布隆过滤器配置
 */
@Configuration
public class RBloomFilterConfiguration {

    /**
     * 防止用户注册查询数据库的布隆过滤器
     */
    @Bean
    public RBloomFilter<String> userRegisterCachePenetrationBloomFilter(RedissonClient redissonClient) {
        RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter("userRegisterCachePenetrationBloomFilter");
        cachePenetrationBloomFilter.tryInit(0, 0);
        return cachePenetrationBloomFilter;
    }
}

tryInit 有两个核心参数:

  • expectedInsertions:预估布隆过滤器存储的元素长度。
  • falseProbability:运行的误判率。

错误率越低,位数组越长,布隆过滤器的内存占用越大。

错误率越低,散列 Hash 函数越多,计算耗时较长。

一个布隆过滤器占用大小的在线网站:Bloom Filter Calculator

散列函数的function约等于10, 你看到1亿条才约等于171.39MB;我们将代码里l和v设为100000000L, 0.001

 impl实现如下,用RequiredArgsConstructor导入

@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, UserDO> implements UserService {

    private final RBloomFilter<String> userRegisterCacheBloomFilter;
    @Override
    public UserRespDTO getUserByUsername(String username) {
        LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
                .eq(UserDO::getUsername, username);
        UserDO userDO = baseMapper.selectOne(queryWrapper);
        if (userDO == null) {
            throw new ClientException(UserErrorCodeEnum.USER_NULL);
        }
        UserRespDTO result = new UserRespDTO();
        BeanUtils.copyProperties(userDO,result);
        //Spring 的 BeanUtils.copyProperties(Object source, Object target) 方法要求源对象在前,目标对象在后
        return result;
    }
    @Override
    public Boolean hasUsername(String username) {
        return userRegisterCacheBloomFilter.contains(username);
    }
}

因为现在这里面是没有元素的,我们要在有了注册功能之后,注册以后就把用户名给放到布隆过滤器里面去

使用布隆过滤器的两种场景:

  • 初始使用:注册用户时就向容器中新增数据,就不需要任务向容器存储数据了。
  • 使用过程中引入:读取数据源将目标数据刷到布隆过滤器。

问题1:在使用数据库直接查询可以查询到已有的username,但是使用布隆过滤器查询却没有这是为什么?

解答:因为此时布隆过滤器里面是没有元素的,需要在后面有了注册功能之后,把用户名放在布隆过滤器才可以查询到。

布隆过滤器是一种概率数据结构,其本质是通过多个哈希函数将元素映射到位数组上。若元素未被显式添加(如用户注册时未将用户名插入布隆过滤器),即使数据库中存在该用户名,布隆过滤器仍会返回「不存在」。因此,需在用户注册时主动将用户名插入布隆过滤器,否则无法检测到。例如:

  • 用户 Alice 注册时未插入布隆过滤器 → 布隆过滤器判定 Alice 不存在。
  • 数据库中 Alice 存在 → 直接查询数据库能查到。
    结论 :布隆过滤器需与注册逻辑绑定,确保数据同步更新。

问题2:请问这个布隆过滤器怎么设置过期时间呢,从数据库中删除用户,但是redis中的还在,删除的用户名不能重新注册了
回答:

布隆过滤器本身不支持删除操作 (普通版本)(增强版,布谷鸟过滤器),原因如下:

  1. 哈希碰撞问题 :多个元素可能共享同一位,删除一个元素会导致其他元素的误判。
  2. 过期时间限制 :布隆过滤器的位数组是静态的,无法直接设置TTL。
  • 正确解决方案
    计数布隆过滤器(Counting Bloom Filter) :使用计数器代替位数组,支持删除(需额外空间)。
  • 缓存空值(Negative Caching) :对已删除的用户名缓存短时间的空值,避免穿透。
  • 定时重建布隆过滤器 :定期从数据库全量同步数据到布隆过滤器,覆盖过期数据。

同时短链接有个面试问题解答文档可以看下,有说这种带删除的解决方案

 问题3:如果已经使用了布隆过滤器,但一开始只设置了布隆过滤器元素长度为1千万。项目跑了一段时间发现用户注册数远超于1千万,这时候将元素长度设置成1亿,那么布隆过滤器已存在的历史数据是否需要删掉,然后用任务方式,重新往布隆过滤器里面设置值?还是说历史数据不会被影响,依旧能被查询得到?

回答:位数组长度变的话,哈希函数就要重新选择了,要将历史数据通过新的哈希函数重新映射到新的位数组上

 问题4:加入redis挂了重启后,原布隆过滤器中的数据会丢失,这种还是要通过任务重新初始化布隆过滤器吧?

回答:

布隆过滤器的数据是否丢失取决于Redis的持久化配置:

  1. 持久化开启(RDB/AOF) :布隆过滤器的位数组数据会被保存,重启后可恢复。
  2. 持久化未开启 :数据丢失,需通过任务重新初始化布隆过滤器(如从数据库全量加载)。
    补充建议
  • 定期备份布隆过滤器(如导出位数组到文件)。
  • 使用Redis的 SAVEBGSAVE 手动触发持久化。

用户如何实现海量请求注册功能  

package com.nageoffer.shortlink.admin.dto.req;

import lombok.Data;

@Data
public class UserRegisterReqDTO {
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 邮箱
     */
    private String email;
    /**
     * 手机号
     */
    private String phone;
    /**
     * 真实姓名
     */
    private String realName;
}

 我相信大家有很多人都是用的userRegisterReqDTO,这样其实显得非常复杂也不统一,老师的习惯像这种对象就用requestparam,一整个对象,很多很多不同的对象建议requestparam代替;
为UserErrorCodeEnum.java添加错误码

USER_NAME_EXIST("B000201","用户名已存在"),
USER_EXIST("B000202","用户记录已存在");
USER_SAVE_ERROR("B000203","用户注册失败");

用户注册流程图: 

 下面是controller,service以及impl,为了不显示波浪线,将所有url的shortlink改成了short-link

    /**
     * 注册用户
     */
    @PostMapping("/api/short-link/v1/user")
    public Result<Void> register(@RequestBody UserRegisterReqDTO requestParam) {
        userService.Register(requestParam);
        return Results.success();
    }
    /**
     * 注册用户
     *
     * @param requestParam 注册用户请求参数
     */
    void Register(UserRegisterReqDTO requestParam);

    @Override
    public void Register(UserRegisterReqDTO requestParam) {
        if (hasUsername(requestParam.getUsername())) {
            throw new ClientException(USER_NAME_EXIST);
        }
        int inserted = baseMapper.insert(BeanUtil.toBean(requestParam, UserDO.class));
        if (inserted < 1) {
            throw new ClientException(USER_SAVE_ERROR);
        }
    }    

你发现mail和create update time 是空的,所以userRegisterReqDTO里面email应该给到mail,还有两个time可以用mybatis-plus实现:每个表里面基本上都有这3个字段,新增的时候要保证这三个要指定的值,比如说这两个时间就是当地时间,flag默认0,

接下来自定义在config中设置MyMetaObjectHandler,UserDTO里面为createtime,updatetime和delflag设置标记

   /*创建时间*/
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
    /**
     * 修改时间
     */
    @TableField(fill = FieldFill.INSERT)
    private Date updateTime;
    /**
     * 删除标识0:未删除1:已删除
     */
    @TableField(fill = FieldFill.INSERT)
    private Integer delFlag;
package com.nageoffer.shortlink.admin.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(metaObject, "delFlag", () -> 0, Integer.class);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
    }
}

由于自动类型没有匹配上,因此修改成如下内容:

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        strictInsertFill(metaObject, "createTime", Date::new, Date.class);
        strictInsertFill(metaObject, "updateTime", Date::new, Date.class);
        strictInsertFill(metaObject, "delFlag", () -> 0, Integer.class);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
    }
}

接下来给username加一个唯一的限制,哪怕布隆过滤器也要有一定几率错误,底层redis在主从复制的时候,哪怕是集群主从复制,也有几率主挂了,从没接到,那部分数据就变成脏数据了,因为脏数据在数据库中没有兜底

在 Navicat Premium 中,右键单击要添加索引的表,选择 “设计表”。点击 “索引” 选项卡,再点击 “添加索引” 按钮。添加唯一索引,你可以尝试一下,这样就能看到“重复的对象tom id_unique_username”

如何防止用户名重复?

通过布隆过滤器把所有用户名进行加载。这样该功能就能完全隔离数据库。

数据库层面添加唯一索引。

@Override
public void Register(UserRegisterReqDTO requestParam) {
    if (hasUsername(requestParam.getUsername())) {
        throw new ClientException(USER_NAME_EXIST);
    }
    int inserted = baseMapper.insert(BeanUtil.toBean(requestParam, UserDO.class));
    if (inserted < 1) {
        throw new ClientException(USER_SAVE_ERROR);
    }
    userRegisterCacheBloomFilter.add(requestParam.getUsername());
}

 2. 如何防止恶意请求毫秒级触发大量请求去一个未注册的用户名?

因为用户名没注册,所以布隆过滤器不存在,代表着可以触发注册流程插入数据库。但是如果恶意请求短时间海量请求,这些请求都会落到数据库,造成数据库访问压力。这里通过分布式锁,锁定用户名进行串行执行,防止恶意请求利用未注册用户名将请求打到数据库。

3. 如果恶意请求全部使用未注册用户名发起注册 

结论:系统无法进行完全风控,只有通过类似于限流的功能进行保障系统安全 

 UserController加个非,impl也是

public Result<Boolean> hasUserName(@RequestParam("username")String username) {
    return Results.success(!userService.hasUsername(username));
}
public Boolean hasUsername(String username) {
    return !userRegisterCacheBloomFilter.contains(username);
}
if (!hasUsername(requestParam.getUsername())) {
    throw new ClientException(USER_NAME_EXIST);
}

返回false说明预期结果正常

 接下来进一步改造,redisson分布式锁有一种看门狗的机制,更安全,底层用net做的网络通信更高效,在constant中新建如下文件

package com.nageoffer.shortlink.admin.common.constant;

/**
 * redisson 的常量命名
 */
public class RedisCacheConstant {
    public static final String LOCK_USER_REGISTER_KEY = "short-link:lock-user-register:";
}
    @Override
    public void Register(UserRegisterReqDTO requestParam) {
        if (!hasUsername(requestParam.getUsername())) {
            throw new ClientException(USER_NAME_EXIST);
        }
        //后面应该加一个requestParam.getUsername(),不拼的话就变成全局锁了,拼了才会两个加一起整体的锁
        RLock lock = redissonClient.getLock(LOCK_USER_REGISTER_KEY+requestParam.getUsername());
        /**
         * 这里不能用lock,因为它会一直等待你上一个锁释放
         * 这里不需要,只要有一获取到锁,我们就认为这个能获取到锁的用户可以注册成功
         *  所以说其他那种没获取到锁的直接try lock,try lock之后我们也不用去给他等,直接注册
         *  try lock失败的
         */
        try {
            if (lock.tryLock()) {
                int inserted = baseMapper.insert(BeanUtil.toBean(requestParam, UserDO.class));
                if (inserted < 1) {
                    throw new ClientException(USER_SAVE_ERROR);
                }
                userRegisterCachePenetrationBloomFilter.add(requestParam.getUsername());
                return;
            }
            throw new ClientException(USER_NAME_EXIST);
        } finally {
            lock.unlock();
        }
    }

通过分布式锁完善用户注册功能,防止恶意请求段时间使用统一未注册用户名注册新用户

 如果海量用户怎么办???

一些问题 

### **问题1:可以使用MyBatis Plus的逻辑删除吗?**
**回答正确性:✅ 正确(需补充说明)**  
**分析**:  
MyBatis Plus的逻辑删除配置(`logic-delete-field`、`logic-delete-value`等)会在以下场景生效:  
- **查询时**:自动添加 `WHERE flag = 0` 条件,过滤已逻辑删除的数据。  
- **更新时**:调用 `removeById` 等方法时,自动将 `flag` 设为 `1`(逻辑删除)。  
**代码中的实践**:  
- 用户注册时插入数据(`baseMapper.insert(...)`)需显式设置 `flag` 字段值(如 `0`),否则数据库默认值或MP自动填充需保证。  
- 若未配置默认值且未手动赋值,可能导致插入数据时 `flag` 为 `NULL`,逻辑删除失效。  
**结论**:  MP提供的这个配置是查询和更新时自动拼接逻辑删除字段值,马哥代码写的是插入数据库填充的逻辑删除字段的值,就不用在数据库里设置默认值了,个人理解,有错请指正
- **MP逻辑删除配置有效**,但需确保插入操作时逻辑删除字段有值(通过数据库默认值或代码自动填充)。  
- 原回答正确,但需明确插入数据时需保证逻辑删除字段初始化。

### **问题2:用户名唯一索引的重要性及分布式锁问题**

1. 用户名设置唯一索引的重要性,从目前代码来看,可能会出现插入数据库成功,但写布隆过滤器失败的情况。如果又来一个该用户名的请求,代码会执行到插入数据库这行,这时如果没有唯一索引的限制的话,会出现重复用户名记录。
2. 继续体会没有银弹这句话,代码中通过加分布式锁防止多个请求注册同一用户名的情况。但是也可能出现,拿到分布式锁的请求因为某些请求没有成功注册该用户名,而同一时间其它请求因为没有拿到分布式锁,而显示用户名已被使用(实际上并没有被使用)。从用户的角度出发,他并不知道用户名并没有被使用,而后续的用户如果继续使用该用户名注册也是可以的。

**回答正确性:✅ 正确(需补充解决方案)**  
#### **1. 唯一索引的必要性**  
- **问题**:若数据库无唯一索引,插入成功但布隆过滤器写入失败时,后续请求可能因布隆过滤器误判(认为用户不存在)而再次插入,导致重复数据。  
- **解决方案**:  
  - **数据库唯一索引**:确保用户名字段(如 `username`)有唯一约束,防止重复插入。  
  - **布隆过滤器重试机制**:插入数据库后,若布隆过滤器写入失败,通过异步任务重试或补偿机制更新布隆过滤器。  

#### **2. 分布式锁的误判问题**  
- **问题**:持有锁的线程因异常(如超时、网络故障)未完成注册,其他线程因未获取锁而误判用户名已被占用。  
- **解决方案**:  
  - **锁续期机制**:使用Redisson的看门狗(Watchdog)自动延长锁过期时间。  
  - **注册状态检查**:获取锁失败前,先检查数据库或布隆过滤器是否已存在该用户名,避免误判。  

**结论**:用户指出的问题正确,需结合唯一索引和锁优化方案解决。

问题3:为何使用MP自动填充而非数据库默认语句'`update_time` datetime DEFAULT current_timestamp on update current_timestamp COMMENT '修改时间''? 
**回答正确性:✅ 正确(需对比优缺点)**  

MP自动填充 vs 数据库默认值

特性MP自动填充数据库默认值
可控性代码层控制,避免依赖数据库配置数据库层控制,代码无需处理
灵活性可根据业务逻辑动态设置值固定值,无法动态调整
一致性需确保插入操作始终经过MP逻辑,否则失效所有插入操作均生效(包括手动SQL)
可维护性代码集中管理,易维护分散在数据库配置中,易被忽略

**原回答补充**:  
- **MP自动填充优势**:避免程序员忘记赋值,且可在业务逻辑中动态调整(如根据条件设置不同值)。  
- **数据库默认值优势**:对所有插入操作生效(包括非MP调用的SQL)。  
**结论**:两者均可,MP自动填充更适合需要动态控制的场景。

问题4:`hasUsername` 方法不加 `!` 是否正确?**
**回答正确性:❌ 错误(代码逻辑错误)**  
#### **布隆过滤器逻辑分析**  
- **布隆过滤器用途**:用于快速判断元素是否存在,但存在假阳性(可能误判存在)。  
- **代码中的 `hasUsername` 方法**:  
  public Boolean hasUsername(String username) {
      return !userRegisterCachePenetrationBloomFilter.contains(username);
  }
  - **`contains` 方法返回 `true`**:表示用户名可能存在(需进一步验证)。  
  - **`contains` 返回 `false`**:用户名一定不存在。  
- **当前逻辑问题**:  
  - `!contains(...)` 的含义是「用户名不存在」,但 `hasUsername` 方法名暗示「用户名是否存在」。  
  - 调用 `hasUsername("Alice")` 返回 `true` 时,实际表示「用户名不存在」,语义矛盾。  

正确逻辑
public Boolean hasUsername(String username) {
    // 返回 true 表示用户名存在(可能误判),false 表示一定不存在
    return userRegisterCachePenetrationBloomFilter.contains(username);
}

**结论**:原代码逻辑错误,`hasUsername` 方法应直接返回 `contains(...)` 的结果,而非取反。

问题5:`unlock()` 放在 `finally` 中是否导致异常?
回答正确性:✅ 正确(需代码优化)

try {
    if (lock.tryLock()) {
        // 注册逻辑
        return;
    }
    throw new ClientException(USER_NAME_EXIST);
} finally {
    lock.unlock();
}

问题:若 `tryLock()` 失败(未获取锁),`finally` 块仍调用 `unlock()`,导致 `IllegalMonitorStateException`。  

if (!lock.tryLock()) {
    throw new ClientException(USER_NAME_EXIST);
}
try {
    // 注册逻辑
} finally {
    lock.unlock();
}

改进点:  
  1. **提前判断锁获取**:未获取锁直接抛异常,避免无效解锁。  
  2. **确保仅持有锁的线程解锁**:`finally` 块仅在获取锁后执行 `unlock()`。  
解决方案正确如下

昨天在群里看到有人说这个,我在点评里学到的是可以把trylock提出来,这样其
他线程没获取到锁的话直接抛异常,如果获取到锁的话就进行注册,这样的话就不
用加额外的判断。代码如下:
// 上锁,开始操作,对当前用户加锁
RLock lock = redissonClient.getLock(LOCK_USER_REGISTER_KEY + username);
// 调用用户服务获取锁
if(lock.tryLock()){
    // 用户获取不到锁,直接失败
    throw new ClientException(UserCodeEnum.USER_SAVE_FAIL);
}
try{
    // 将注册请求,将注册信息存入数据库
    // 数据库保存成功
    UserDao userDao = BeanUtil.toBean(userRegisterRequestDTO, UserDao.class);
    // 存入数据库
    boolean isSuccess = save(userDao);
    if(isSuccess){
        // 将用户名存入缓存
        userRegisterCachePermissionUtil.add(userDao.getUsername());
        // 将当前注册成功的用户信息存入分组
        groupPermissionUtil.add(userDao.getUsername(), "user");
        return Result.success();
    }
    // 数据库保存失败,抛出异常
    throw new ClientException(UserCodeEnum.USER_SAVE_FAIL);
}finally {
    // 无论上述步骤是否成功,解锁
    lock.unlock();
}
问题回答正确性说明
问题1✅ 正确MP逻辑删除配置有效,但需确保插入时逻辑删除字段有值。
问题2✅ 正确需结合唯一索引和锁优化方案解决重复注册和误判问题。
问题3✅ 正确MP自动填充更灵活可控,但需根据场景选择。
问题4❌ 错误hasUsername方法逻辑错误,需直接返回contains(...)结果。
问题5✅ 正确提前判断锁获取状态,避免unlock()异常。

- 对问题4修正 `hasUsername` 方法逻辑。  
- 对问题2补充唯一索引和锁续期方案。 ​