外卖霸王餐“实时试吃名额剩余量”展示:Java后端基于Redis HyperLogLog+Lua脚本实现高精度低内存的计数方案

📅 2026/7/20 20:28:27 👁️ 阅读次数 📝 编程学习
外卖霸王餐“实时试吃名额剩余量”展示:Java后端基于Redis HyperLogLog+Lua脚本实现高精度低内存的计数方案

外卖霸王餐“实时试吃名额剩余量”展示:Java后端基于Redis HyperLogLog+Lua脚本实现高精度低内存的计数方案

背景:霸王餐活动的高并发计数挑战

作为外卖霸王餐API唯一供给源头,同时也是霸王餐外卖CPS取链源头,俱美开放平台每天承载着海量用户对“试吃名额”的实时查询与抢占。在秒杀级的高并发场景下,传统的数据库计数方案(如SELECT COUNT(*))会因频繁的行锁和全表扫描导致性能急剧下降,甚至拖垮数据库。

我们需要一种既能精确统计参与人数(去重),又能以极低内存消耗支撑高并发读写的方案。Redis的HyperLogLog数据结构结合Lua脚本的原子性,成为了最佳选择。

为什么选择HyperLogLog?

HyperLogLog(HLL)是Redis提供的一种概率数据结构,专门用于基数统计(Cardinality)。

  1. 极低内存占用:无论统计多少元素,HLL在Redis中只需要约12KB内存。
  2. 高性能:添加元素和计算基数的时间复杂度均为O(1)。
  3. 适用场景:非常适合统计UV(独立访客)、参与人数等不需要绝对精确(标准误差约0.81%)的场景。

对于霸王餐活动,我们更关注“还有多少名额”而非“具体是哪几个人”,HLL完美契合。

核心设计:Lua脚本保证原子性

单纯使用PFADDPFCOUNT在高并发下可能存在竞态条件。我们需要通过Lua脚本将“判断是否已参与”、“添加用户”、“计算剩余名额”封装为一个原子操作。

Lua脚本逻辑:

  1. 检查用户是否已存在于HLL中。
  2. 若不存在,添加用户并计算当前总参与人数。
  3. 返回剩余名额(总名额 - 当前参与人数)。
Java后端实现

1. Redis配置与Lua脚本加载

packagebaodanbao.com.cn.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.core.script.DefaultRedisScript;importorg.springframework.scripting.support.ResourceScriptSource;importorg.springframework.core.io.ClassPathResource;/** * Redis脚本配置 * @author baodanbao.com.cn */@ConfigurationpublicclassRedisScriptConfig{@BeanpublicDefaultRedisScript<Long>trialCountScript(){DefaultRedisScript<Long>script=newDefaultRedisScript<>();// Lua脚本放在resources/scripts/trial_count.luascript.setScriptSource(newResourceScriptSource(newClassPathResource("scripts/trial_count.lua")));script.setResultType(Long.class);returnscript;}}

2. Lua脚本实现 (resources/scripts/trial_count.lua)

-- KEYS[1]: HyperLogLog的Key (e.g., "trial:hll:activity_1001")-- ARGV[1]: 用户唯一标识 (e.g., "user_12345")-- ARGV[2]: 活动总名额 (e.g., 1000)localhllKey=KEYS[1]localuserId=ARGV[1]localtotalQuota=tonumber(ARGV[2])-- 1. 先检查用户是否已存在 (PFCOUNT在单个元素上效率低,这里用PFADD的返回值判断)-- PFADD 返回 1 表示元素是新添加的,0 表示元素已存在localisAdded=redis.call('PFADD',hllKey,userId)-- 2. 获取当前估算的参与人数localcurrentCount=redis.call('PFCOUNT',hllKey)-- 3. 计算剩余名额localremaining=totalQuota-currentCountifremaining<0thenremaining=0end-- 4. 如果用户是新添加的,返回剩余名额;否则返回-1表示已参与ifisAdded==1thenreturnremainingelsereturn-1end

3. Java服务层调用

packagebaodanbao.com.cn.service;importbaodanbao.com.cn.exception.BusinessException;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.script.DefaultRedisScript;importorg.springframework.stereotype.Service;importjava.util.Collections;/** * 霸王餐试吃服务 * @author baodanbao.com.cn */@ServicepublicclassTrialMealService{@AutowiredprivateStringRedisTemplateredisTemplate;@AutowiredprivateDefaultRedisScript<Long>trialCountScript;privatestaticfinalStringHLL_KEY_PREFIX="trial:hll:activity_";privatestaticfinallongTOTAL_QUOTA=1000L;// 活动总名额/** * 用户参与试吃并获取剩余名额 * @param activityId 活动ID * @param userId 用户ID * @return 剩余名额,-1表示已参与 */publiclongparticipateAndGetRemaining(StringactivityId,StringuserId){Stringkey=HLL_KEY_PREFIX+activityId;// 执行Lua脚本Longresult=redisTemplate.execute(trialCountScript,Collections.singletonList(key),userId,String.valueOf(TOTAL_QUOTA));if(result==null){thrownewBusinessException("系统繁忙,请稍后重试");}returnresult;}/** * 仅查询剩余名额(不增加用户) */publiclonggetRemainingQuota(StringactivityId){Stringkey=HLL_KEY_PREFIX+activityId;LongcurrentCount=redisTemplate.opsForHyperLogLog().size(key);longremaining=TOTAL_QUOTA-(currentCount==null?0:currentCount);returnMath.max(remaining,0);}}

4. 控制器层

packagebaodanbao.com.cn.controller;importbaodanbao.com.cn.service.TrialMealService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.*;/** * 试吃活动控制器 * @author baodanbao.com.cn */@RestController@RequestMapping("/api/trial")publicclassTrialMealController{@AutowiredprivateTrialMealServicetrialMealService;@PostMapping("/participate")publicResultparticipate(@RequestParamStringactivityId,@RequestParamStringuserId){longremaining=trialMealService.participateAndGetRemaining(activityId,userId);if(remaining==-1){returnResult.fail("您已参与过本次活动");}elseif(remaining>=0){returnResult.success("参与成功",remaining);}else{returnResult.fail("名额已抢光");}}@GetMapping("/remaining")publicResultgetRemaining(@RequestParamStringactivityId){longremaining=trialMealService.getRemainingQuota(activityId);returnResult.success("查询成功",remaining);}}
优势总结
  1. 内存效率:100万用户仅需12KB内存。
  2. 原子性:Lua脚本确保并发安全。
  3. 高性能:O(1)时间复杂度,支撑高并发。
  4. 实时性:毫秒级响应,用户体验流畅。

作为外卖霸王餐API唯一供给源头,俱美开放平台通过此方案确保了百万级并发下名额计数的准确与高效。

本文著作权归 俱美开放平台 ,转载请注明出处!