ShowMeBug 是一款远程面试工具,双方可经过在线面试板进行实时沟通技术。因此关键技术要点在于 “实时同步”。关于实时同步,ShowMeBug 采用了如下技术。前端
本质上,ShowMeBug 核心就是多人同时在线实时编辑,难点即在这里。由于网络缘由,操做多是异步到达,丢失,与他人操做冲突。想一想这就是个复杂的问题。 git
通过研究,最好用户体验的方式是 OT 转换算法。此算法由 1989 年 C. Ellis 和 S. Gibbs 首次提出,目前像 quip,google docs 均用的此法。 github
OT 算法容许用户自由编辑任意行,包括冲突的操做也能够很好支持,不用锁定。它的核心算法以下: web
文档的操做统一为如下三种类型的操做( Operation ):面试
retain(n)
: 保持 n 个字符insert(s)
: 插入字符串 sdelete(s)
: 删除字符串 s而后客户端与服务端各记录历史版本,每次操做都通过必定的转换后,推送给另外一端。 算法
转换的核心是 编程
S(o_1, o_2) = S(o_2, o_1) 安全
换言之,把正在并发的操做进行转换合并,造成新的操做,而后应用在历史版本上,就能够实现无锁化同步编辑。 服务器
下图演示了对应的操做转换过程。 websocket
https://daotestimg.dao42.com/ipic/070918.jpg
这个算法的难点在于分布式的实现。客户端服务端均须要记录历史,而且保持必定的序列。还要进行转换算法处理。
本质上,这是一个基于 websocket 的算法应用。因此咱们没有怀疑就选用 ActionCable 做为它的基础。想着应该能够为咱们节省大量的时间。实际上,咱们错了。
ActionCable 实际上与 NodeJS 版本的 socket.io 同样,不具有任何可靠性的保障,作一些玩意性的聊天工具还能够,或者作消息通知容许丢失甚至重复推送的弱场景是能够的。但像 OT 算法这种强要求的就不可行了。
由于网络传输的不可靠性,咱们必须按次序处理每个操做。因此首先,咱们实现了一个互斥锁,也就是针对某一个面试板,准备一个锁,同时只有一个操做能够进行操做。锁采用了 Redis 锁。实现以下:
def unlock_pad_history(lock_key)
logger.debug "\[padable\] unlock( lock\_key: #{lock\_key} )..." old\_lock\_key = REDIS.get(\_pad\_lock\_history\_key) if old\_lock\_key == lock\_key REDIS.del(\_pad\_lock\_history\_key) else log = "\[FIXME\] unlock\_pad\_history expired: lock\_key=#{lock\_key}, old\_lock\_key=#{old\_lock\_key}" logger.error(log) e = RuntimeError.new(log) ExceptionNotifier.notify\_exception(e, lock\_key: lock\_key, old\_lock\_key: old\_lock\_key) end
end
# 为防止死锁,锁的时间为5分钟,超时自动解锁,但在 unlock 时会发出异常
def lock_pad_history(lock_key)
return REDIS.set(\_pad\_lock\_history\_key, lock\_key, nx: true, ex: 5\*60)
end
def wait_and_lock_pad_history(lock_key, retry_times = 200)
total\_retry\_times = retry\_times while !lock\_pad\_history(lock\_key) sleep(0.05) logger.debug '\[padable\] locked, waiting 50ms...' retry\_times-=1 raise "wait\_and\_lock\_pad\_history(in #{total\_retry\_times\*0.1}s) #{lock\_key} failed" if retry\_times == 0 end logger.debug "\[padable\] locking it(lock\_key: #{lock\_key})..."
end
服务端的并发控制完毕后,客户端经过 “状态队列” 技术一个个排队发布操做记录,核心以下:
class PadChannelSynchronized {
sendHistory(channel, history){
channel.\_sendHistory(history) return new PadChannelAwaitingConfirm(history)
}
}
class PadChannelAwaitingConfirm {
constructor(outstanding_history) {
this.outstanding\_history = outstanding\_history
}
sendHistory(channel, history){
return new PadChannelAwaitingWithHistory(this.outstanding\_history, history)
}
receiveHistory(channel, history){
return new PadChannelAwaitingConfirm(pair\_history\[0\])
}
confirmHistory(channel, history) {
if(this.outstanding\_history.client\_id !== history.client\_id){ throw new Error('confirmHistory error: client\_id not equal') } return padChannelSynchronized
}
}
class PadChannelAwaitingWithHistory {
sendHistory(channel, history){
let newHistory = composeHistory(this.buffer\_history, history) return new PadChannelAwaitingWithHistory(this.outstanding\_history, newHistory)
}
}
let padChannelSynchronized = new PadChannelSynchronized()
export default padChannelSynchronized
以上,便实现了一个排队发送的场景。
除此以外,咱们设计了一个 PadChannel 用来专门管理与服务器通讯的事件,维护历史的状态,处理断线重传,操做转换与校验。
解决了编辑器协同的问题,才是真正的问题的开始。每次的 ”代码运行”,“编辑”,“清空终端”,“首次同步” 都是须要记录的历史操做。因而,ShowMeBug 定义了如下协议:
# 包含如下: edit( 更新编辑器内容 ), run( 执行命令 ), clear( 清空终端 ), sync( 同步数据 )
# select( 光标 ), locate( 定位 )
# history 格式以下:
#
# {
# op: 'run' | 'edit' | 'select' | 'locate' | 'clear'
# id: id // 全局惟一操做自增id, 首次前端传入时为 null, 服务端进行填充, 若是返回时为空, 则说明此 history 被拒绝写入
# version: 'v1' // 数据格式版本
# prev_id: prev_id // JS端生成 history 时上一次收到服务端的 id, 用于识别操做序列
# client_id: client_id // 客户端生成的 history 的惟一标识
# creator_id: creator_id // 操做人的用户id, 为了安全首次前端传入时为 null,由中台填充
# event: { // op 为 edit 时, 记录编辑器 OT 转化后的数据, see here: https://github.com/Aaaaash/bl...
# [length, "string", length]
# // op 为 select 时, 记录编辑器选择区域(包括光标)
# }
# snapshot: {
# editor_text: '' // 记录当前编辑器内容快照, 此快照由服务端填充
# language_type: '' // 记录当前编辑器的语言种类
# terminal_text: '' // 记录当前终端快照
# }
# }
# created_at: created_at // 生成时间
值得说明的是,client_id
是客户端生成的一个8位随机码,用于去重和与客户端进行 ACK 确认。
id
是由服务端 Redis 生成的自增 id,客户端会根据这个判断历史是不是新的。prev_id
用来操做转换时记录所须要进行转换操做的历史队列。
event
是最重要的操做记录,咱们用 OT 的转换数据进行存储,如: [length, "string", length]
经过上述的设计,咱们将面试板的全部操做细节涵盖了,从而实现多人面试实时同步,面试题和面试语言自动同步,操做回放等核心功能。
篇幅限制,这里只讲到 ShowMeBug 的核心技术,更多的细节咱们之后继续分享。
ShowMeBug 目前承载了 3000 场面试记录,成功支撑大量的实际面试官的面试,可靠性已获得进一步保障。这里面有两种重要编程范式值得考虑:
ShowMeBug( showmebug.com ) 让你的技术面试更高效,助你找到你想要的候选人。