[特殊字符]实战:使用 Canal + MQ + ES + Redis + XXL-Job 打造高性能地理抢单系统
📚目录
-
项目背景
-
技术栈总览
-
详细流程分析
-
3.1 Canal监听MySQL Binlog
-
3.2 MQ中转传递订单变化
-
3.3 Elasticsearch存储并查询附近订单
-
3.4 Redis高性能抢单+Lua防止抢单冲突
-
3.5 XXL-Job定时任务处理
-
-
完整系统流程图
-
总结
一、项目背景
针对类似外卖、跑腿、上门维修等业务场景,存在大量订单需要快速分配给周边服务人员。为了保证抢单速度快、系统高可用,我们设计了一套基于:
-
MySQL主库 + Canal监听Binlog
-
MQ异步推送订单变更
-
Elasticsearch进行地理位置查询
-
Redis加速抢单并防止重复抢单
-
XXL-Job异步修正状态
的完整解决方案。
二、技术栈总览
技术 | 用途 |
---|---|
Canal | 监听数据库Binlog变化 |
MQ (RocketMQ) | 订单变更异步推送 |
Elasticsearch | 地理位置检索订单 |
Redis | 高并发缓存抢单、Lua保证原子操作 |
XXL-Job | 定时任务处理订单 |
三、详细流程分析
3.1 Canal监听MySQL Binlog
目的:实时感知订单表(如:order
)中数据变化(新建/更新/删除)。
步骤:
-
Canal连接MySQL,模拟一个Slave。
-
订阅需要监听的表,比如:
instance.filter.regex = db_name.order
-
把监听到的变更事件,转换成统一格式,推送到MQ。
注意:
-
Canal要有正确的binlog起始点。
-
reset master
后要删除Canal的meta.dat
文件。
Canal配置关键项:
canal.instance.master.address=127.0.0.1:3306
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.filter.regex=order.*
3.2 MQ中转传递订单变化
目的:削峰填谷,异步解耦,解放数据库压力。
-
Canal将每条订单变更消息发送到MQ,例如RocketMQ的
OrderChangeTopic
。 -
消费端程序监听此Topic,异步拉取消息,处理数据同步到ES和Redis。
RocketMQ发送格式示例(JSON):
{"eventType": "UPDATE","orderId": "123456","status": "NEW","longitude": 113.2644,"latitude": 23.1291
}
3.3 Elasticsearch存储并查询附近订单
目的:让服务人员可以基于地理位置检索周边5km内未被抢的订单。
订单结构设计(Mapping):
{"mappings": {"properties": {"orderId": { "type": "keyword" },"status": { "type": "keyword" },"location": { "type": "geo_point" }}}
}
Java (ES 8+ Lambda Client) 地理查询示例:
SearchResponse<OrderDoc> response = client.search(s -> s.index("orders").query(q -> q.bool(b -> b.must(m -> m.match(mq -> mq.field("status").query("NEW"))).filter(f -> f.geoDistance(gd -> gd.field("location").distance("5km").location(l -> l.latlon(23.1291, 113.2644)))))),
OrderDoc.class
);
注意:
-
地理位置需要保存为
geo_point
格式。 -
查询时注意单位(km、m)。
3.4 Redis高性能抢单+Lua防止抢单冲突
目的:防止高并发下,出现多个服务人员抢到同一订单的问题。
抢单流程:
-
抢单时直接查Redis,避免访问MySQL。
-
使用Lua脚本,原子性判断是否已被抢。
Lua抢单脚本示例:
if redis.call("get", KEYS[1]) == false thenredis.call("set", KEYS[1], ARGV[1])return 1
elsereturn 0
end
Java调用Lua脚本示例:
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScriptContent);
redisScript.setResultType(Long.class);Long result = stringRedisTemplate.execute(redisScript,Collections.singletonList("order:123456"),"TAKEN"
);if (result == 1L) {// 抢单成功
} else {// 已被抢
}
注意:
-
KEY设计为
order:{orderId}
-
失效时间根据业务设置,比如订单未支付自动失效。
3.5 XXL-Job定时任务处理
目的:在服务人员未支付、超时未接单等情况下,自动取消订单,释放资源。
常见定时任务:
-
每分钟扫描未支付的订单,关闭超时订单。
-
抢单后超时未接单,订单重新入池。
XXL-Job处理示例:
@XxlJob("orderTimeoutHandler")
public void orderTimeoutHandler() {List<String> timeoutOrderIds = orderService.findTimeoutOrders();for (String orderId : timeoutOrderIds) {orderService.cancelOrder(orderId);}
}
调度配置:
-
调度周期:每1分钟
-
失败重试:3次
-
超时时间:30秒
四、完整系统流程图
五、总结
通过这一套组合:
-
Canal保证数据库变化实时捕捉
-
MQ解耦数据同步和订单更新
-
ES支撑复杂地理位置查询
-
Redis + Lua保障高并发原子抢单
-
XXL-Job负责后置状态修正
整套抢单系统实现了:
✅ 高可用
✅ 高性能
✅ 地理范围精准控制
✅ 数据一致性
适合各类跑腿、外卖、上门维修等业务场景。