导读:前段时间有个需求是提供一个接口供客户端增量更新数据,当有数据被删除了以后客户端也需要感知到,并且要支持一定并发;

关键词:高并发,增量更新

前言

何谓增量更新,顾名思义就是只更新变化的部分,这样即经济(尤其对流量敏感型用户)又高效,比如微信朋友圈,微博的消息,头条推荐等等。要实现增量更新,首先要解决三个问题,1.如何识别数据的变化,2.如何识别增量更新的起始位置,3.如何感知数据被删除。

初步分析

首先说说如何识别数据的变化,简单来说就是每条记录都需要有一个版本信息,可能是时间戳或者是一个自增的数字,为了简答起见我选用的是时间戳,但是时间戳在并发极其高的时候可能会重复,从而导致增量更新永远结束不了,这个需要特别注意,总体来说,整个数据集需要有一个全局递增的版本号,有了这个前提才能满足第二点“识别增量更新的起始位置”,这样很容易就能想到只要每次查询的时候让前端把最后一条记录的版本信息带上来作为查询条件就可以满足需求了,至于第三点,其实就是对数据做逻辑删除。画个简答的时序图加深理解。

直抒胸臆

我分析完之后首先想到的就是使用redis的SortedSet来实现,用member存储uid,用score来存储uid的最后更新时间,借助ZRANGEBYSCORE实现增量更新。当用户信息添加或者修改的时候使用ZADD来修改uid的最后更新时间。删除的时候就稍微麻烦一些,不仅要修改最后更新时间,还需要将删除的uid保存起来,使用一个SortedSet实现不了,需要将删除的uid暂存到另一个SortedSet(set更简单,为什么不用?)中,这样在查询数据的时候判断下uid是否已被删除然后打标即可让前端感知到数据被删除。

剥茧抽丝

前面说过我会使用ZADD来修改uid对应的score,也就是最后更新时间,那这个score由谁产生呢?第一版使用的是服务器的当前时间(System.currentTimeMillis),似乎一切都很完美,但是跑了几天以后客户端反馈说有个用户的信息已经删除了,但是客户端却没有感知到,查了服务端的操作日志并没有发现什么问题,uid的score是最新的,那为什么增量接口没有感知到呢?后来在分析客户端请求日志的时候发现了一个细节,客户端携带的version(本质就是个时间戳)居然比有问题uid的score要大,那就好解释为什么查询不到了,我是将version作为ZRANGEBYSCORE的min参数来实现增量更新的,如果version已经是最新的了,就不会返回数据,那version怎么就超前了呢?前面说过我是使用服务器的当前时间作为score,有没有可能程序里取到的时间忽早忽晚呢,说到这儿可能有人不信,但这就是事实,在集群环境下时钟不是强同步的,通过一个表格来还原现场。

原因找到了,怎么解决呢?有人会说,搞单台服务器,对不起,我们毕竟是一个追求高可用的系统。用消息队列,单线程消费呢?这个倒是可行,但说实话复杂了,能不能让redis内部消化呢,我们都知道,redis执行指令内部也是排队的,如果这个score让redis生成就可以解决问题,带着问题我查看了redis的api,发现TIME命令可以满足需求,TIME命令返回两个字符串,第一个代表当前的秒,第二个代表当前这一秒已经过去的微秒,做一个简单的计算就就可以得到当前时间对应的微秒,最终的jua脚本如下:

local times=redis.call('TIME') ;
local score = times[1]*1000000+times[2] //通过计算得到当前时间的微秒
redis.call('zadd',KEYS[2],score,ARGV[1])  
接着再来聊下删除操作的细节,删除时有两步操作要完成,为了确保这两步操作的原子性,还是要借助lua来实现,最终lua脚本如下:
local times=redis.call('TIME') ;
local score = times[1]*1000000+times[2] ;
redis.call('zadd',KEYS[1],score,ARGV[1]) ;//修改uid最后更新时间
redis.call('zadd',KEYS[2],score,ARGV[1]) ;//插入删除set

查询时如何给用户打标,首先通过ZRANGEBYSCORE查询出一批uid,然后遍历uid判断是否已删除,如果已删除给uid打标,考虑到性能,这里还是使用lua来实现,最终lua脚本如下:

local signlist = redis.call('zrangebyscore',KEYS[1],ARGV[1],ARGV[2],'WITHSCORES','LIMIT',ARGV[3],ARGV[4])
local signTable = {}
//使用lua脚本判断uid是否删除可以避免多次网络请求(加入将signList返回给服务端,服务端再判断)
for i = 1, #signlist, 2 do
local h = {} h['uid'] = signlist[i] h['t']= signlist[i + 1] h['status']=1
if(redis.call('ZRANK',KEYS[2],signlist[i])) then
h['status'] =0 //使用ZRANK判断uid是否在删除列表中,如果已删除标志位删除状态
end
table.insert(signTable,h)
end local result = #signTable>0 and cjson.encode(signTable) or '[]'
return result

  

最后的一点优化

截止到这,整个方案可以说介绍完了,不知道细心的你有没有发现一个问题,那些逻辑删除的数据终将会成为一颗炸弹,想象一下redis被撑爆的那天,送给自己两句诗,“待到山花烂漫时,她在丛中笑”。为了避免悲剧,还是早日将优化提到日程上来吧(不知道从哪听到的一句话,程序员都抱有侥幸心理),怎么优化呢?归根节点就是要将这部分数据给清除了,但是物理删除客户端就感知不到了,似乎陷入了矛与盾的世界中。搞什么增量更新,真是麻烦啊,要不每次拉全量。每次全量绝对不行,倒是可以偶尔拉一次全量,这个偶尔最终被赋予的含义是“如果客户端两天没在线就拉全量数据,否则拉增量数据”,鉴于此客户端还需要一个最后请求时间(有人会问为什么不用最后一条记录的更新时间呢?毕竟不是一直有数据被更新啊),这样服务端就可以将两天之前逻辑删除的数据做物理删除了,前面有个疑问是“存储删除用户的uid用set不是更简单吗,为什么用SortedSet”,正好在这解答下,因为我可以很方便的找出两天之前被删除的那些uid。

