下面我将从核心逻辑、技术实现、关键挑战与优化三个方面,为你详细拆解如何开发一个抢单功能。

核心逻辑与业务流程
在写代码之前,必须先理清楚业务流程,一个完整的抢单流程通常包括以下几个环节:
-
任务创建与发布
- 触发方:用户(C端)或商家(B端)。
- 操作:创建一个任务/订单,包含所有必要信息,如:任务类型、取货/送达地址、物品信息、期望完成时间、悬赏金额等。
- 状态:任务状态变为
待抢单或等待接单。
-
任务广播与发现
- 触发方:系统。
- 操作:系统将
待抢单的任务推送给在线的、符合条件的执行者(骑手/师傅)。 - 关键:推送的范围和规则决定了抢单的公平性和效率。
-
执行者抢单
(图片来源网络,侵删)- 触发方:执行者(骑手/师傅)。
- 操作:执行者在App/小程序上看到任务列表,点击“抢单”按钮。
- 关键:这里需要有一个“锁单”机制,防止多人同时抢到同一个单。
-
抢单结果处理
- 成功:
- 系统将任务状态从
待抢单更新为已接单或进行中。 - 将该任务从其他执行者的可抢单列表中移除。
- 通知下单用户和接单的执行者,告知订单已被接取。
- 记录抢单日志,用于后续的数据分析和风控。
- 系统将任务状态从
- 失败:
- 手速慢了:在你点击抢单的瞬间,已经有别人抢成功了,系统需要提示“手慢了,单子已被抢走”。
- 资格不符:点击后发现不符合抢单条件(如距离太远、技能不符等),前端应做校验,后端也要做二次校验。
- 成功:
-
任务后续流程
- 触发方:执行者。
- 操作:执行者开始执行任务(如取货、配送),完成后在系统中标记任务完成。
- 状态:任务状态变为
已完成。
技术实现方案
数据库设计
这是整个功能的基础,你需要设计几张核心表:
tasks (任务表)
| 字段名 | 类型 | 描述 |
| :--- | :--- | :--- |
| id | BIGINT | 任务ID (主键) |
| task_no | VARCHAR(64) | 业务号,如订单号 |
| user_id | BIGINT | 下单用户ID | | VARCHAR(255) | 任务标题 |
| type | TINYINT | 任务类型(1:外卖, 2:跑腿...) |
| pickup_location | POINT | 取货地点 (GIS坐标) |
| delivery_location | POINT | 送达地点 (GIS坐标) |
| status | TINYINT | 核心状态 (1:待抢单, 2:已接单, 3:进行中, 4:已完成, 5:已取消) |
| reward | DECIMAL(10,2) | 悬赏金额 |
| created_at | DATETIME | 创建时间 |
| picked_at | DATETIME | 接单时间 |
| assigned_to | BIGINT | 接单人ID (执行者ID) |

