news 2026/8/18 22:22:09

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用户注册链路设计:从责任链到分布式锁,再到布隆过滤器

一、注册链路概览

注册是几乎所有业务系统的入口,看似简单,却暗藏诸多并发与一致性问题。本文以一个真实的用户注册链路为例,逐层剖析其设计思路与实现细节。

完整链路流程:

前端请求 → Controller接收 → 责任链校验(3个handler) → 分布式锁(Redis) → 插入用户表 → 插入手机号关联表 → 唯一索引兜底 → 布隆过滤器标记

以下逐层展开。

二、Controller:接收请求

接收前端返回的JSON

@PostMapping("/user-service/register") public Result<UserRegisterRespDTO> register(@RequestBody @Valid UserRegisterReqDTO requestParam) { return Results.success(userLoginService.register(requestParam)); }

解析:

  • @PostMapping:非幂等操作,注册本身不可重复执行
  • Result<T> :统一响应包装,所有接口返回格式一致
  • @Valid:触发 JSR-303 参数校验,如非空、格式等
  • UserRegisterReqDTO:前端传参的数据传输对象,与数据库实体解耦

三、责任链模式:校验逻辑的解耦与编排

3.1为什么需要责任链?

注册校验通常包含多项规则:参数非空、用户名唯一性、证件号注销次数限制等。如果全部写在 Service 里,代码会变得臃肿且难以扩展。

使用责任链的好处:

每个校验规则独立封装为 Handler

新增/删除/调整顺序只需增删 Handler 或调整 @Order

单一职责,便于单元测试

3.2调度器与 Mark 机制

基于AbstractChainContext(自定义的链式调度器)来管理所有 Handler。

Mark是什么?

Mark 是一个字符串标识,用于将 Handler 划分到不同的业务小组。同一个 Mark 的 Handler 会在同一条链上按顺序执行。

public interface UserRegisterCreateChainFilter<T> extends AbstractChainHandler<T> { @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));

为什么不能直接用 DTO?

@TableName("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 Result<Void> handleServiceException(ServiceException e) { return Results.failure(e.getCode(), e.getMessage()); } }

这样保证了所有业务异常返回统一的 JSON 格式,前端只需要按 Result 结构解析即可。

八、总结与思考

8.1 链路设计亮点

层次设计作用
校验层责任链模式解耦、可扩展、顺序可控
并发层Redisson 分布式锁跨实例互斥,防止重复注册
数据层数据库唯一索引兜底保障,数据最终一致性
性能层布隆过滤器拦截绝大部分无效查询,防缓存穿透

8.2 值得进一步思考的问题

  1. 布隆过滤器误判率如何调优?可根据用户规模设置合适的容量和哈希函数数量,在内存与准确性之间取得平衡。

  2. 锁超时与看门狗:如果注册逻辑因网络抖动执行超时,Redisson 的看门狗机制如何自动续期?何时需要手动配置leaseTime

  3. 注销用户名的复用:布隆过滤器不支持删除,注销后的用户名如何安全地重新开放注册?这需要结合业务状态和分片锁共同设计。

  4. 极端高并发下的优化:Redis 分布式锁在超高并发下是否成为瓶颈?是否可引入分段锁或先尝试布隆过滤器拦截大部分请求?

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/18 22:20:05

Coze工作流插件实战:从天气查询到AI应用自动化

这次我们来看一个在AI应用开发领域非常实用的工具——扣子&#xff08;Coze&#xff09;的工作流插件。如果你正在寻找一种能快速构建、自动化执行复杂AI任务链的方法&#xff0c;并且希望这个过程能像搭积木一样直观&#xff0c;那么扣子工作流及其插件系统值得你花时间了解。…

作者头像 李华
网站建设 2026/8/18 22:12:28

AI视频画质修复插件实战指南:从原理到PR/AE稳定工作流

这类视频画质修复工具&#xff0c;最值得先看的不是它宣传的“一键修复”或“黑科技”&#xff0c;而是它到底能不能在你的PR或AE里稳定跑起来&#xff0c;以及修复效果是否真的可控。很多新手一上来就被“4K超清”、“秒变清晰”这类词吸引&#xff0c;结果装完插件要么报错&a…

作者头像 李华
网站建设 2026/8/18 22:08:31

公交车新能源化:技术路线、基础设施与运营重构全解析

1. 从一纸通知到行业变革&#xff1a;公交车新能源化的深层逻辑 最近&#xff0c;一份由四个部委联合发布的关于推进公交车新能源化的通知&#xff0c;在交通和能源圈子里引发了不小的讨论。表面上看&#xff0c;这只是一份政策文件&#xff0c;但如果你身处公交运营、车辆制造…

作者头像 李华