如何实现一个优雅的派单系统

背景

如果一个app有ugc(用户内容),比如快手,抖音,陌陌,那么一定对应的要有对内容质量的保证,和平台生态的维护工作。其中一定就是要有审核系统。比如有人举报进来,或模型召回的数据,然后交给审核员工派单,审核后对用户进行一定的限制策略。这里考虑实现一套通用的派单 和 入库逻辑,并且满足下面几个要求。

  • 1.可能存在工单优先级的情况,比如一些特定用户(比如主播的举报)要能够提前派到工单
  • 2.可能存在优先级被改变的情况,比如某些工单累计时间比较久要放到优先队列前面
  • 3.可能存在很长时间没有审核员,然后工单累计很多的情况
  • 4.工单在等待审核员工审核的过程中可能被判定为无效工单不需要审核,或被自动同步逻辑同步掉
  • 5.工单在任意状态都存在查询的需求
  • 6.要保证每个审核员拿到的工单都是不同的
  • 7.并且就算一个审核员拿到工单没有审核超过一定时间要能够让其他审核员派到该工单
  • 8.审核的效率要保证,从审核员提交当前工单到获取下一条的工单时间不能太长。

为什么不用消息队列:

消息队列适用的场景比较单一,举报投递,审核员消费,但真实的业务场景比较复杂,消息队列功能太过单一。(1)(2)的需求满足不了,因为消息队列按照FIFO原则,不是优先队列。(3)中的情况发生也会导致消息队列产生积压问题。(4)中如果消息队列中很多单子都被判定无效了那么要过滤很多单子才能找到一个有效的(消息队列中有大量无效工单),这样(8)也无法保证。

方案1 纯mysql实现

落到数据库,标记一个状态,这样3,5可以满足,因为持久化存储可以查询和抗挤压,然后记录一个优先级,每次按照优先级先取优先级高的工单审核(如果超时的优先就先查询超时的,如果没有,然后在按照优先级查询),1,2也可以满足。为了防止多个审核员同时索取工单然后拿到重复工单的情况可以,查出来之后标记成已派发(每个审核员只获取未派发的),对于超过一定时间已派发但没有被审核的可以在记录一个派单时间(查询派单时间在超时时间之外的 或 未派发的)这样6,7也可以满足,对于4可以在被同步掉改成结单状态,无法被处理。

CREATE TABLE `audit_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT, // 
  `status` char(10) DEFAULT 'waiting’, // 工单的待审核状态 'waiting' / ‘deal’
  `created_at` int(11) DEFAULT NULL, //创建时间
  `dispatch_time` int(11) DEFAULT NULL, // 已派单时间 NULL 表示没被派单过
  `priority` int(11) DEFAULT '1, // 优先级
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

// 派单:
BEGIN;
select * from audit_table where status = ‘waiting’ AND (dispatch_time IS NULL OR dispatch_time <5分钟前”) order by priority desc limit 1;
update audit_table set dispatch_time = “现在” where status = ‘waiting' AND id = xxx;
COMMIT;

为了防止查到重复工单,每次查询前要加一个锁,可以考虑自旋锁。另外还要考虑,如果查询过程中工单被更新成已处理的情况,更新的时候判断一下status 为 waiting就相当于一个乐观锁,更新失败了要重试。
在这里插入图片描述

方案2 mysql + redis

如果每条工单都锁一遍很影响审核效率,可以考虑结合redis 队列,审核员不断从redis队列中pop 如果没有数据或数据量比较少的时候触发一个异步或同步任务,往redis队列里增加数据,redis队列可以只放数据库主键,每次派发到工单通过主键去数据库中找其他信息,为了防止多个请求触发更新逻辑,可以在更新逻辑上设置一个nx锁,更新完成后解除掉。这样通过预加载的方式增加派单效率,由于是派单时才会触发增加redis队列,所以不会有积压问题(3),落库可以满足(5),对于(6)pop不会重复,但redis的队列里的数据又可能重复或者有已派发未审核工单的情况,可以在将已派发的工单 货 已经push的工单 单独记录一下,在往redis push 或 pop的时候过滤一下。对于(1)(2)则可以在从库中取数据时就取优先级高的,(4)则需要在更新库的时候检查队列中是否有该工单,对于(7)则可以给记录已派发的工单加个过期时间 或 将久的工单过滤掉。
但这个方案有很多问题

  • 1.优先级实时性不高,高优先级的数据落库后要等当前待审核队列消费完了才能拿到(所以待审核队列不能太长,要及时从库中查询,可以将实时数据放到一个优先队列,当优先队列里没数据了才走普通的队列)
  • 2.在其他逻辑中,比如自动同步逻辑要考虑redis队列 和 数据库一致(或者每次拿出来数据判断一下,然后重新pop)
  • 3.因为在数据库中没有记录哪些工单已派发,又要避免一次从库中拉取所有待审核工单(待审核工单过多,导致慢查询),所以存在明明库中有很多工单,但是又派不到单的情况(获取的都是在已派发或已经放到redis队列里的,如果更新redis队列采用的是异步任务,那么每次派单都会触发,并且redis队列的数量还很低)。有三种方案:
    1. 是对于这种问题可以在表中加一个字段标记已经拉取过(但这样可能导致有工单永远无法拉取到,比如被审核员pop出来但没有审核,这样就需要额外记录一下每个审核员那些没有审核的工单,然后由定时任务回收重新塞回到redis队列中,这样其实就可以不过滤哪些工单已经放到redis队列里,或哪些工单已经分配给其他审核员了)。
    2. 是当派不到单时,增加一下单次从库中拉取的数据条数,但这样会导致库的压力变大。这个就不能记录一下上次拉取的位置(比如最大最小时间),因为这个时间范围内或id范围内有可能有没拉取到的(因为存在优先级,外加limit)。
    3. 是按照时间筛选工单 和 优先级排序工单,这样就可以多记录一个上次获取工单的最大最小时间,下次获取时就查这个范围之外的,但这样也会有第一种方法的问题(需要定时任务回扫),并且会导致比如很晚进来的高优先级单子要等前面所有低优先级单子审核完了才行。

目前公司里现有的就是这套,实现起来比较复杂,而且每个业务方都需要自行过滤是否有已审核的单子,也经常出现库里有单子派不到单子的情况(可以考虑记录一个已经拉取的字段,增加一个回收脚本)。

在这里插入图片描述

方案3 mysql + redis + 守护进程

可以将方案2改进一下,通过定时任务的方式(可以用while sleep(1),常驻进程),检查队列的长度如果小于最小长度就增加量,增加到最大长度。这样将派单和填充队列解耦,同时在这个定时器中可以用来回收那些超时还在审核员手里的工单。其他跟方案2一样,问题也一样。

评论 5 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:深蓝海洋 设计师:CSDN官方博客 返回首页

打赏作者

前人种树

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值