小结UID替换器的实现

在推荐系统中,我们常需要根据用户的标示(UID)和访问行为给用户生成推荐。但某些情况下,我们需要替换用户请求中的UID为特殊的UID,以测试推荐结果。本周基于这个需求,实现了UID替换器。

功能

首先我们需要做一个管理后台,对我们要替换的UID或者其他ID进行管理,有如下需求:

1 增加,修改,删除UID替换规则
2 UID规则启用/停用
3 UID替换规则在线上生效

以上第三个需求相对难以实现一些。UID替换规则在线上生效,其潜台词是每次修改了替换规则之后,需要其立即在线上生效。对于UID替换规则对象,我们可以用关系数据库,如MySQL存储。

一般的想法是在线上请求过来的时候,依据请求中的UID查询数据库,看是否有相应的规则,如果有则取出替换UID进行替换,如果没有,则略过。这个想法在请求量不大的时候没有问题,但如果是每秒成千上万次请求,MySQL数据库难以抗住,就会出现问题。

这时候我们想到的是把规则数据加载到内存中,线上请求直接从内存中查找,就不需要每个请求都要查找数据库。在管理系统中,如果对规则修改了之后,“UID替换规则在线上生效”就是把修改后的数据重新加载到内存。

这就是“UID替换规则在线上生效”这个需求的真实含义。

其次,我们的线上系统要提供两方面的功能:

1 提供加载MySQL数据到线上内存的接口,方便管理系统调用
2 在请求过来的时候,根据请求中的UID和内存中的规则,实现UID替换

第一个功能,由线上系统暴露一个API,管理系统需要使线上生效时,就调用该API,重新由MySQL对应的规则表加载数据到内存对象中。线上API可以直接访问MySQL数据,也可以由管理系统暴露一个服务,然后线上API调用该服务获取生效规则数据。

第二个功能的实现要求从MySQL中加载出来的数据保存在一个全局的对象中。当然,也可以直接存储在Tair/Redis/Memcache这样的内存数据库中,甚至可以做成一个服务,供其他服务调用。不过此处为了简略,我们实现的版本就是直接放在一个全局的Java的HashMap对象中。然后对线上的Servlet做一个Filter,在Filter中使用全局对象提供的规则数据实现替换逻辑。

在本处我们使用了HashMap作为规则存储对象,不用担心线程安全的问题。HashMap并发读没有问题,并发写会出现问题。但在我们的场景中,写数据场景有二:线上服务启动时,要加载数据到HashMap;我们刷新时,要重新加载数据到HashMap。这两个场景都是单线程去写,所以HashMap堪堪够用,如果实在不放心,可以改用concurrentHashMap。

设计

设计主要有两个方面,一个是数据库表的设计,另一个是HashMap的设计。前者可以这么做:

---- table idmapper
id int 规则ID 自增
name varchar 规则名
source_id varchar 源ID
direct_id varchar 目的ID
enable int 是否启用 0:不启用,1:启用

除了常见的对字段的增删改查功能之外,还需要向外暴露已经启用的规则,查询语句如下:

select * from idmapper where enable=1

可在Web框架中,如Spring MVC,把这个查询的结果以API的方式暴露,访问API可以获得对应的Json数据。

第二个是HashMap的设计,因为在文章里面描述的场景相对简单,而实际上我们有更复杂的需求,如不止替换UID这样的参数,还可以替换别的参数,还有一些场景过滤,如只在特定场景下并且有对应规则才实现替换。这些功能使得HashMap的设计相对复杂,此处只需要将sourceID为key,directID为value。

第三个是要设计一个单例。单例中包含一个静态的HashMap对象,以及加载数据到HashMap对象的函数和根据UID查找替换UID的函数。实际上我们的系统相对复杂很多,提供有专门的数据服务接口,需要按照该接口进行开发。此处为了方便,我们简单就设计一个单例就好。伪代码大概如下:


public class MapRuler {

    private static HashMap ruler = new HashMap();
    // 私有构造函数
    private MapRuler() {}
    
    // 此处使用饿汉式即可
    private static MapRuler mapRuler = new MapRuler();

    public void loadEnableRuler() {
            ruler = ... // 调用管理系统提供的API,将Json数据解析成HashMap
    }

    public String getDirectId(String sourceId) {

            if(ruler.containsKey(sourceId)) {
                    return ruler.get(sourceId);
            }
            // 没有匹配的规则返回null
            return null;
   }
 
 public static MapRuler getInstance() {
            return mapRuler;
    }

}

单例使用饿汉式就可以,因为规则数据在线上应用启动时就要加载到内存对象中,这时候就要存在MapRuler对象。这个场景并不需要延迟加载这种功能。

实现

