利用Redis实现排队需求

利用Redis实现排队需求

近期在项目中做了一个用户排队等待接入客服的需求,此文记录自己的实现思路与过程,以及一些考虑的异常。

需求

大部分人都有排队等待接入客服的经历,所以需求不难理解:“存在一个在线客服列表,用户发起接入请求时,从客服列表中选择空闲的进行配对接入,如果没有可用客服则用户进入等待队列,每当出现客服空闲时接入等待队列中的第一个用户”。

设计

这部分主要描述自己从需求中理解的实体与关系以及选择它们的存储方式。

实体

从这个需求中分解出来的实体有以下几种:

  • 客服
  • 用户
  • 房间

客服接待用户所处的“空间”称为房间,主要作为客服-用户沟通的载体

  • 等待队列

所有没能进入房间的用户形成的队列,是一个先来先服务(FIFO)的队列

房间这个实体是虚拟出来的,可以让用户与客服直接产生联系,这样就不需要房间实体。

关系

上述实体之间的关系:

  • 客服-房间

房间与客服绑定,伴随客服存在,客服接待用户时房间处于占用状态

  • 用户-房间

用户被接入时,用户与房间处于暂时绑定关系且与客服产生间接关系,构成关系:客服-房间-用户

  • 用户-等待队列

没能被接入的用户都处于等待队列中

存储

确定实体与关系后,接着考虑如何存储这些实体与关系,有如下几种选择:

  • 内存

列内存这个选项主要是给小白看,因为曾经在一个项目中见到有人这么做的,所以我认为有必要强调一下选内存方案的致命缺点:

  • 无法水平扩展,在Node.js中意味只能启一个进程,即一个线程
  • 应用重启会导致实体与关系丢失
  • Redis

这个场景用Redis做存储,只能用完美来形容

  • MySQL

这个场景用MySQL做存储,理论上可行,但实体与关系会频繁进行读写,所以在性能方面会远逊于Redis

  • Redis + MySQL

这个方案主要是考虑可以用MySQL存储实体之间的历史关系数据(咨询历史、排队历史等),而用户、客服这类实体是系统外部已存在的,所以不在这个系统内考虑它们的存储

思路

目前没保存实体之间的历史关系数据,所以只用到了Redis,首先定义几个存放在Redis的结构:

S2R(servicer-to-room): key-value,客服ID对应的房间ID
R2U(room-to-user): key-value,房间ID对应的用户ID
U2R(user-to-room): key-value,用户ID对应的房间ID
Q: Sorted-Set,用户排队的队列

// 上述的Redis结构在存储时会对key添加前缀,主要为了避免冲突以及方便`keys`命令搜索key

定义这几个结构后,对其进行合理操作即可实现用户接入与排队,下面对操作过程做一个简要描述:

  1. 客服登录系统,为其分配roomId,即在redis中设置一个servicerId对应roomId的key-value,同时会巧用key的ttl做客服掉线处理
  2. 客服轮询自己的房间状态,可以通过S2R、R2U判断房间内是否有用户接入
  3. 用户发起接入请求,将用户放置到匹配队列,同时设置U2R的数据为'',这里同样可以利用key的ttl做用户掉线判断
  4. 用户加入队列后触发一次匹配接入,随后查询用户U2R的数据可得到接入状态
  5. U2R查到的roomId不为空则用户接入成功,进入房间
  6. U2R查到的roomId为空则用户出于排队中,持续轮询直到接入成功

上述过程中的匹配接入步骤:

  1. 利用keys prefix:xxx方式从S2R中寻找在线客服
  2. 使用mget从S2R中获取客服房间
  3. 使用mget从R2U中获取房间用户,挑选一个空闲房间准备接入
  4. 从Q中取用户,设置U2R与R2U完成匹配

在此方案下每次匹配接入需要查询所有客服的状态并挑选出空闲房间,有一定的IO开销且对Redis会造成一定压力,所以要考虑不同匹配接入触发时机会的利弊,自己想到的方案有如下几种:

  • 用户每次轮询自己排队状态时触发

此方案适用于咨询人数少排队出现频率低的情况,不适合咨询人数多客服少的情景

  • 定时器触发

使用定时器触发匹配的优点是让系统压力分布平均,缺点是定时器的间隔不好把握,太短的话系统压力大,太长的话用户响应时间长(可结合第一种方案优化)

  • 首次排队或客服空闲事件触发

这个方案是一个较优的选择,不会有多余的匹配操作,缺点是客服空闲事件不好捕捉

  • 客服主动触发

前面三个方案都是自动接入,这个方案是由客服主动接入,属于产品经理决定的内容

实现

已有的代码实现依赖所处项目环境,就不贴出来了,这里将重要流程用代码表现一下,并且添加一些注释说明。

const redis = {
   }; // 表示对redis操作的对象
const timeout = 15; // 断线的超时时间,如用户的排队掉线,咨询过程中掉线等

exports.applyRoomByUserId = applyRoomByUserId; // 用户排队请求
exports.markUserAliveById = markUserAliveById; // 用户接入后心跳

exports.assignRoomByServicerId = assignRoomByServicerId; // 给客服分配房间
exports.roomStateByServicerId = roomStateByServicerId; // 客服房间状态
exports.markServicerAliveById = markServicerAliveById; // 客服心跳

// 根据用户id申请房间,即请求接入
  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值