订单风险前置校验:把风险挡在门外
做交易系统这么多年,我有个很深的体会:风险控制,越早做越好。等订单进了核心交易链路再拦截,就像火都烧到厨房了你才去找灭火器——晚了。
今天聊的这三个校验,说白了就是三道门岗。订单还没进交易大厅,先在外面过一遍筛子。我习惯叫它「前置校验三件套」:金额阈值、频率限制、重复检测。
一、金额阈值校验:别让大单砸懵系统
金额阈值校验,名字听着挺唬人,其实逻辑很简单——超出设定范围的订单,直接拦下。
我在项目中遇到过一件事:某次促销活动,系统突然涌入一批金额异常高的订单。正常客单价也就200块,结果来了几笔单笔50万的。后来一查,是某个渠道的接口参数传错了。你说这要是没拦住,直接进到清算环节,那麻烦就大了。
校验维度
- 单笔金额上限:比如单笔不超过10万
- 单笔金额下限:比如不低于0.01元(防止刷单)
- 累计金额上限:比如单个用户当日累计不超过100万
核心原则:阈值不是拍脑袋定的,要结合业务场景和历史数据。我一般会拉过去90天的交易数据,取P99.9分位值作为上限参考。
代码示例
public class AmountThresholdValidator {
// 配置化:从配置中心动态获取
private static final double MAX_SINGLE_AMOUNT = 100000.00;
private static final double MIN_SINGLE_AMOUNT = 0.01;
public ValidationResult validate(Order order) {
// 单笔上限校验
if (order.getAmount() > MAX_SINGLE_AMOUNT) {
return ValidationResult.fail("订单金额超过上限");
}
// 单笔下限制校验
if (order.getAmount() < MIN_SINGLE_AMOUNT) {
return ValidationResult.fail("订单金额低于下限");
}
return ValidationResult.success();
}
}
小技巧:阈值别写死在代码里。用配置中心(比如Apollo、Nacos)动态下发,这样调整阈值不用重启服务。我曾经因为硬编码吃过亏,后来就学乖了。
二、订单频率限制:别让刷子把系统刷崩
频率限制,说白了就是控制单位时间内的请求次数。为什么需要这个?你想想看,正常用户谁会一秒下10单?
我见过最夸张的一次,某个用户用脚本在1秒内下了2000单。要不是频率限制拦住了,数据库连接池直接被打满,整个交易链路都得瘫痪。
常见的限流策略
| 策略 | 原理 | 适用场景 |
|---|---|---|
| 固定窗口 | 统计固定时间窗口内的请求数 | 简单场景,实现容易 |
| 滑动窗口 | 按时间粒度滑动统计 | 精度要求高的场景 |
| 令牌桶 | 匀速发放令牌,有令牌才能请求 | 突发流量场景 |
| 漏桶 | 固定速率处理请求 | 流量整形场景 |
我个人习惯用滑动窗口 + 令牌桶的组合。滑动窗口做第一道防线,令牌桶做第二道。为什么?滑动窗口能精确控制时间粒度,令牌桶能应对突发流量。两者互补,效果不错。
频率限制的维度
- 用户维度:单个用户每秒/每分钟最多N单
- IP维度:单个IP每秒最多N次请求
- 设备维度:单个设备ID每分钟最多N单
- 全局维度:系统整体每秒最多处理N单
注意:频率限制的阈值设置要留有余量。比如系统能扛1000 TPS,你限流设成800就行。别卡着上限设,万一有突发流量,系统容易被打穿。
三、订单重复性检测:别让同一笔订单被处理两次
重复订单,是交易系统里最头疼的问题之一。用户多点了一下「提交」,网络抖动导致重试,消息队列重复投递——这些都会导致同一笔订单被创建多次。
我记得有一次线上事故,用户下单买一台手机,结果系统给他创建了3个订单。用户投诉说「我就买一台,你们扣了我三台的钱」。嗯,那天的值班日志写得特别长。
幂等性设计
解决重复订单的核心思路是幂等性——同一笔请求,无论处理多少次,结果都一样。
实现方案
- 唯一键约束:用订单号或业务流水号作为数据库唯一索引
- 分布式锁:处理前先加锁,处理完释放
- 状态机校验:订单状态只能单向流转,已处理的订单不再处理
代码示例
public class DuplicateOrderDetector {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean isDuplicate(String orderNo) {
// 使用Redis的SETNX命令实现分布式锁
// key: order:duplicate:{orderNo}
// value: 当前时间戳
// 过期时间: 24小时
String key = "order:duplicate:" + orderNo;
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, String.valueOf(System.currentTimeMillis()),
24, TimeUnit.HOURS);
// 如果返回false,说明key已存在,订单重复
return !Boolean.TRUE.equals(result);
}
}
避坑指南:我曾经用数据库唯一索引做重复检测,结果高并发下频繁报主键冲突异常。后来改成Redis的SETNX,性能提升了一个数量级。记住:重复检测要放在缓存层做,别直接怼数据库。
四、三者如何协同工作
这三个校验不是各自为战的。我一般会设计一个前置校验管道,按顺序执行:
- 先做金额阈值校验(计算量最小,最快)
- 再做重复性检测(需要查缓存,稍慢)
- 最后做频率限制(需要统计计数,最慢)
为什么这么排?你想想看,如果金额都不对,后面两个校验就没必要做了。这叫快速失败,能省不少资源。
五、核心逻辑流程图
下面这张图,是我自己画的前置校验流程。你看一眼就明白了:
这张图里,三个校验是串行执行的。每个校验失败,订单直接进入「拦截」分支。只有三个都通过,订单才能进入后续的交易链路。
总结一下:金额阈值校验防大单、频率限制防刷单、重复检测防重单。这三道门岗守住了,交易系统的稳定性就有了基本保障。
嗯,前置校验这块,说白了就是把简单的事情做到极致。别小看这三个校验,很多线上事故都是因为「觉得太简单没做」导致的。我见过太多案例了,教训深刻啊。
最后说一句:这三个校验的日志一定要打全。哪天线上出问题了,查日志的时候你就知道这些日志有多重要了。我曾经因为少打了一行日志,排查问题花了整整一个通宵……
公众号:蓝海资料掘金营,微信deep3321