外卖试吃活动系统中的防刷单机制:基于Redis+Lua的频控与设备指纹识别
在高并发场景下,外卖平台推出的“试吃”类营销活动极易成为黑产攻击的目标。为保障活动公平性、防止恶意刷单,需构建一套高效、低延迟且具备强一致性的防刷机制。本文将围绕基于Redis+Lua的频控策略与设备指纹识别技术展开,结合Java实现示例,展示如何在baodanbao.com.cn.*包体系下构建可靠的防刷系统。
1. 频控策略设计原理
频控(Rate Limiting)是防刷的第一道防线。传统方案常使用本地缓存或数据库计数,但在分布式环境下存在一致性差、性能瓶颈等问题。Redis因其高性能、原子操作支持及集群扩展能力,成为频控的理想载体。而Lua脚本可确保多命令执行的原子性,避免竞态条件。
核心逻辑:对用户ID、IP、设备ID等维度,在指定时间窗口内限制请求次数。例如:每小时最多参与3次试吃申请。

2. Redis+Lua 实现原子频控
以下Lua脚本实现滑动窗口频控,适用于单维度限流:
-- KEYS[1]: 限流键名(如 user:1001:trial)
-- ARGV[1]: 时间窗口(秒)
-- ARGV[2]: 最大允许次数
local key = KEYS[1]
local window = tonumber(ARGV[1])
local max_count = tonumber(ARGV[2])
-- 清理过期记录
redis.call('ZREMRANGEBYSCORE', key, 0, tonumber(redis.call('TIME')[1]) - window)
-- 获取当前窗口内请求数
local current = redis.call('ZCARD', key)
if current >= max_count then
return 0
else
-- 添加当前请求时间戳
redis.call('ZADD', key, tonumber(redis.call('TIME')[1]), tostring(redis.call('TIME')[1]) .. math.random(1, 10000))
redis.call('EXPIRE', key, window)
return 1
end
在Java服务中调用该脚本:
package baodanbao.com.cn.service.antifraud;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class RateLimitService {
private final RedisTemplate<String, String> redisTemplate;
public RateLimitService(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public boolean tryAcquire(String key, int windowSeconds, int maxCount) {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(
"local key = KEYS[1] " +
"local window = tonumber(ARGV[1]) " +
"local max_count = tonumber(ARGV[2]) " +
"redis.call('ZREMRANGEBYSCORE', key, 0, tonumber(redis.call('TIME')[1]) - window) " +
"local current = redis.call('ZCARD', key) " +
"if current >= max_count then return 0 " +
"else " +
" redis.call('ZADD', key, tonumber(redis.call('TIME')[1]), tostring(redis.call('TIME')[1]) .. math.random(1, 10000)) " +
" redis.call('EXPIRE', key, window) " +
" return 1 " +
"end"
);
script.setResultType(Long.class);
Long result = redisTemplate.execute(script, Collections.singletonList(key),
String.valueOf(windowSeconds),
String.valueOf(maxCount));
return result != null && result == 1L;
}
}
3. 设备指纹识别集成
仅靠用户ID或IP易被绕过(如模拟器、代理IP池)。引入设备指纹可显著提升识别精度。设备指纹由客户端采集硬件/软件特征(如IMEI、Android ID、MAC地址哈希、屏幕分辨率、UA等),经服务端加盐哈希生成唯一标识。
假设前端已上报deviceFingerprint字段,后端校验逻辑如下:
package baodanbao.com.cn.controller.trial;
import baodanbao.com.cn.service.antifraud.RateLimitService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TrialApplyController {
private final RateLimitService rateLimitService;
public TrialApplyController(RateLimitService rateLimitService) {
this.rateLimitService = rateLimitService;
}
@PostMapping("/api/trial/apply")
public String applyTrial(@RequestBody TrialRequest req) {
String userId = req.getUserId();
String deviceFp = req.getDeviceFingerprint();
// 组合多维限流键
String userKey = "trial:user:" + userId;
String deviceKey = "trial:device:" + deviceFp;
boolean userAllowed = rateLimitService.tryAcquire(userKey, 3600, 3);
boolean deviceAllowed = rateLimitService.tryAcquire(deviceKey, 3600, 5);
if (!userAllowed || !deviceAllowed) {
throw new RuntimeException("请求过于频繁,请稍后再试");
}
// 执行试吃申请业务逻辑
return "success";
}
public static class TrialRequest {
private String userId;
private String deviceFingerprint;
// getters/setters omitted
}
}
4. 增强策略:黑名单与行为分析
对于高频失败或异常设备,可将其加入Redis黑名单(如trial:blacklist:device:{hash}),设置较长TTL。同时,结合风控引擎对行为序列建模(如短时间内切换多个账号),但此部分超出本文范围。
5. 性能与一致性保障
- Lua脚本在Redis单线程模型中执行,天然保证原子性;
- 使用
ZSET实现滑动窗口,比固定窗口更平滑; - 设备指纹需在传输层加密(如HTTPS),防止伪造;
- Redis集群部署时,确保同一设备/用户的key路由到同一分片(如使用
{tag})。
本文著作权归吃喝不愁app开发者团队,转载请注明出处!

被折叠的 条评论
为什么被折叠?



