1. 用户限额系统设计背景与核心挑战在电商和虚拟商品交易场景中用户购买限额控制是一个常见但极具挑战性的需求。最近我负责设计了一个通用限额系统用于限制用户对特定商品的购买次数。这个系统需要支持多种维度的限额控制包括用户ID、设备UUID、手机号等用户标识维度以及商品ID、商品类型等商品维度。核心业务需求可以概括为限制用户在一定时间范围内如一天、一周或自定义时间段对某类商品的购买次数。例如某虚拟商品限制每个用户每天最多购买3次每周最多购买10次。当用户尝试下单时系统需要实时检查并更新购买计数确保不会超出限额。2. 系统架构设计与关键决策2.1 存储选型与数据结构设计经过评估我们选择Redis作为限额系统的核心存储主要基于以下考虑高性能Redis的读写性能可以满足高并发场景原子性操作支持incr、set等原子命令过期机制天然支持key的TTL设置完美匹配时间限额需求Redis Key的设计采用了多维度组合的方式{用户维度类型}_{用户标识}_{商品维度类型}_{商品标识}例如U_12345_T_GAME表示用户ID 12345对游戏类商品的限额P_13800138000_P_ITEM_1001表示手机号13800138000对商品ID 1001的限额这种设计提供了良好的扩展性新增维度只需在Key中追加字段即可。2.2 一致性保障方案选型保证限额扣减的一致性是最核心的技术挑战。我们评估了三种方案方案一LUA脚本优点原子性执行完美解决一致性问题缺点Redis Cluster环境下跨节点的Key无法在同一个LUA脚本中操作结论不适用我们的分布式环境方案二IncrBy回滚流程先IncrBy增加计数 → 检查是否超限 → 如超限则DecrBy回滚问题多Key操作时部分成功部分失败难以处理网络超时无法确定操作状态结论复杂度高可靠性不足最终方案分布式锁Set命令在用户提单前后加分布式锁确保串行处理使用简单的Set命令而非IncrBy避免回滚复杂度支持无脑重试简化错误处理关键经验当发现方案过于复杂时往往意味着需要换个思路。分布式锁虽然引入了额外开销但大幅降低了系统复杂度。3. 核心实现细节与优化3.1 分布式锁设计要点我们采用Redisson实现的分布式锁关键设计包括锁粒度以业务线用户ID为锁Key平衡并发性能与安全性锁超时设置合理的超时时间如30秒避免死锁锁续期实现看门狗机制自动延长持有锁时间锁的使用流程// 加锁 RLock lock redisson.getLock(order:limit: userId); try { if (lock.tryLock(10, 30, TimeUnit.SECONDS)) { // 执行限额检查和扣减 processQuota(userId, itemId); } } finally { lock.unlock(); }3.2 限额扣减流程详解完整的限额扣减流程如下加锁阶段获取用户维度分布式锁锁范围限定在当前业务线避免影响其他业务限额检查阶段查询Redis中所有相关维度的当前计数检查是否任一维度已达到限额如已达限额直接返回失败提单阶段执行实际的下单业务逻辑如下单失败直接返回无需处理限额扣减阶段如下单成功使用MSET一次性更新所有维度的计数记录幂等键防止重复扣减释放锁阶段无论成功失败最终释放分布式锁3.3 时间维度限额实现对于时间范围的限额控制我们利用Redis的过期时间特性自然日限额计算当天剩余秒数作为TTL例如expireTime 86400 - (currentTime % 86400)滚动时间窗口限额设置固定TTL如24小时86400秒每次更新计数时刷新TTL关键代码示例// 设置自然天过期 long now System.currentTimeMillis() / 1000; long expireSeconds 86400 - (now % 86400); redisTemplate.expire(key, expireSeconds, TimeUnit.SECONDS); // 或者设置固定时间窗口 redisTemplate.expire(key, 24, TimeUnit.HOURS);4. 系统扩展性与接口设计4.1 通用接口设计我们设计了高度灵活的接口允许业务方自定义限额维度public class QuotaOpCmd { private int bizId; // 业务线ID private int opType; // 操作类型扣减/回滚 private String idemKey; // 幂等键 private ListQuotaOpItem opItems; // 限额项列表 } public class QuotaOpItem { private int userInfoType; // 用户维度类型 private String userInfoId; // 用户标识 private ListString dimensions; // 商品维度 private int expireTime; // 过期时间 private int opNum; // 操作数量 }这种设计使得新增用户维度如身份证号无需修改核心逻辑商品维度完全由业务方定义不同业务线可以独立配置4.2 幂等性保障我们采用多层幂等保障机制操作级别幂等每个操作有唯一idemKey防止重复执行订单级别幂等订单ID作为幂等键的一部分短期有效期幂等键默认1小时过期避免长期占用内存幂等键存储结构idem:quota:{bizId}:{orderId} - 1设置过期时间redisTemplate.opsForValue().set( idem:quota: bizId : orderId, 1, 1, TimeUnit.HOURS );5. 生产环境经验与优化5.1 性能优化实践Pipeline批量操作对多Key查询使用pipeline减少网络往返redisTemplate.executePipelined((RedisCallbackObject) connection - { for (String key : keys) { connection.stringCommands().get(key.getBytes()); } return null; });Lazy过期策略不主动清理过期Key依赖Redis自身淘汰机制设置适当的内存淘汰策略volatile-ttl本地缓存对限额规则进行本地缓存减少Redis访问使用Caffeine实现带TTL的缓存CacheString, Integer localCache Caffeine.newBuilder() .expireAfterWrite(5, TimeUnit.MINUTES) .maximumSize(10_000) .build();5.2 监控与告警我们建立了完善的监控体系Redis关键指标内存使用率命令延迟命中率业务指标限额检查QPS扣减成功率锁等待时间告警规则Redis内存超过80%扣减失败率突增平均锁等待时间超过500ms5.3 踩坑与解决方案问题一Redis大Key风险现象某业务使用了过长的维度组合导致单个Key达1KB解决强制限制各维度长度添加长度校验问题二锁竞争激烈现象秒杀活动时锁等待时间飙升优化引入锁分段按用户ID哈希分成16段问题三网络闪断导致锁失效现象机房网络问题导致锁提前释放加固实现锁续约机制定期刷新锁TTL6. 总结与最佳实践这个限额系统的设计经历让我深刻体会到好的架构需要在严谨性和灵活性之间找到平衡。以下是关键经验总结简单即美最初的LUA脚本方案看似完美但实际复杂最终分布式锁方案虽然笨但可靠面向扩展设计通过抽象用户维度和商品维度系统轻松支持了后续新增的身份证号维度监控驱动优化没有监控的系统就像盲人摸象完善的监控帮我们及时发现并解决问题适度抽象核心组件只关注限额的原子操作业务规则完全交由上层决定对于类似系统的设计我的建议是优先考虑一致性需求选择最简单可靠的方案预留扩展空间但不要过度设计性能优化要基于实际监控数据避免过早优化完善的错误处理和重试机制是系统健壮性的关键限额系统上线后稳定支撑了日均千万级的限额检查请求期间灵活支持了多个业务线的定制化需求验证了设计的有效性。这个案例也证明好的系统设计不是一蹴而就的而是需要不断迭代优化。