users (用户表)
| 字段名 | 类型 | 描述 |
| :--- | :--- | :--- |
| id | BIGINT | 用户ID (主键) |
| name | VARCHAR(64) | 用户名 |
| role | TINYINT | 用户角色 (1:C端用户, 2:执行者) |
| status | TINYINT | 用户状态 (1:在线, 2:离线, 3:忙碌中...) |
order_logs (订单日志表) - 强烈推荐
| 字段名 | 类型 | 描述 |
| :--- | :--- | :--- |
| id | BIGINT | 日志ID (主键) |
| task_id | BIGINT | 关联的任务ID |
| operator_id | BIGINT | 操作人ID |
| operator_type | TINYINT | 操作人类型 (1:用户, 2:执行者, 3:系统) |
| action | VARCHAR(32) | 操作行为 (e.g., 'created', 'grabbed', 'completed') |
| remark | VARCHAR(255) | 备注 |
| created_at | DATETIME | 操作时间 |
抢单逻辑实现(核心)
这里的关键是解决并发问题,即“超卖”问题,必须保证一个任务只能被一个人抢到,以下是几种主流的实现方案:
数据库乐观锁(推荐,简单高效)
这是最常用和最简单的方法,利用数据库的 UPDATE ... WHERE ... 语句的原子性。
流程:
- 执行者点击“抢单”。
- 后端代码发起一个
UPDATE请求。 - SQL语句示例:
UPDATE tasks SET status = 2, assigned_to = ?, picked_at = NOW() WHERE id = ? AND status = 1; -- status = 1 是“待抢单”状态
- 判断结果:
UPDATE语句影响的行数> 0,说明抢成功了!- 如果影响的行数
= 0,说明抢失败了,可能的原因是:- 单子已经被别人抢走了(
status不再是1)。 - 任务ID不存在。
- 单子已经被别人抢走了(
优点:
- 实现简单,不依赖额外的中间件。
- 利用数据库原生机制,可靠性高。
缺点:
- 在极高并发下(如秒杀场景),可能会产生大量无效的
UPDATE语句,增加数据库压力。
Redis 分布式锁(更强大的控制)
如果业务逻辑更复杂,或者需要更精细的抢单控制(比如只推送给3公里内的骑手),可以使用Redis。
流程:
- 生成锁:当任务进入
待抢单状态时,在Redis中为这个任务ID创建一个锁,SET task_lock:{task_id} {executor_id} NX PX 5000,意思是:如果task_lock:{task_id}这个键不存在,就设置它,值为执行者ID,并设置5秒的过期时间(防止死锁)。 - 抢单人尝试获取锁:执行者点击抢单时,尝试用
SETNX命令获取这个锁。- 获取成功:说明自己是第一个拿到锁的人,然后执行数据库的
UPDATE操作,将任务状态改为已接单,并释放Redis锁 (DEL task_lock:{task_id})。 - 获取失败:说明别人已经先拿到了锁,直接返回“手慢了”。
- 获取成功:说明自己是第一个拿到锁的人,然后执行数据库的
- 数据库操作:即使拿到了Redis锁,也要执行数据库的
UPDATE操作来最终确认,因为Redis可能存在数据丢失或主从同步延迟的风险。
优点:
- 可以在业务层面做更多控制(如资格预审)。
- 减轻了数据库的并发压力。
缺点:
- 引入了Redis依赖,系统复杂度增加。
- 需要处理锁的续期、释放等细节,避免死锁。
消息队列(如RabbitMQ, RocketMQ)
这种方式更适合“系统派单”或“抢单广播”的场景,但也可以用于抢单。
流程:
- 发布任务:当任务创建后,系统向一个抢单队列(如
grab_order_queue)发送一个消息,消息体包含任务ID。 - 消费者抢购:所有在线的执行者服务都作为这个队列的消费者。
- 每个消费者从队列中获取一个消息(任务ID)。
- 消费者获取到消息后,立刻执行数据库的乐观锁
UPDATE操作。 - 如果抢成功,则开始处理后续逻辑;如果失败,则忽略该消息。
- 消息确认机制:确保消费者处理成功后才从队列中移除消息,避免消息丢失。
优点:
- 天然解耦,削峰填谷。
- 可以通过多个消费者水平扩展抢单能力。
缺点:
- 系统架构最复杂,引入了MQ组件。
- 抢单延迟取决于MQ的消费速度。
推送策略(如何让执行者看到单子)
这是抢单功能体验的关键。
-
广播推送:
- 实现:所有
待抢单的任务都推送给所有在线的执行者。 - 优点:抢单成功率高,任务能被快速接取。
- 缺点:对执行者干扰大,可能导致“单子满天飞”的糟糕体验。
- 实现:所有
-
地理围栏推送:
- 实现:只将任务推送给地理位置在任务附近(例如3-5公里)的在线执行者。
- 技术:需要用到地理位置索引(如MySQL的
SPATIAL索引,或PostGIS扩展)来快速查询附近的执行者。 - 优点:执行者看到的单子都是相关的,体验好,也符合业务逻辑(远处的骑手不会来抢近处的单)。
- 缺点:技术实现稍复杂,需要维护执行者的实时位置。
-
混合策略:
- 实现:结合以上两种,先根据地理位置筛选出候选执行者,然后随机或按某种规则(如等级、抢单成功率)向其中一部分人推送。
- 优点:平衡了效率和体验。
关键挑战与优化
-
公平性问题
- 问题:如何防止“外挂”或“专业抢单团队”垄断所有好单?
- 解决方案:
- 引入随机性:在推送时加入随机因素,而不是固定顺序。
- 抢单冷却:执行者抢到一单后,设置一个短暂的冷却时间(如30秒),在此期间不能抢下一单,给其他人机会。
- 信誉/等级体系:信誉高、服务好的执行者可以有更高的抢单优先级或看到更多优质单子。
-
性能与高并发
- 问题:在高峰期(如午晚高峰),可能有大量任务和大量执行者同时在线,系统压力巨大。
- 解决方案:
- 缓存:使用Redis缓存热门任务列表、执行者在线状态等,减少数据库查询。
- 读写分离:抢单主要是写操作,但查询任务列表是读操作,可以利用数据库读写分离来分担压力。
- 异步化:抢单成功后的通知、日志记录等操作,可以通过异步消息队列处理,不要阻塞主流程。
-
用户体验
- 问题:执行者如何快速找到合适的单子?如何避免无效点击?
- 解决方案:
- 智能排序:在任务列表中,根据距离、赏金、任务类型等维度对单子进行排序。
- 资格预检:在执行者点击“抢单”按钮的前端,就根据当前状态(如距离、技能)判断他是否有资格抢,这可以减少无效的后端请求。
- 实时反馈:无论抢成功还是失败,都要给用户一个清晰、及时的反馈。
开发一个抢单功能,可以按照以下步骤进行:
- 明确业务流程:梳理从任务发布到完成的完整闭环。
- 设计数据模型:建好核心的
tasks表和必要的日志表。 - 选择抢单核心逻辑:对于大多数O2O业务,数据库乐观锁是性价比最高的起点。
- 设计推送策略:地理围栏推送是提升用户体验的关键。
- 考虑并发与性能:引入缓存、异步等机制应对高并发。
- 关注公平性与体验:通过随机、冷却、排序等手段优化,让系统更健康、用户更满意。
从简单到复杂,你可以先用乐观锁实现一个基础版,然后根据业务发展,逐步引入地理围栏、消息队列等更高级的特性。