总结

看似一个简单的需求,当你用心去完成的时候一定会带给你意想不到的收获,如果觉得不错,请点个推荐。


使用Redis SortedSet实现增量更新的更多相关文章

  1. [置顶]使用scrapy_redis,自动实时增量更新东方头条网全站新闻

    存储使用mysql,增量更新东方头条全站新闻的标题 新闻简介 发布时间 新闻的每一页的内容 以及新闻内的所有图片.项目文件结构. 这是run.py的内容 1 #coding=utf-8 2 from ...

  2. Elasticsearch 索引的全量/增量更新

    Elasticsearch 索引的全量/增量更新 当你的es 索引数据从mysql 全量导入之后,如何根据其他客户端改变索引数据源带来的变动来更新 es 索引数据呢. 首先用 Python 全量生成 ...

  3. 前端遇上Go: 静态资源增量更新的新实践

    前端遇上Go: 静态资源增量更新的新实践https://mp.weixin.qq.com/s/hCqQW1F8FngPPGZAisAWUg 前端遇上Go: 静态资源增量更新的新实践 原创: 洋河 美团 ...

  4. 谈谈混合 App Web 资源的打包与增量更新

    综述 移动 App 的运行环境具有带宽不稳定,流量收费,启动速度比较重要等特点,所以混合 App 如何加载 Web 资源并不是一个新问题.本文目的是总结出一种资源打包下载的思路和方案,并且提供一种打包 ...

  5. SSIS Design2:增量更新

    一般来说,ETL实现增量更新的方式有两种,第一种:记录字段的最大值,如果数据源中存在持续增加的数据列,记录上次处理的数据集中,该列的最大值:第二种是,保存HashValue,快速检查所有数据,发现异动 ...

  6. android studio增量更新

    一.概述 1.1 概念 增量更新即是通过比较 本机安装版本 和 想要安装版本 间的差异,产生一个差异安装包,不需要从官网下载并安装全量安装包,更不需要将本机已安装的版本下载,而仅仅只是安装此差异安装包 ...

  7. Android 增量更新(BSDiff / bspatch)

    Android 增量更新 BSDiff / bspatchhttp://www.daemonology.net/bsdiff/android的代码目录下 \external\bsdiff bsdiff ...

  8. 【转载】Unity 合理安排增量更新(热更新)

    原帖地址:由于我看到的那个网站发的这篇帖子很大可能是盗贴的,我就暂时不贴地址了.避免伤害原作者 原版写的有点乱,我个人修改整理了下. --------------------------------- ...

  9. Unity5 如何做资源管理和增量更新

    工具 Unity 中的资源来源有三个途径:一个是Unity自动打包资源,一个是Resources,一个是AssetBundle. Unity自动打包资源是指在Unity场景中直接使用到的资源会随着场景 ...

随机推荐

  1. Day2-I-Knight's Problem POJ - 3985

    You must have heard of the Knight's Tour problem. In that problem, a knight is placed on an empty ch ...

  2. 「UVA10559」Blocks

    传送门 Luogu 解题思路 考虑区间 \(\text{DP}\). 设 \(f[i][j][k]\) 表示 \([i,j]\) 这段区间接上后面 \(k\) 个与 \(j\) 颜色相同的块得到的答案 ...

  3. ajax请求QQ音乐

    搜索歌曲 function go() {                var val = document.getElementById("name").value;       ...

  4. 学习angularJs(1)--引用文件

    <script src="http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js">< ...

  5. Linux关于文件处理命令

    一.登陆用户和机器名称 示例:[root@hadoop01 ~]# root:表示用户名 @hadoop01表示机器名称 ~表示当前文件目录是家目录 #表示输入命令提示符,用户可以在其后输入命令:非r ...

  6. 安装数据库Typical path for xclock: /usr/X11R6/bin/xclock 错误问题

    [oracle@localhost database]$ ./runInstaller Starting Oracle Universal Installer... Checking Temp spa ...

  7. 结题报告:luogu P2014

    题目链接:P2014 选课 简单的树形\(dp\),借助\(dfs\)实现. 一般的树形\(dp\)数组是需要二维的,其中一维记录节点(编号或父/子节点的状态(有时三维)),另一维记录权值或计数. 重 ...

  8. 【蓝桥】第八届C语言C组第7题 Excel地址(进制变形题,stack()简单使用)转载

    标题: Excel地址 Excel单元格的地址表示很有趣,它使用字母来表示列号. 比如, A表示第1列, B表示第2列, Z表示第26列, AA表示第27列, AB表示第28列, BA表示第53列, ...

  9. 微信access_token设计的原理解析

    1.access_token是加密的字符串,其目的是为了接口安全考虑,不然随便就能调用微信服务器的接口会有很大风险. 2.用户在公众号中填写的Token就相当于本项目中的xiaoming,是签名验证中 ...

  10. Mysql数据库的简单介绍与入门

    Mysql数据库的简单介绍与入门 前言 一.下载与安装 1.下载 官网下载MYSQL5.7.21版本,链接地址https://www.mysql.com/downloads/.下载流程图如下: 找到M ...