要解决的问题:
目前审核分为三种,一种是审核评价文字,会变更评价的整体状态;一种是审核评价带图,会变更评价图片的状态;还有一种是审核回应,会变更回应的状态。现在存在一种情况,当审核方同时或者在很短的时间间隔内提交一个评价的多张图片状态更新请求,会出现某张图片的更新操作没有生效。
出现此问题的根本原因是因为,每次更新图片状态时,都会先读整个ReviewData,更新该图片在ReviewData中的状态,然后再根据ReviewData更新Tair & 索引表 & Base表。下面是一次出现错误更新的示例:
解决方案:
NOTE: 由于此方案以前未经线上验证,所以目前只实现图片审核原子化,但是为评价审核原子化预留了处理字段,图片审核原子化验证没问题后,年后会上线评价审核原子化。
分布式锁+延时队列+时间戳:
分布式锁:
什么是分布式锁:如果不同系统或者同一系统中的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这时便需要使用分布式锁。
本方案的实现:使用Tair实现了简单的分布式锁,使用了SetNx接口,如果Tair中不存在当前key,则setNx成功,如果Tair中存在,则setNx失败。设置超时时间为1分钟。
延时队列:
本方案的实现:使用了Mafka的消息粒度延时的功能,可以为每个消息设置不同的延时时间。
实现思路:
基础版
以Review为维度在Tair中建锁,当审核回调更新图片状态时,执行以下步骤:
- 先获取picId对应的reviewId, 构建该reviewId对应的Key;
- 获取该图片所在Review的锁(setNx),如果获取到(setNx成功),则执行更新操作;
- 如果获取不到,则按照图片在评价带图中的位置确定不同的延时时间,发送到mafka延时队列,以消费消息的方式来执行更新操作。为了防止读写Tair失败,将锁的失效时间置为1分钟。
下面为执行流程图:
改进版
如果按上述思路来实现,会引入另外一种更新出错,例如对一张原始状态是1的图片进行2次审核回调更新操作,第一次图片新状态是2,第二次图片新状态是1,即整个更新是(1→ 2 → 1)。第一次更新操作未获取到锁,扔到延时队列,第二次获取到锁后执行了更新操作(图片状态 1 → 1),待第一次更新操作被消费时,图片状态更新操作(图片状态 1→ 2)。
为了解决此问题,需要增加时间戳判断,每次审核回调请求来的时候,都记录下当前系统时间(auditTime),存在ReveiwAuditMsg中。并且在执行一次更新操作后,以当前图片ID为key,将当前操作的auditTime记录在Tair中,表示上次更新时间LastTime(失效时间1分钟)。在执行更新操作之前,比较当前ReviewAuditMsg的auditTime和LastTime,如果当前auditTime < LastTime, 则说明此次更新为无效更新,只记log。
下面是审核回调更新操作的完整流程图:
excute: 执行了更新操作, send_mafka和send_mafka_success: 加锁失败,存入mafka, consume_mafka: 消费mafka消息, expire_msg: 当前消息无效
NOTE : 我们使用的Mafka机器的系统时间比ugc-review-service所在机器的系统时间快了3秒,所以会有计数和Mafka队列消息记录对不上的情况。
前后数据对比:
更新效果对比
以2016年1月21日为例,当天共有16,293次图片审核回调更新请求;有3280次请求获取锁失败,会存入队列后被消费;有918次请求存入队列后变为过期更新。
下面为方案上线前出现的错误更新:
ID 变更类型 变前 变后 时间 解释
213391736 | 信审 | 2016-12-15 17:32:41 | |||
213391748 | 信审 | picId: 2391693565 status: 1 picId: 2391969716 status: 1 picId: 2391113970 status: 1 | picId: 2391693565 status: 5 picId: 2391969716 status: 1 picId: 2391113970 status: 1 | 2016-12-15 17:32:42 | 将照片2391693565置为审核删除状态 |
213391749 | 信审 | picId: 2391693565 status: 1 picId: 2391969716 status: 1 picId: 2391113970 status: 1 | picId: 2391693565 status: 1 picId: 2391969716 status: 5 picId: 2391113970 status: 1 | 2016-12-15 17:32:42 | 将照片2391969716置为审核删除状态, 但覆盖了picId: 2391693565的状态 |
此例中,几乎同时更新了picId=2391693565 和picId=2391969716的状态,造成了picId=2391693565的审核操作失效(状态是1→ 5 → 1)。
下面为方案上线后的更新记录示例:
247191427 | 信审 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | 2017-01-22 07:52:17 | 加锁成功,直接更新 |
247191428 | 信审 | 1 | 2 | 2017-01-22 07:52:17 | |
247191429 | 信审 | 2 | 1 | 2017-01-22 07:52:18 | |
247191462 | 信审 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | 2017-01-22 07:52:22 | 加锁失败,消费消息更新 |
247191473 | 信审 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | picId: 2396861271 status: 2 picId: 2396846539 status: 2 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | 2017-01-22 07:52:23 | 加锁失败,消费消息更新 |
247191476 | 信审 | picId: 2396861271 status: 2 picId: 2396846539 status: 2 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | picId: 2396861271 status: 1 picId: 2396846539 status: 2 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | 2017-01-22 07:52:23 | 加锁成功,直接更新 |
247191479 | 信审 | picId: 2396861271 status: 1 picId: 2396846539 status: 2 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | picId: 2396861271 status: 1 picId: 2396846539 status: 2 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | 2017-01-22 07:52:24 | 加锁成功,直接更新 |
247191484 | 信审 | picId: 2396861271 status: 1 picId: 2396846539 status: 2 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | 2017-01-22 07:52:24 | 加锁成功,直接更新 |
247191487 | 信审 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | 2017-01-22 07:52:24 | 加锁成功,直接更新 更新picId=2396237778 状态 1→ 2 → 1, ID=247191505的操作失效 |
247191505 | 信审 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 2 | 2017-01-22 07:52:26 过期审核信息 | 加锁失败,过期审核信息 |
247193207 | 信审 | 1 | 2 | 2017-01-22 07:56:42 | |
247193209 | 信审 | 2 | 1 | 2017-01-22 07:56:42 | |
用户编辑 | |||||
247193206 | 信审 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | 2017-01-22 07:56:42 | 加锁成功,直接更新 |
247193240 | 信审 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | 2017-01-22 07:56:46
| 加锁失败,消费消息更新 |
247193261 | 信审 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | 2017-01-22 07:56:48 | 加锁成功,直接更新 更新picId=2396846539的状态 1→ 2 → 1, 操作247193271失效 |
247193263 | 信审 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 2 picId: 2396237778 status: 1 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | 2017-01-22 07:56:49 | 加锁成功,直接更新 |
247193268 | 信审 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | 2017-01-22 07:56:49 | 加锁成功,直接更新 更新picId=2396237778的状态1→ 2 → 1, 操作247193274失效 |
247193271 | 信审 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | picId: 2396861271 status: 2 picId: 2396846539 status: 2 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | 2017-01-22 07:56:49 过期审核信息 | 加锁失败,过期审核信息 |
247193274 | 信审 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 2 | 2017-01-22 07:56:49 过期审核信息 | 加锁失败,过期审核信息 |
247193299 | 信审 | picId: 2396861271 status: 2 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | picId: 2396861271 status: 1 picId: 2396846539 status: 1 picId: 2396443150 status: 1 picId: 2396237778 status: 1 | 2017-01-22 07:56:53 | 加锁失败,消费消息更新 |
可以看到, 日志中有很多加锁失败,消费消息更新的记录,使用延时队列岔开了图片更新时间,使得每次只有一个操作在更新ReviewData,例如ID=247191462,ID=247191473,ID=247191427表示在很短的时间内更新了picId=2396861271, 2396846539, 2396443150的状态,其中ID=247191427加锁成功,直接在2017-01-22 07:52:17完成了更新操作,而ID=2396846539和ID=2396443150加锁失败,放入延时队列后,分别在2017-01-22 07:52:22 和2017-01-22 07:52:23完成更新操作。
性能对比
使用本方案前,ReviewPicService:updateReviewPicStatusByAdmin接口99线约16ms, 使用本方案后99线约为25ms,由于处理逻辑变复杂,增加了与tair的交互操作,99线上升10ms是在预期之内。