ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用户注册链路设计:从责任链到分布式锁,再到布隆过滤器

用户注册链路设计:从责任链到分布式锁,再到布隆过滤器 一、注册链路概览注册是几乎所有业务系统的入口看似简单却暗藏诸多并发与一致性问题。本文以一个真实的用户注册链路为例逐层剖析其设计思路与实现细节。完整链路流程前端请求 → Controller接收 → 责任链校验(3个handler) → 分布式锁(Redis) → 插入用户表 → 插入手机号关联表 → 唯一索引兜底 → 布隆过滤器标记以下逐层展开。二、Controller接收请求接收前端返回的JSONPostMapping(/user-service/register) public ResultUserRegisterRespDTO register(RequestBody Valid UserRegisterReqDTO requestParam) { return Results.success(userLoginService.register(requestParam)); }解析PostMapping非幂等操作注册本身不可重复执行ResultT 统一响应包装所有接口返回格式一致Valid触发 JSR-303 参数校验如非空、格式等UserRegisterReqDTO前端传参的数据传输对象与数据库实体解耦三、责任链模式校验逻辑的解耦与编排3.1为什么需要责任链注册校验通常包含多项规则参数非空、用户名唯一性、证件号注销次数限制等。如果全部写在 Service 里代码会变得臃肿且难以扩展。使用责任链的好处每个校验规则独立封装为 Handler新增/删除/调整顺序只需增删 Handler 或调整 Order单一职责便于单元测试3.2调度器与 Mark 机制基于AbstractChainContext自定义的链式调度器来管理所有 Handler。Mark是什么Mark 是一个字符串标识用于将 Handler 划分到不同的业务小组。同一个 Mark 的 Handler 会在同一条链上按顺序执行。public interface UserRegisterCreateChainFilterT extends AbstractChainHandlerT { Override default String mark() { return UserChainMarkEnum.USER_REGISTER_FILTER.name(); // USER_REGISTER_FILTER } }类比3 个安检员佩戴同色臂章都隶属于「注册安检组」共同完成一套安检流程。3.3三条校验规则调度器会自动扫描所有实现了USER_REGISTER_FILTER标记的 Handler按getOrder()升序执行OrderHandler职责0参数非空校验拦截 null 或空字符串1用户名唯一性校验布隆过滤器快速判断详见后文2证件号注销次数校验同一证件号注销超过 5 次则拦截核心机制 任一 Handler 抛出异常后续 Handler 不再执行异常直接上抛由 Web 层统一处理返回前端。四、分布式锁跨实例的并发控制防止重复注册的关键4.1 问题背景为什么本地锁不够用先明确一个概念实例 一个正在运行的服务进程。假设 user-service 部署了 3 个实例3 台服务器前端请求经过网关随机分发用户A请求注册张三 → 网关 → 实例1放行用户B请求注册张三 → 网关 → 实例2放行 ← 同时发生产生重复注册本地锁synchronized / ReentrantLock的作用范围锁类型作用范围类比synchronized单个 JVM 进程一栋楼的保安ReentrantLock单个 JVM 进程更高级的保安能排队/超时但仍只守一栋楼在微服务多实例部署下必须使用分布式锁。4.2 基于 Redisson 的分布式锁实现// 1. 创建锁Key 为用户名 RLock lock redissonClient.getLock(LOCK_USER_REGISTER requestParam.getUsername()); // 2. 尝试抢锁非阻塞 boolean tryLock lock.tryLock(); if (!tryLock) { throw new ServiceException(HAS_USERNAME_NOTNULL); // 没抢到 → 用户名已被他人占用 } try { // 3. 执行业务逻辑插入数据库等 } finally { // 4. 释放锁 lock.unlock(); }关键点锁存储在 Redis 中所有实例共享同一个 Redis因此能跨实例互斥底层使用 Redis 原子命令 SET NX EX由 Redisson 封装锁的粒度精确到用户名不同用户互不影响项目中的锁 Key 定义//用户注册锁Key Prefix 用户名 public static final String LOCK_USER_REGISTER index12306-user-service:lock:user-register:; //用户注销锁Key Prefix 用户名 public static final String USER_DELETION index12306-user-service:user-deletion:; //用户注册可复用用户名分片Key Prefix Idx public static final String USER_REGISTER_REUSE_SHARDING index12306-user-service:user-reuse:; //用户乘车人列表Key Prefix 用户名 public static final String USER_PASSENGER_LIST index12306-user-service:user-passenger-list:;进阶思考tryLock() 无参调用默认等待时间 30 秒Redisson 看门狗机制。如果业务执行超时看门狗会自动续期避免锁提前释放。这一机制值得深入理解。五、数据库插入对象转换与唯一索引兜底5.1 DTO → DO 转换前端传参是 UserRegisterReqDTO数据库操作需要 UserDO。int inserted userMapper.insert(BeanUtil.convert(requestParam, UserDO.class));为什么不能直接用 DTOTableName(t_user) // UserDO 类上的注解 public class UserDO { // 字段映射... }MyBatis-Plus 通过 TableName 和字段注解识别表名与列映射必须传入 UserDO 类型。BeanUtil.convert() 负责按字段名复制属性值。5.2 唯一索引最后的防线用户表插入成功后还需往 t_user_phone 表插入手机号关联记录try { userPhoneMapper.insert(userPhoneDO); } catch (DuplicateKeyException dke) { throw new ServiceException(PHONE_REGISTERED); }双保险机制分布式锁前置拦截→ 唯一索引兜底保障即使分布式锁出现极端情况如锁过期、Redis 宕机数据库唯一索引仍能保证数据一致性将重复插入转化为业务异常返回前端。注意事项在 Transactional 事务中捕获 DuplicateKeyException 后抛出业务异常需确保事务正确回滚避免用户表已插入而手机号表插入失败的不一致状态。六、布隆过滤器防止缓存穿透6.1 使用场景用户名唯一性校验是注册链路的高频查询。如果每次都查库在恶意流量下可能造成数据库压力过大。解决方案使用布隆过滤器Bloom Filter作为前置快速判断。6.2 核心逻辑// 注册前快速判断用户名是否可能存在 // 注意布隆过滤器说不存在则一定不存在说存在则可能存在有误判率 if (bloomFilter.contains(username)) { // 触发二次查库确认避免误判拦截真实新用户 } // 注册成功后标记该用户名已存在 userRegisterCachePenetrationBloomFilter.add(username);6.3 重要特性特性说明判断结果「不存在」→ 一定不存在「存在」→ 可能存在支持删除不支持因此只增不减使用原则注册成功后必须 add否则下次查询会误判为不存在误判处理命中后需查库二次确认不能直接拦截6.4 缺陷与应对布隆过滤器不支持删除用户注销后无法清除标记。项目中的应对方案是// 用户名复用分片锁用于处理注销后用户名的重新注册 public static final String USER_REGISTER_REUSE_SHARDING user-service:user-reuse:;通过分片锁控制用户名复用的并发安全同时配合业务表记录注销状态形成完整的用户名生命周期管理。七、异常的统一处理补充整个链路中抛出的异常校验异常、锁异常、数据库异常不会直接暴露给前端。项目通过全局异常处理器统一拦截ControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(ServiceException.class) public ResultVoid handleServiceException(ServiceException e) { return Results.failure(e.getCode(), e.getMessage()); } }这样保证了所有业务异常返回统一的 JSON 格式前端只需要按 Result 结构解析即可。八、总结与思考8.1 链路设计亮点层次设计作用校验层责任链模式解耦、可扩展、顺序可控并发层Redisson 分布式锁跨实例互斥防止重复注册数据层数据库唯一索引兜底保障数据最终一致性性能层布隆过滤器拦截绝大部分无效查询防缓存穿透8.2 值得进一步思考的问题布隆过滤器误判率如何调优可根据用户规模设置合适的容量和哈希函数数量在内存与准确性之间取得平衡。锁超时与看门狗如果注册逻辑因网络抖动执行超时Redisson 的看门狗机制如何自动续期何时需要手动配置leaseTime注销用户名的复用布隆过滤器不支持删除注销后的用户名如何安全地重新开放注册这需要结合业务状态和分片锁共同设计。极端高并发下的优化Redis 分布式锁在超高并发下是否成为瓶颈是否可引入分段锁或先尝试布隆过滤器拦截大部分请求
返回列表