最近与同事协作完成微信客服项目的设计和开发工作。系统要完成的功能也很简单:打通微信与客服IM系统,让用户(像跟普通好友聊天一样)在自己微信上就可以实现与客服系统的直接对话。

对微信生态中好友、消息和群的第三方管理系统也有很多,不过为了稳定性和功能快速定制化,我们依然决定自研这套系统。现简单介绍一下这个系统。

系统功能介绍图

目前该系统在登录的时候仍然需要用手机扫码登录,不过一经登录基本不会掉线。

开发难点 / 技术点

在系统开发过程中遇到的一些难点和有意义的知识点,记录并分享下。

项目从单机到集群:可横向扩容的改造

微信消息轮询获取,目前只能支持单机模式。在较恶劣的情况下(单机偶现不可用、代码发布重启应用时)会出现微信登录状态丢失、微信服务不可用的情况。针对此问题,我们重构了消息轮询逻辑,将微信登录状态、微信接收的消息等内容持久化至Redis中,通过分布式轮询的方式保证微信客服的高可用,详见下方说明:

借助微信API实现的业务逻辑:

客服每登录一个微信号,都需要创建三个线程。其中线程A很快退出,线程B和线程C则持久运行。

线程B和线程C对绑定了微信Session信息的HttpClient重度依赖,微信消息队列也使用的应用共享内存,这导致: 1)应用无法做分布式和可高可用; 2)线程资源无法共享(缺少线程池的概念)

针对这种情况,微信客服项目准备做如下重构: 1)微信Session信息和微信消息,分别接入分布式存储和队列(实验性阶段,暂时用Redis实现) 2)创建线程池,分别执行线程B和线程C的原有业务内容

重构前流程图:

重构后流程图:

并发模式下,如何利用Mysql特性避免同一记录被重复插入

用户通过微信发送的消息到达我方的IM服务器后,需要自动为该用户分配一个会员UID,如果已有则会直接使用该UID。存储UID的表结构如下:

我们会根据表中的备注名生成会员邮箱,所以每次注册会员获取会员的UID时,会员服务化会保证返回的UID总是同一个,我们在获取到该UID后,会尝试将信息插入上图的wx_bb_uid_map表中。

但问题是,我们处理消息的方式是并发的,上面的步骤有可能被并发执行,但是我们的system_uid又没有设置唯一索引,所以在高并发下场景下我们发现有多条记录保存的是一模一样的值。

遇到这种问题,在插入前使用分布式锁也是可以的,不过能不能利用Mysql的特性去实现“已存在的记录不再插入”的目的呢?答案是:可以的。由于system_uid未使用唯一索引,所以无法利用:INSERT ON DUPLICATE KEY UPDATE 或者 INSERT IGNORE,但是利用INSERT INTO SELECT句式可以达到:如果system_uid已存在,则不插入记录的目的。

从SQL语句中可以看出,这是直接SELECT的值(而不是某表的字段)作为INSERT的入参,而且SELECT内容在WHERE判断记录已存在的情况下将会为空,即插入操作由于未发现值而被跳过。

INSERT INTO `wx_bb_uid_map` ( wxuin, nick_name, remark_name, system_uid, gmt_create, gmt_modified ) 
SELECT
    123,
    "wx小号",
    "姓名",
    1024,
    1531816594,
    1531816594 
FROM
    `wx_bb_uid_map` 
WHERE
    NOT EXISTS ( SELECT id FROM `wx_bb_uid_map` WHERE system_uid = 1024) 
    LIMIT 1

客服回复消息频率控制

由于我方的微信账号(个销号)是:给每个账号分配若干个人工客服进行消息收发,为防止由于消息发送频率过高导致的被风控,我们对人工客服对单个微信账号的回复频率做了频率限制。此处的频率限制使用了Redis做频率计数器,并以秒为单位过期计数器,达到控制发送速率的目的。