news 2026/10/8 8:48:22

令牌桶与用户画像结合:Java实现动态限流引擎控住群发节奏

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
令牌桶与用户画像结合:Java实现动态限流引擎控住群发节奏

上周五晚上十点,运营同学在群里发了一句:“再给这批客户补发一次优惠券推送。”我看着监控大屏上已经跑满的发送通道,心里咯噔一下。微信私域群发这件事,量从来不是最难的,难的是发送节奏——量控制不好,用户投诉和平台侧频控约束会一起找上门。所以我们用 Java 实现了一套“令牌桶 + 用户画像”的动态限流引擎,把群发从粗暴的“最大并发”变成精细化、分人群、分优先级的可控投放。这篇文章把这个引擎从设计目标、算法选择、用户画像调权到分布式落地的完整链路拆开讲清楚,适合正在做微信生态运营系统、消息推送服务或对限流算法感兴趣的 Java 后端同学参考。

先把边界说清楚:整个方案讨论的是在平台接口频控约束范围内,从业务侧做自我保护式的发送节奏管理。平台的开放接口有各自的频率限制,我们需要做的是规划好自己的速率曲线,避免因为业务突发把自己推到上限附近,也避免同一批用户短时间被反复打扰。这不是也不鼓励任何钻空子的思路。

1. 一次“补发优惠券”引发的反思:静态限流的三个盲区

1.1 事故还原:从 120 秒洪峰到投诉电话

当时我们线上跑的还只是固定速率的限流器:配置了一个maxQps = 50,请求超过就直接拒绝,拒绝后丢到一个阻塞队列里慢慢消费。这个设计平时看起来挺稳,但“补发优惠券”那天运营在晚上十点发起活动,用户触点链路重试加上多个任务并发,实际流量瞬间冲到每秒 150 条以上。阻塞队列从几千膨胀到八万多,内存直接告警,部分请求超时后又触发重试,等于又把流量放大了一遍。

真正让我头疼的不是服务器,而是前一天的投诉曲线。同一个客户在两个小时里收到了三条内容几乎一样的营销消息,有人直接在后台点了“投诉”,有人默默取消了关注。平台侧也返回了一定比例的“操作频繁”错误。我们被自己的发送系统锤了一拳——限流器明明在工作,但它只看“每秒多少个请求”,完全没看“这些请求打给了谁”。

1.2 风控控的不是“速度”,而是“代价”

静态限流最大的认知误区,是觉得控制住 QPS 就够了。但在微信私域群发这件事里,一条消息发出去,有两层“代价”。

第一层是平台侧代价。每个可信接口、每个账号在单位时间内都有调用频次约束,官方文档写得很清楚。一旦超过,轻则接口报错,重则被限制功能。我们控制速率曲线,本质上是让业务波峰不要撞上平台约束的波峰。

第二层是用户侧代价。同一个用户,如果过去 30 天经常打开消息、点击链接,那再收到一条营销推送的“讨厌成本”很低;但如果对方已经三十天没登录、退订过两次,或者因为营销消息投诉过一次,那我们再发一条就等于往火里浇油。量化下来,这两类用户收到同一条消息的代价可能差十几倍。

所以正确的风控设计不应该只问“单位时间能发几条”,而应该问“当前这条消息发给这个人,消耗的代价是几个‘标准单位’”。这也是为什么只在应用入口放一个固定 QPS 计数器,根本拦不住真实事故。

1.3 群发场景的三个特殊变量

我复盘那晚的故障时,总结了群发场景区别于普通 API 限流的三个特殊变量。

第一,时间突发性极强。运营天然喜欢在整点、节假日、直播开场这类时间节点冲量,流量曲线不是平稳的,而是陡峭的尖峰。固定限流如果按峰值配置,日常就会超发;按均值配置,活动一来队列就爆炸。

第三,消息类型之间没有统一频率。服务通知、营销活动、会员关怀这三类消息,用户能接受的频率完全不同。同一个用户,一天收到一条物流通知没问题,但一天收到三条促销贴,体验直接崩掉。