在实现过程中,我遇到不少问题,踩了不少坑。一方面是我很久没写前端了,目前的管理系统是之前留下来的,数据都用ajax请求后端API,然后前端写js把数据组装呈现。整个前端代码中JS/CSS/HTML混杂在一起,让人感觉很不好。如果要让我开发这个管理系统,我肯定不会这么做。

  1. css/js/html要尽可能做到分离开来,css放在专用的文件中,js的函数尽可能做到复用,不能每个页面都写一套js函数(这个项目几乎是的)。
  1. 现有的一些前端框架如vue/anglar/react都能帮助省很多功夫。这个系统中只使用了angular的路由功能,其他是jQuery和原生js拼接。
  2. 不会考虑只用cookie标记用户身份,居然还是明文的用户名,我也是醉了。其实内部伪造一下这个cookie,管理系统的权限如同虚设。像这种后端只负责提供API,并由前端呈现的应用,有专门的权限方案,例如授权机制,或者token令牌等。
  3. 代码里面不使用eval函数执行js函数。eval本身就不够安全,很多语言中都不怎么建议使用这个函数,js也是一样。

这种前后端分离的做法优点很多,其一后端减轻了重担,只需要提供API接口。而前端相应的任务就重了些,不过功能实现会更加灵活。我们的后端是用Spring MVC写的,实现对规则的增删改查的API也很简单。但是在前端实现上踩了不少坑。

例如用jQuery取id的数据时,如果前面忘了加#,然后就取不到数据。另外,原有的管理系统的代码有一个比较坑爹的BUG。在实现修改功能的时候,onclick对应的方法传进了很多参数,那些参数是通过js用字符串拼接的,然后用eval执行。这造成某个参数是json字符串,或者里面含有单引号的时候,该修改按钮点击无效。我尝试了好几个办法,都没有效果。但后来想到其实可以把带有单引号的字符串先用Base64加密,去掉单引号,然后作为参数传入onclick对应的方法,在onclick对应的方法里面用Base64解密,这样问题才解决。其实一个比较好的办法是,在onclick对应的函数参数中,只传入id,函数实现中用ajax从后端获取数据。

有一个比较好的工具可能帮助省了很多前端问题。chrome的开发者工具,真是神器,用好了这个东西,前端问题基本搞定了一半。不会的,不确定的代码都可以拿到控制台运行一下,看看结果,再写到生产环境的代码中。

chrome控制台

另一方面是我对线上API还不够熟悉,也跟不熟悉aone发布系统有关,毕竟这个东西也是刚用起来。推荐API中会对请求用一个Filter进行包装,包装成我们所需要的用于推荐的Request。我最开始走了弯路,把UID替换放在了该Filter之前,那时候我们只能通过getParameter()取得UID参数,但修改了后是没法用setParameter()函数改回去的,因为没有这个函数。正确的做法是把替换的Filter放在包装Filter之后,根据我们自己定义的Request对象替换掉里面的UID。还值得一说的是,替换Filter的配置要在包装Filter之后,这样获得的Request对象才正常而不是一个null,毕竟只有先把原request转换成推荐Request,我们才能用上推荐Request是不是?

总结

说起来这并不是一个很难实现的东西,但借助这次的实现,重新写了一把js,学了点Spring MVC,以及了解了线上API的东西。当我在预发机器上测试并打印出替换成功的结果时,感觉还是很开心的。但是我也知道,要学的东西还有很多。接下来可能找时间评估一下把后台管理系统重写的任务量,前端部分考虑用vue或者其他框架,优化一些使用功能。

虽然目标是一个数据工程师,但怎么也得会Web应用开发吧。So,走起来。

    雄关漫道真如铁,而今漫步从头越。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,710评论 4 376
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,839评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,295评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,776评论 0 223
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,198评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,074评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,200评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,986评论 0 214
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,733评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,877评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,348评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,675评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,393评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,209评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,212评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,003评论 2 280

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,598评论 25 707
  • (六) 五年前。 那天晚上是上元节,整个长安城举城欢庆,江寒浸也从冷林阁出来。 东风夜放花千树,更吹...
    酥小盐阅读 333评论 0 0
  • 今天练习 木有哭... 我说的最过分的词 是两次滚 在她第一百次小跑上场的时候 朱先生窝在沙发刷手机 听见我吼他闺...
    Tt_80dc阅读 201评论 0 0
  • 双十一过后的第二天,王先森发来微信问:你零花钱还剩多少? 我回答:今天11月12号。 王先森:怎么了? 我回答:马...
    苏素的异想时空阅读 739评论 0 0
  • 经常在电视剧里面看到犯罪嫌疑人指认的环节,以及目击者询问的环节,一般目击者都会详细的,知无不言的还原作为目击者看到...
    十方迦南阅读 856评论 0 1