在DDD+事件溯源架构中如何处理时效性业务规则?
OTP时效性逻辑实现方案选择(事件溯源+DDD场景)
我们有一个代表一次性密码(OTP)的实体,15分钟后过期。系统采用事件溯源架构并遵循DDD原则,定义的核心事件如下:
public record OtpCreatedEvent( string MobilePhoneNumber, string Password, IPAddress RequesterIp, DateTimeOffset OccurredAt ) : IEvent;
业务规则要求:
- 创建新OTP时(
CreateOtp命令):- 单个客户端(按IP标识)1小时内为任意手机号创建的OTP不得超过15个;
- 同一手机号的两次OTP请求间隔至少20秒。
- 校验OTP时(
VerifyOtp命令):需获取该手机号所有未过期的OTP,校验输入密码是否匹配其中任意一个。
针对时效性逻辑,目前有两种可行方案:
- 命令调用时,将时效逻辑整合到数据查询中,示例:
var nonExpiredOtps = dataStore.Otps.Where(otp => otp.CreatedAt > DateTime.Now.AddMinutes(-15)) - 创建新OTP时,调度一个15分钟后执行的
ExpireOtp命令,触发OtpExpiredEvent事件标记OTP过期。
方案2需要额外基础设施支持,方案1存在代码不够优雅的问题,想请教哪种方案更推荐及原因,同时是否存在其他方案?
推荐方案及原因
优先选择方案1,原因如下:
- 贴合事件溯源核心设计:OTP过期是时间驱动的属性过滤,而非业务行为触发的状态变更。事件溯源的事件应记录业务动作带来的状态转换,过期不属于这类行为,没必要用事件标记。
- 降低系统复杂度:方案1无需依赖定时调度基础设施(如Quartz、Hangfire),避免了调度失败(服务宕机、任务丢失)导致的状态不一致问题,减少运维成本。
- 可优化代码优雅性:时效逻辑可封装到专门的查询服务(如
IOtpQueryService)中,把过滤、频率统计等逻辑统一封装,业务代码只需调用服务方法(如GetActiveOtpsForPhone(string phone)、CheckIpRateLimit(IPAddress ip)),避免业务逻辑中散落查询条件。
方案2仅适合需要主动触发过期后业务动作的场景(如给用户发送OTP过期提醒、清理关联业务资源),若只是为标记过期引入调度,属于过度设计。
其他可选方案
1. 基于事件投影维护活跃OTP读模型
在事件溯源架构中,通过订阅OtpCreatedEvent构建专门的读模型:
- 投影层接收事件后,将OTP数据写入读模型并记录创建时间;
- 定期(如每分钟)清理读模型中已过期的OTP,或查询时自动过滤过期数据;
- 业务代码直接查询该读模型,无需重复编写时效过滤逻辑,符合CQRS分工,同时简化业务代码。
2. 结合缓存实现高并发时效管理
用Redis等缓存存储未过期的OTP:
- 创建OTP时存入缓存并设置15分钟过期时间,同时用缓存原子命令实现频率限制:
- IP的1小时15个限制:用
INCR统计请求次数,设置1小时过期时间; - 手机号的20秒间隔:用
SET存储最后请求时间,设置20秒过期时间,仅当不存在时允许写入;
- IP的1小时15个限制:用
- 校验OTP时直接从缓存读取,缓存自动处理过期逻辑,性能更高,适合高并发场景,同时简化业务代码的时效处理。
内容的提问来源于stack exchange,提问作者Arad
相关产品推荐
相关产品推荐