第二,用户风险分布极不均匀。高活跃用户、沉默用户、刚加好友的用户、退订边缘的用户,对消息的容忍度完全不同。用一个全局速率去管所有人,等于假设所有人都是一模一样的“平均用户”,这在私域场景里就是个危险的简化。

既然静态方案有这么多盲区,那很自然想到一个问题:能不能让限流器在单位时间内放行的“代价总量”保持稳定,而具体到每个用户身上的速率,根据画像动态调整?这正是我们接下来要落地的核心思路。

2. 令牌桶工程化落地:Java 实现与突发流量处理

2.1 为什么是令牌桶,而不是漏桶或固定窗口

选令牌桶之前,我们其实把几种常见算法都过了一遍。固定窗口简单,但临界点问题严重,某个窗口最后 100 毫秒和下一个窗口前 100 毫秒之间,可能放行两倍流量;滑动窗口能缓解,但实现复杂度和内存代价都不低。漏桶的思路是强制恒定速率,不管上游多猛,下游永远按固定节奏处理,优点是极度平滑,缺点是完全没有突发能力。

令牌桶是这两者的折中。它维护一个“桶”,桶里累积一些令牌,每次请求消耗令牌,同时系统以恒定速率往桶里补令牌。这个方案既能保证长期平均速率不超标,又能容忍短时突发——因为桶里攒下来的令牌可以一次性消耗掉。

用生活里的例子类比:漏桶像是一个人在传送带上匀速放箱子,不管仓库里堆了多少,传送带速度永远不变;令牌桶则像是你手里握着一把地铁票,每过一个闸机付一张,闸机不关心你前面是不是堵了一群人,只要你票够就能进。地铁票是你平时慢慢攒下来的,高峰期能多走几个。

对于群发场景来说,“允许在活动开始的瞬间冲一波量,但长期平均速度可控”恰好是我们要的行为模型。

2.2 单机版实现:并发安全的令牌桶

我直接贴一版我们线上用过的最简实现,去掉了一些统计埋点,保留核心逻辑。

import java.util.concurrent.locks.ReentrantLock; public class TokenBucket { private final long capacity; private final double refillPerSecond; private final ReentrantLock lock = new ReentrantLock(); private double storedTokens; private long lastRefillNanos; public TokenBucket(long capacity, double refillPerSecond) { this.capacity = capacity; this.refillPerSecond = refillPerSecond; this.storedTokens = capacity; this.lastRefillNanos = System.nanoTime(); } public boolean tryAcquire(double needTokens) { lock.lock(); try { refill(); if (storedTokens < needTokens) { return false; } storedTokens -= needTokens; return true; } finally { lock.unlock(); } } private void refill() { long now = System.nanoTime(); double deltaSeconds = (now - lastRefillNanos) / 1_000_000_000.0; if (deltaSeconds <= 0) { return; } storedTokens = Math.min(capacity, storedTokens + deltaSeconds * refillPerSecond); lastRefillNanos = now; } }

这里有几个容易踩的细节。

第一,tryAcquire的参数我故意设计成double,因为后面接入用户画像后,一个用户消耗的令牌量不是整数。比如高活跃用户消耗0.4个令牌,沉默用户消耗2.0个令牌。如果你在第一步就写死int tokens,画像调权就做不进去了。

第二,用ReentrantLock而不是synchronized。低并发下两者差不多,但桶的校验和扣减是一个读改写过程,锁粒度越细越好;synchronized偏向锁在竞争不激烈时也有优化,不过我们在高并发下实测ReentrantLock的吞吐更稳定。当然你也可以用AtomicLong+ CAS 做无锁版本,但那套方案在涉及“按时间补令牌”时处理边界很绕,普通业务场景没必要。

第三,refill()每次都在加锁代码里调用,不要用调度线程定期补,因为那样既增加线程管理成本,又会在“长时间没请求后突然来请求”时出现桶已空但还没补到位的尴尬。懒式补令牌是令牌桶最优雅的写法,每次请求来了,一算“距离上次补了多久”,把该补的一次性补上。

2.3 参数计算:capacity 和 refillPerSecond 怎么定

参数定不好,令牌桶就是个花架子。我们的经验是,先把目标抽象成两个数字:日常平均速率R和峰值突发能力B。

假设一个用户池有 5 万人,希望在 20 分钟内全部发完第一轮,那么R = 50000 / 1200 ≈ 42,也就是每秒大约补 42 个令牌。如果运营要求活动开始后允许短时间内冲到每秒 120 的速度,且这个峰值持续 30 秒,那这 30 秒里比日常多消耗的令牌是(120 - 42) * 30 = 2340。所以桶容量B可以设为 2400 左右。

这里面有个很容易犯的错误:一开始总觉得桶容量越大越“稳”,但令牌桶的容量决定了最大突发量。容量太大时,系统可能在冷启动时一口气放行几千条消息,队列照样被冲爆。我们最后定的原则是:B尽量往小里调,够覆盖“运营发起一次真正活动的瞬时量”就行,而不是覆盖到“并发重试叠加的失控量”。

2.4 桶容不下时,消息往哪里去

tryAcquire返回false不代表消息就应该直接丢弃。群发消息可以等、可以降级,但不能丢失。我们在线上的处理优先级有三条线。

第一条线是排队。把未获取到令牌的消息放进一个带优先级的延迟队列,让重要消息类型先出队。比如活动通知可以等 5 秒,服务通知可以等 2 秒,纯营销消息等 30 秒甚至降级。

第二条线是指数退避重试。对被限流的用户生成一个retryAfter,下次重试时间按指数增长,比如 1 秒、2 秒、4 秒、8 秒,最多重试 3 次。重试必须做幂等——通过发送记录表或消息唯一 ID 去重,否则限流引擎刚把速率控住,重试伞兵又把流量抬回来了,这就是事故当晚最大的教训。

第三条线是降级发送。如果某个池子的桶水位长期处于高位,就把营销类消息降级到“晚间低谷时段再发”,服务类消息继续优先。这本质上是在有限令牌预算下做资源分配,而不是把流量硬压下去。

3. 用户画像动态调权:让限流从“一刀切”变成“千人千面”

3.1 从五个维度给用户打分

要让限流动态起来,第一步是给每个用户算一个“打扰成本分”。我们内部叫profileScore,分数越低,代表这个人相对更能接受触达。

我建议至少看五个维度,权重可以根据业务灵活调:

  • 活跃度:过去 30 天打开消息频次、登录天数。活跃用户对触达更敏感,但接受度也更高。
  • 历史互动:打开率、点击率、转化率。互动越好,越愿意收到同类内容。
  • 负面反馈:投诉次数、退订次数、拉黑次数。这个权重必须拉满,一次投诉的代价远高于十次打开。
  • 标签分层:会员等级、兴趣标签、消费频次。高价值用户值得精细触达,而不是高频轰炸。
  • 时间衰减:最近一次互动距今多少天。超过一定阈值,说明关系在降温,发送频率要同步降。

这里给一个简化但不失真的代码骨架,用几个特征算出profileScore。

public class UserProfileScorer { public static double score(UserPortrait p) { double active = p.last30DaysOpenRate(); // 0.0 ~ 1.0 double interaction = p.historicalClickRate(); // 0.0 ~ 1.0 double recency = clamp(1.0 - p.lastActiveDays() / 180.0, 0.0, 1.0); double negativePenalty = p.complaintCount() * 1.5 + p.unsubscribeCount() * 1.2; double score = 0.35 * active + 0.30 * interaction + 0.20 * recency - negativePenalty; return score; } }

特征值一定要归一化,不要直接拿原始天数或次数做运算,不然投诉多的人分会直接被压到极值,导致所有负数用户被一视同仁地拦截。归一化的方式不一定要复杂的 min-max,简单用一个180 天做分母的衰减函数就够用了。

3.2 把画像分数折算成令牌消耗倍数

有了profileScore,接下来有一个关键设计:我们不直接按分数决定发不发,而是把它映射成一个“令牌消耗倍数costFactor”。

public static double toCostFactor(double profileScore) { double normalized = 1.0 / (1.0 + Math.exp(-profileScore)); return 0.4 + 1.6 * normalized; // 范围 [0.4, 2.0] }

为什么用 Sigmoid 函数?因为画像分数在小范围波动时,我们希望成本变化是平滑的;而在极端区间,成本又能被封顶,不至于出现某个用户消耗 100 个令牌的极端情况。0.4到2.0这个区间意味着:最活跃、互动最好的用户发一条消息只消耗 0.4 个令牌,普通用户消耗 1 个,沉默或投诉用户消耗 2 个。桶的总容量和补令牌速度没变,单位时间发出的消息数量却会根据人群自动调整了。

接口上的用法就变成这样:

double baseCost = 1.0; double cost = baseCost * toCostFactor(profile.getScore()); if (bucket.tryAcquire(cost)) { // 发送 } else { // 入队、重试或降级 }

这里有一个细节要注意:令牌桶的capacity和refillPerSecond是按“标准令牌”定义的。当成本从整数变成浮点后,桶的“容量”含义变成了“总打扰预算”。我们内部干脆把日志里的指标从qps改成了tps(touching per second),因为它衡量的是“发出了多少份打扰”,而不是“多少次 HTTP 调用”,这个语义转变帮助运营同学理解了限流存在的意义。

3.3 人群隔离:对沉默用户最温柔的做法是不发

动态成本解决了“同一秒内不同用户的取舍”,但还有一些用户,即使消耗 10 个令牌也不该发。我们因此加了人群隔离策略,在令牌桶之前先做一次硬性过滤:

  • 高活跃池:成本系数最低,基本不拦。
  • 普通池:按页面的实时桶水位公平竞争。
  • 低活跃池:只允许服务通知类消息,营销类直接拦截。
  • 风险池(有投诉记录):进入观察期,观察期内完全不发营销消息;后续通过主动互动恢复画像分后才能移出。
  • 黑名单:不参与任何群发。

这些规则优先级高于成本系数。原因是:投诉和退订用户的“代价”不是线性增长,而是断崖式增长——发一条骚扰消息给刚投诉过的人,可能直接导致账号被拉黑,这不是消耗 2 个令牌能模拟的。

3.4 动态权重下的全局保护:别让低活跃用户吃光预算

接入动态成本后,出现过一个有趣的问题:因为最活跃的用户成本只有 0.4,系统倾向于优先把消息发给高活跃人群,导致高活跃池的触达频率过高,低活跃池反而“被保护”过头了。

解决办法是两个维度同时限。令牌桶按照“人群分桶”,每个池子有自己的桶,比如高活跃池refillPerSecond = 20,普通池= 30,然后每个桶内部再用动态成本调权。这样既保证容量按人群分配,又保证同池内部用户之间有差异化。

真正落到代码上也不复杂,就是一个BucketManager,用ConcurrentHashMap<String, TokenBucket>维护多个桶,key 可以是业务线:人群池,比如marketing:high-active、service:normal。每次发送前,先用画像和黑名单规则确定它在哪个池,再从对应的桶取令牌。

4. 动态限流引擎的完整架构:从单机桶到集群方案

4.1 模块切分与一次发送的完整链路

单机版跑通之后,我们开始把引擎模块化,方便多实例部署。代码结构大致分四个部分:画像服务、限流核心、决策器、发送执行器。

  • 画像服务:负责拉取标签、行为数据,算profileScore和costFactor,结果缓存 10 分钟。
  • 限流核心:包含令牌桶和动态参数配置,对外暴露tryAcquire(userId, scene)。
  • 决策器:组合黑名单、人群隔离、优先级、令牌桶结果,输出最终动作:放行、排队、拦截、降级。
  • 发送执行器:真正调用推送通道,负责幂等去重和失败重试。

一次完整发送的决策链路,我简单描述一下:

  1. 消息进入引擎,带上userId + scene。
  2. 查询画像服务,得到costFactor和人群池pool。
  3. 走决策器硬规则:黑名单直接拦截,低活跃池营销消息直接拦截。
  4. 从BucketManager拿到对应池的桶,尝试消耗costFactor个令牌。
  5. 拿到令牌后,生成消息唯一 ID,写入发送记录表,再交给发送执行器。
  6. 拿不到令牌,按消息优先级进入延迟队列或指数退避。

这个流程看起来就是几行代码的事,但每一步都对应一个线上真实踩过的坑。画像服务高可用问题,限流核心 Redis 抖动问题,发送记录表撑不住的问题,全部要提前做预案。

4.2 Redis + Lua 分布式令牌桶

单机版多实例部署后,第一个炸出来的问题是:每个实例都有自己独立的桶,总速率会叠加。比如两台机器各自配refillPerSecond = 50,总体可能跑到 100,等于限流失效。

分布式方案我们选了 Redis + Lua,原因很简单:Lua 脚本在 Redis 里是原子的,可以安全地完成“读取令牌、补充令牌、扣减令牌、回写”这整个操作序列。

脚本核心逻辑示意如下:

local tokenKey = KEYS[1] .. ':token' local timeKey = KEYS[1] .. ':time' local capacity = tonumber(ARGV[1]) local refillRate = tonumber(ARGV[2]) local need = tonumber(ARGV[3]) local now = tonumber(redis.call('TIME')[1]) local tokens = tonumber(redis.call('get', tokenKey) or capacity) local last = tonumber(redis.call('get', timeKey) or now) local delta = now - last if delta > 0 then tokens = math.min(capacity, tokens + delta * refillRate) end redis.call('set', timeKey, now) if tokens >= need then redis.call('set', tokenKey, tokens - need) return 1 end redis.call('set', tokenKey, tokens) return 0

Java 侧只需要把脚本传给DefaultRedisScript<Long>,每次调用传入key和三个参数。注意need必须支持浮点,所以 Lua 里tonumber之后做比较是没问题的,Java 侧返回的Long只是“是否放行”的标记。

用分布式桶还有个工程细节:key 不能设计成全局唯一一个大桶。单桶在低并发时很好,但一旦流量冲高,这个 key 会变成 Redis 热 Key,而且业务之间互相挤占。我们最终的 practice 是:key = 业务线:人群池:分钟级时间片,这样既能把热点分散,又能天然实现“不同人群不同预算”。

4.3 两级限流:本地桶兜底,中心桶管控

全链路每次发送都查一次 Redis,性能会很尴尬。我们做了两级限流:

第一级是本地无锁令牌桶,负责挡住明显的超量请求。比如某台实例在 10 毫秒内收到了 2000 个发送任务,本地桶直接拦下大部分,有效降低 Redis 的压力。

第二级才是 Redis 分布式桶,真正做全局速率控制。本地桶的容量和速率设成全局配置的 20%,这样即使多个实例同时放行,全局总量也不会超过中心桶太多。

有一个坑必须提:两级桶的参数不能各自独立调。我们一开始把本地桶设得比较宽,结果每个实例各自放行了一批,中心桶迅速被打满,大量消息排队。后来把两级参数收敛到同一个表达式里——本地桶速率为全局的1/N(N 为预期实例数),中心桶速率为全局配置,才稳定下来。

4.4 画像数据更新与动态配置下发

画像服务的实时性直接决定限流效果。活跃度、互动率这类特征可以小时级更新,而投诉、退订这种负面信号必须准实时,最好 5 分钟内同步到画像缓存中。我们的做法是:投诉/退订事件通过业务消息队列推送给画像服务,画像服务更新后主动失效 Redis 里的用户缓存;而定时任务只负责刷新活跃度指标。

动态限流引擎的“动态”还体现在参数可以运行时调整。我们把每类消息的基准速率、桶容量、人群池划分都放到配置中心,运营活动前只需要改配置,不需要发版。上线初期我发现运行同学经常在后台改数值,后来干脆做了一个简单的“限流策略预览”页面,输入活动预估人数、期望发送时长,自动算出推荐速率和容量。

4.5 异常降级:Redis 抖动的时候我们不能停

Redis 一旦超时或不可用,所有限流请求都会卡住,发送链路会雪崩。我们的降级策略分三档:

一档:Redis 超时时间控制在 50 毫秒以内,失败后直接走本地桶放行,但同步把本地桶速率降为正常值的 1/3。如果连本地桶都是满的,就直接拒绝并让消息排队。

二档:连续 N 次 Redis 连接失败,开启熔断。熔断期内所有业务统一走保守本地速率,同时发送记录照常落库。

三档:恢复后不立刻切回全量流量,而是用“慢启动”逐步增加 Redis 桶的调用比例,避免恢复瞬间又把 Redis 打成热点。

这套策略的核心思想是:限流器可以暂时不够精确,但绝对不能变成整个发送链路的新故障点。

5. 上线后踩过的那些坑:压测、监控与参数调优

5.1 压测不要只堆 RPS,要按画像分布构造

第一次做压测的时候,我们理所当然地拿测试账号跑全量随机流量,结果发现令牌桶拦下的比例非常低,性能指标也好看得不像真的。后来一查原因:测试账号没有任何画像数据,默认costFactor = 1.0,而且在同一个池子里均匀分布,动态成本根本没生效。

正确的压测方式是模拟真实人群分布。比如高活跃 20%、普通 60%、沉默 20%,并按真实用户画像分布生成对应的costFactor,再用脚本控制流量曲线,让前 10 秒打一个 3 倍峰值,再回落到均值。你会发现,决策逻辑正确时,低活跃用户被拦截的比例远高于高活跃用户,这才是正常现象。

压测过程中建议把每个池子的通过/拒绝数据都打出来,观察动态成本是否真的在按画像分流。

5.2 必须盯住的四个核心指标

限流引擎上线后,我们监控面板上有四个核心指标:

  • 桶水位:当前存量令牌占总容量百分比。长期接近 0,说明速率配得太紧;长期接近 100%,说明业务根本没有触达瓶颈。
  • 拒绝量:每个池子单位时间被拒绝的消息数。拒绝量飙升时,要去排查是配置过紧、画像成本计算异常还是真的流量洪峰。
  • 发送成功率:放行后通道返回的成功比例。成功率低说明限流不是瓶颈,下游通道才是。
  • 投诉率/退订率:这是最容易被忽略的“结果指标”。限流做得再好,如果投诉率没有下降,那一切都没有意义。我们每轮活动后都会拉一次人群维度的投诉率,用数据倒推画像权重是否需要调整。

这四个指标要放在一张图里看,单独看任何一列都容易误判。比如拒绝量变高,但发送成功率反而变高,说明限流挡掉了一批本不该发的低价值流量,策略是有效的。

5.3 参数调优记录与一些经验

我们上线两个月内最重要的参数调优记录,大致可以整理成一张表:

问题现象排查方向调优策略
活动刚开始大量消息排队,延迟飙升桶容量 B 太小按峰值持续时间重算 B,适当上调 20%
日常发送速率过低,队列堆积补令牌速率 R 偏小拉长目标时长重新计算 R
高活跃用户收到消息频率过高,开始出现退订高活跃池权重 0.4 太便宜把最低 costFactor 从 0.4 提到 0.6
某类营销消息投诉率异常画像标签维度缺失增加“同类消息历史投诉”维度做惩罚
深夜活动触发大量发送缺少时段因子给深夜时段叠加成本系数 1.5

一个有价值的教训是:参数不要频繁微调。每调整一次至少观察 3 到 5 天,否则你很难区分是参数效果还是业务节奏本身在变化。我们吃过一次亏:某个池子投诉率上升,我立刻把成本系数从 1.0 提到 1.8,结果转化率也跟着跌了不少——后来发现是那周素材质量太差,根本不是频率问题。

5.4 分布式场景下的公平性与热点问题

最后补充两个分布式限流特有的问题。

第一个是公平性。全局只有一个 Redis 桶时,如果某个大池子连续抢占令牌,其他池子可能长期饿死。所以我们坚持“每池一桶”,并且给调度层加了类似“加权轮询”的配额倾斜机制:每个池子有保底速率,比如普通池保底refillRate * 0.3,剩余 70% 按实时水位竞争。

第二个是热 Key 问题。某个千万级用户池的桶 key 可能成为 Redis 热点。我们的缓解方案是按时间片分桶,比如marketing:normal:20250615-10:00,这个 key 只在当前时间窗口内被访问,窗口结束就自然分散到下一个 key。配合本地桶前置拦截,Redis 侧单 key 的压力可以控制在合理范围。

还有个容易被忽略的细节:TIME命令在 Lua 脚本中返回的是 Redis 服务器时间,多实例部署时尽量避免本地时钟参与限流计算,否则会出现不同实例速率不一致的歪曲现象。我们所有时间计算都以 Redis 时间为准。

最后说一个我自己调整这个引擎的心得:上线后真正让参数变准的不是压测报告,而是每天导出的拒绝日志。我会定期把被限流的用户 ID 按画像维度聚合,看被拦截最多的是哪类人群、他们后续的转化率是多少。如果被拦截的客户里转化率反而很高,说明画像权重算错了;如果被放行的客户投诉了,说明某些负面标签没有及时拉高成本。限流引擎表面上是控制发送频率,本质上是一个帮你不断修正“对用户理解”的反馈回路。再遇到运营同学说“再补一轮”,我第一反应已经不是焦虑了,而是打开面板看一眼当前桶水位和画像分布,把参数调对了再松手。

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

SpringBoot+Vue同城上门喂遛宠物系统:前后端分离设计与部署全攻略

周末在家码了两天&#xff0c;把同城上门喂遛宠物系统从零完整落地到部署上线。这套系统用了最经典的前后端分离组合&#xff1a;SpringBoot负责后端接口&#xff0c;Vue搭前端页面&#xff0c;MyBatis管理数据库操作&#xff0c;MySQL存业务数据。源码和部署教程我都同步整理出…

作者头像 李华
网站建设 2026/10/8 8:48:16

鸿蒙Flutter适配:正确使用characters包处理Unicode字符簇

鸿蒙化适配这件事&#xff0c;做底层字符处理的时候是真的容易踩坑。前段时间在一个跨端项目里处理输入框字符长度限制和表情统计&#xff0c;发现同样一段文案&#xff0c;iOS 上数出来 5 个字符&#xff0c;Android 上数出来 7 个&#xff0c;到了鸿蒙那边又变成 6 个。追根溯…

作者头像 李华
网站建设 2026/10/8 8:47:01

AI时代职业升级指南:从任务拆解到AI Agent工作流实战

这两年的AI热潮&#xff0c;说实话&#xff0c;最让我有感的不是各种发布会上的参数&#xff0c;而是身边越来越多的朋友开始问同一个问题&#xff1a;“这玩意儿会不会让我失业&#xff1f;”我所在的企业服务群里&#xff0c;天天有人转发AI短剧、AI写代码、AI Agent代替员工…

作者头像 李华
网站建设 2026/10/8 8:45:52

SparkSQL性能优化实战:从数据倾斜到Catalyst优化器关键技巧

1. 先说清楚&#xff1a;SparkSQL到底替你扛了哪些脏活累活下午刚帮同事排查完一个跑了40分钟的Spark任务&#xff0c;最后定位到问题出在一段写得极其绕的DataFrame API链式调用上——我把它改写成三条SQL&#xff0c;执行时间掉到7分钟。这种场景在我手里已经发生过无数次&am…

作者头像 李华
网站建设 2026/10/8 8:45:21

智能体感知系统从入门到实战:看得清,才想得对

1. 感知系统到底是什么&#xff1a;为什么它是智能体的大脑入口&#xff0c;而不是传感器 做了几年智能体项目&#xff0c;我越来越觉得&#xff0c;多数团队对"感知系统"的理解是模糊的。大家一上来就调模型、写工具、接API&#xff0c;结果第一版demo经常是&#x…

作者头像 李华
网站建设 2026/10/8 8:45:20

双创大赛评委打分系统实战:规则设计、技术选型与现场避坑

做赛事技术支持这行&#xff0c;最怕的不是系统当场崩了&#xff0c;而是比赛流程被一些不起眼的小环节拖到崩溃。前阵子我负责了“邮储杯”嘉兴乡村振兴双创大赛的评委打分系统&#xff0c;40多个参赛项目&#xff0c;20多位评委&#xff0c;上午场路演、下午场颁奖前要出完整…

作者头像 李华