以后接定制化软件项目需要注意的事项

上周去了朋友介绍的想做内部管理软件的公司,第四次去。

到了后,打开电脑,直接演示功能

打开浏览器,输入部署在服务器上的一个网址,开始讲核心功能。

然后就提出一些细节问题,

比如什么表单上的控件应该是用checkbox, 而不是文本框;这几个数据为什么没有体现出来 ;以后系统开放给他的客户后客户应该怎样怎样……

一时间就有点懵逼,这是什么事?和上次开会讲的要求完全不一样,本来说好只演示核心功能,就是一个数据转换的功能,开始玩其他花式,内部系统你扯什么开放给客户用?

再说功能演示的网址也在之前就发出去,然后也没看……

强调再三演示用的界面只是一个功能示意,并非最后成品界面,还在纠结哪哪应该是什么样的东西,控件应该怎么摆,听不懂话也是醉了。也许只是对方找的一个借口!

于是对方还是挺客气地表示和他想要的东西不一样,先暂停

这件事前后也经历二个月,花了精力、时间最后没有达成还是稍许有遗憾

总结有以下注意事项

1:不能因为是朋友介绍就不按正规流程来操作,首先碰头开会后需要形成基本的会议记录,确认下一次推进的目的,内容。

2:管理客户预期很重要。注意对方人员的言语,如果很发散,天马行空,需要限定此次项目的开发内容,将多余内容放入到可能的二期再说,强调当前开发内容。此类人员特别需要用确定的文档界定出功能列表。

3:非确定不进入详细demo演示,如果需要原型、demo明确告知在签约、需求调查后进行,至少收到首笔款后,防止投入的人力、时间损失。朋友介绍的项目也按此处理。如果必要可以将对方认定核心难点作一个简单实现,以打消对方顾虑,需要强调只是为了表示有这个能力实现,而不是后完成后一模一样。

4:了解对方性格脾气很重要。对于那些对界面要求和他想象中一毛一样的人,此类项目需要提高报价,因为后期可能会有很多违反正常布局的要求,此类人控制欲比较强,外行强行要指导内行,只能让开发吃苦头,更高报价就是补偿开发的精神损失。他有建议权,但是要能听劝、讲道理,如果确实太强势,非到无法生存此类项目慎接!

5:正常这类小项目,基本一二次就能确认会不会做,没有必要三次、四次连续跟踪。如果有反复、不停外延拓展、增加需求也说明对方并没有想清楚自己要什么,开发可能也只是用来做头脑风暴的一个耗材,要清楚机会并不大,可以冷处理,不必花精力。明确后续推进需要以签约、收首笔款为条件。

6:以签约为终极目标,没签约前的吹牛、吃饭、展望未来纯属浪费。

7:总而言之你想着他手里的项目、要他的钱,他想着你为他干活、实现他要的功能。

不是 Windows,也不是 Linux,Shrine 才是“神之操作系统”

在生活中,我们都曾使用过多种操作系统。有些好,有些坏。但你能说你使用过由“神”设计的操作系统吗?今天,我想向你介绍 Shrine(圣殿)。

不是 Windows,也不是 Linux,Shrine 才是“神之操作系统”

什么是 Shrine?

不是 Windows,也不是 Linux,Shrine 才是“神之操作系统”
Shrine 界面

从介绍里,你可能想知道这到底是怎么回事。嗯,这一切都始于一个叫 Terry Davis 的人。在我们进一步介绍之前,我最好提醒你,Terry 在生前患有精神分裂症,而且经常不吃药。正因为如此,他在生活中说过或做过一些不被社会接受的事情。

总之,让我们回到故事的主线。在 21 世纪初,Terry 发布了一个简单的操作系统。多年来,它不停地换了几个名字,有 J Operating System、LoseThos 和 SparrowOS 等等。他最终确定了 TempleOS[1](神庙系统)这个名字。他选择这个名字是因为这个操作系统将成为“神的圣殿”。因此,“神”给 Terry 的操作系统规定了以下 规格[2]

  • 它将有 640×480 的 16 色图形显示
  • 它将使用 “单声道 8 位带符号的类似 MIDI 的声音采样”
  • 它将追随 Commodore 64,即“一个非网络化的简单机器,编程是目标,而不仅仅是达到目的的手段”
  • 它将只支持一个文件系统(名为 “Red Sea”)
  • 它将被限制在 10 万行代码内,以使它 “整体易于学习”
  • “只支持 Ring-0 级,一切都在内核模式下运行,包括用户应用程序”
  • 字体将被限制为 “一种 8×8 等宽字体”
  • “对一切都可以完全访问。所有的内存、I/O 端口、指令和类似的东西都绝无限制。所有的函数、变量和类成员都是可访问的”
  • 它将只支持一个平台,即 64 位 PC

Terry 用一种他称之为 HolyC(神圣 C 语言)的编程语言编写了这个操作系统。TechRepublic 称其为一种 “C++ 的修改版(‘比 C 多,比 C++ 少’)”。如果你有兴趣了解 HolyC,我推荐 这篇文章[3]RosettaCode[4] 上的 HolyC 条目。

2013 年,Terry 在他的网站上宣布,TempleOS 已经完成。不幸的是,几年后的 2018 年 8 月,Terry 被火车撞死了。当时他无家可归。多年来,许多人通过他在该操作系统上的工作关注着他。大多数人对他在如此小的体积中编写操作系统的能力印象深刻。

现在,你可能想知道这些关于 TempleOS 的讨论与 Shrine 有什么关系。好吧,正如 Shrine 的 GitHub 页面[5] 所说,它是 “一个为异教徒设计的 TempleOS 发行版”。GitHub 用户 minexew[6] 创建了 Shrine,为 TempleOS 添加 Terry 忽略的功能。这些功能包括:

  • 与 TempleOS 程序 99% 的兼容性
  • 带有 Lambda Shell,感觉有点像经典的 Unix 命令解释器
  • TCP/IP 协议栈和开机即可上网
  • 包括一个软件包下载器

minexew 正计划在未来增加更多的功能,但还没有宣布具体会包括什么。他有计划为 Linux 制作一个完整的 TempleOS 环境。

体验

让 Shrine 在虚拟机中运行是相当容易的。你所需要做的就是安装你选择的虚拟化软件。(我的是 VirtualBox)当你为 Shrine 创建一个虚拟机时,确保它是 64 位的,并且至少有 512MB 的内存。

一旦你启动到 Shrine,会询问你是否要安装到你的(虚拟)硬盘上。一旦安装完成(你也可以选择不安装),你会看到一个该操作系统的导览,你可以由此探索。

总结

TempleOS (和 Shrine)显然不是为了取代 Windows 或 Linux。即使 Terry 把它称为 “神之圣殿”,我相信在他比较清醒的时候,他也会承认这更像是一个业余的作业系统。考虑到这一点,已完成的产品相当 令人印象深刻[7]。在 12 年的时间里,Terry 用他自己创造的语言创造了一个稍稍超过 10 万行代码的操作系统。他还编写了自己的编译器、图形库和几个游戏。所有这些都是在与他自己的个人心魔作斗争的时候进行的。

参考资料

[1]

TempleOS: https://templeos.org/

[2]

规格: https://web.archive.org/web/20170508181026/http://www.templeos.org:80/Wb/Doc/Charter.html

[3]

这篇文章: https://harrisontotty.github.io/p/a-lang-design-analysis-of-holyc

[4]

RosettaCode: https://rosettacode.org/wiki/Category:HolyC

[5]

GitHub 页面: https://github.com/minexew/Shrine

[6]

minexew: https://github.com/minexew

[7]

令人印象深刻: http://www.codersnotes.com/notes/a-constructive-look-at-templeos/

【分布式系统】唯一ID生成策略总结

文章目录
全局唯一id介绍
    全局唯一id特点:
常见全局唯一id生成策略
    1、数据库自增长序列或字段生成id
    2、UUID
    3、Redis生成ID
    4、zookeeper生成ID
    5、Twitter的snowflake算法
全局唯一id介绍
    系统唯一id是我们在设计阶段常常遇到的问题。在复杂的分布式系统中,几乎都需要对大量的数据和消息进行唯一标识。在设计初期,我们需要考虑日后数据量的级别,如果可能会对数据进行分库分表,那么就需要有一个全局唯一id来标识一条数据或记录。生成唯一id的策略有多种,但是每种策略都有它的适用场景、优点以及局限性。
    全局唯一id特点:
全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求;
趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能;
单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求;
信息安全:如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要ID无规则、不规则;
高可用性:同时除了对ID号码自身的要求,业务还对ID号生成系统的可用性要求极高,想象一下,如果ID生成系统瘫痪,这就会带来一场灾难。所以不能有单点故障;
分片支持:可以控制ShardingId。比如某一个用户的文章要放在同一个分片内,这样查询效率高,修改也容易;
长度适中。
常见全局唯一id生成策略
    1、数据库自增长序列或字段生成id
    最常见的一种生成id方式。利用数据库本身来进行设置,在全数据库内保持唯一。
    【优点】
非常简单。利用现有数据库系统的功能实现,成本小,代码简单,性能可以接受。
ID号单调递增。可以实现一些对ID有特殊要求的业务,比如对分页或者排序结果这类需求有帮助。
    【缺点】
    1. 强依赖DB。不同数据库语法和实现不同,数据库迁移的时候、多数据库版本支持的时候、或分表分库的时候需要处理,会比较麻烦。当DB异常时整个系统不可用,属于致命问题。
    2. 单点故障。在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。
    3. 数据一致性问题。配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。
    4. 难于扩展。在性能达不到要求的情况下,比较难于扩展。ID发号性能瓶颈限制在单台MySQL的读写性能。
    【部分优化方案】
    针对主库单点, 如果有多个Master库,则每个Master库设置的起始数字不一样,步长一样,可以是Master的个数。比如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。这样就可以有效生成集群中的唯一ID,也可以大大降低ID生成数据库操作的负载。
    2、UUID
    常见的生成id方式,利用程序生成。
    UUID (Universally Unique Identifier) 的目的,是让分布式系统中的所有元素,都能有唯一的辨识资讯,而不需要透过中央控制端来做辨识资讯的指定。如此一来,每个人都可以建立不与其它人冲突的 UUID。在这样的情况下,就不需考虑数据库建立时的名称重复问题。
    UUID的标准形式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:550e8400-e29b-41d4-a716-446655440000,到目前为止业界一共有5种方式生成UUID,详情见IETF发布的UUID规范 A Universally Unique IDentifier (UUID) URN Namespace。
   在Java中我们可以直接使用下面的API生成UUID:
UUID uuid  =  UUID.randomUUID(); String s = UUID.randomUUID().toString();
    【优点】
非常简单,本地生成,代码方便,API调用方便。
性能非高。生成的id性能非常好,没有网络消耗,基本不会有性能问题。
全球唯一。在数据库迁移、系统数据合并、或者数据库变更的情况下,可以 从容应对。
    【缺点】
存储成本高。UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。如果是海量数据库,就需要考虑存储量的问题。
信息不安全。基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
不适用作为主键,ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用。UUID往往是使用字符串存储,查询的效率比较低。
UUID是无序的。不是单调递增的,而现阶段主流的数据库主键索引都是选用的B+树索引,对于无序长度过长的主键插入效率比较低。
传输数据量大。
不可读。
    【部分优化方案】
    为了解决UUID不可读, 可以使用UUID to Int64的方法 。
    为了解决UUID无序的问题, NHibernate在其主键生成方式中提供了Comb算法(combined guid/timestamp)。保留GUID的10个字节,用另6个字节表示GUID生成的时间(DateTime)。
    3、Redis生成ID
    当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用Redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。
    可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:
A:1,6,11,16,21B:2,7,12,17,22C:3,8,13,18,23D:4,9,14,19,24E:5,10,15,20,25
    这个负载到哪台机器上需要提前设定好,未来很难做修改。但是3-5台服务器基本能够满足,都可以获得不同的ID。步长和初始值一定需要事先设定好。使用Redis集群也可以防止单点故障的问题。
    比较适合使用Redis来生成日切流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。
    【优点】
不依赖于数据库,灵活方便,且性能优于数据库。。
数字ID天然排序,对分页或者需要排序的结果很有帮助。
    【缺点】
如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。。
需要编码和配置的工作量比较大。
Redis单点故障,影响序列服务的可用性。
    4、zookeeper生成ID
    zookeeper主要通过其znode数据版本来生成序列号,可以生成32位和64位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。
    很少会使用zookeeper来生成唯一ID。主要是由于需要依赖zookeeper,并且是多步调用API,如果在竞争较大的情况下,需要考虑使用分布式锁。因此,性能在高并发的分布式环境下,也不甚理想。
    5、Twitter的snowflake算法
    snowflake(雪花算法)是Twitter开源的分布式ID生成算法,结果是一个long型的ID。这种方案把64-bit分别划分成多段,分开来标示机器、时间等。如图:
【分布式系统】唯一ID生成策略总结
    其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。具体实现的代码可以参看github。
    snowflake算法可以根据自身项目的需要进行一定的修改。比如估算未来的数据中心个数,每个数据中心的机器数以及统一毫秒可以能的并发数来调整在算法中所需要的bit数。
    【优点】
稳定性高,不依赖于数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
灵活方便,可以根据自身业务特性分配bit位。
单机上ID单调自增,毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
    【缺点】
强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
ID可能不是全局递增。在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,也许有时候也会出现不是全局递增的情况。

如喜欢本文,请点击右上角,把文章分享到朋友圈
如有想了解学习的技术点,请留言给若飞安排分享

·END·

作者:梦 * 蝶

来源:https://blog.csdn.net/LZ15932161597/article/details/113397226

微信的原创保护机制到底是如何实现的?

前言

众所周知,目前微信公众号是最具商业价值的写作平台,这与它优秀的原创保护机制密不可分,如果你想将其他公众号上的文章标为原创,微信会给出类似如下的信息告诉你未通过原创校验逻辑。

微信的原创保护机制到底是如何实现的?

如果你抓包会发现微信返回了如下错误

微信的原创保护机制到底是如何实现的?

如果你想改几个字蒙混过关,对不起,不行!依然会报上述错误,这得益于微信原创检测机制所采用的 simhash 技术,它是 Google 为了解决大规模的网页去重而发明的算法,广泛用在大规模的文章,评论判重等地方,效率极高,那么这项技术是如何实现的呢,通过上面的错误信息不难发现微信是为每篇文章生成了一个指纹(fingerprint),最终文章相似性的比较其实是指纹的比较,那么这个指纹又是如何生成的呢,本文将会为你由浅入深地揭晓 simhash 的秘密。

本文的目录结构如下:

  • 传统 Hash 与其局限性
  • 余弦定理实现及其局限性
  • 基于随机投影来实现空间向量的降维
  • simhash 原理及实现

传统 Hash 与其局限性

如何比较两篇文章是否相同,相信大家不难想到以下步骤

  1. 通过一个 Hash 函数(MD5 等)将文章转成定长字符串,比如 32 位
  2. 比较上一步生成的定长字符串是否相等

第一步的主要作用是将大范围映射到小范围,这样使用小范围的定长字符串「一般我们把它称为指纹(fingerprint)」大大缩小了空间,更利于保存,并且更利于比较,但对于计算两篇文章的相似度传统 hash 就无能为力了,因为对于传统 hash 来说,它要求随机性足够好,也就是说对于两个输入字符串,哪怕只有一个字母不同,使用传统 hash 的输出结果也是大不相同。

微信的原创保护机制到底是如何实现的?

如图示,以 SHA1为例,两个字符串「我是中国人」与「我是中国人啊」只相差了一个字,但输出的结果完全不同,根本没法比较,退一步来说,就算要比较,每个 hash 结果也要一个字符一个字符的比,性能极差!

所以我们需要找到这样的一个 hash 函数,它需要满足两个条件

  1. 可以实现局部相似性
  2. 生成的 hash 结果利于比较

先来看第二点,要让 hash 结果利于比较,可以将结果转化为仅由 0,1组成的定长二进制数字,这样只要将结果进行异或运算,算出结果有几位 1 即可,simhash 就是这么做的

微信的原创保护机制到底是如何实现的?

如图示:将结果进行异或运算后只有两位为 1,即只有两位是不一样的

接下来我们再来看第一个问题,simhash 如何输出局部相似性的结果, 它的计算过程与利用余弦定理来计算文本相似度有一定的相似性,可以认为是余弦定理的一个演变,所以我们先来看看如何用余弦定理来计算两者的相似度

余弦定理

第一次听说余弦定理是在吴军的<<数学之美>>里看到的,通过余弦可以判断两篇文章是否相似,步骤都是类似的,将文章转化为 n 个维度的空间向量,再计算这两个空间向量的在空间中的夹角,我们以下两个文本为例来看看如何利用余弦定理来计算这两个文本的相似度(本例子来自阮一峰博客)

句子A:我喜欢看电视,不喜欢看电影。
句子B:我不喜欢看电视,也不喜欢看电影。

步骤一:分词

句子A:我/喜欢/看/电视,不/喜欢/看/电影。
句子B:我/不/喜欢/看/电视,也/不/喜欢/看/电影。

第二步,列出所有的关键词。

我,喜欢,看,电视,电影,不,也。

画外音:使用 TF-IDF 算法来算出所有的关键词,像 「的」,「地」,「得」这种无意义的顿词需要去掉

第三步,计算词频。

句子A:我 1,喜欢 2,看 2,电视 1,电影 1,不 1,也 0。

句子B:我 1,喜欢 2,看 2,电视 1,电影 1,不 2,也 1。

第四步,写出词频向量。

句子A:[1, 2, 2, 1, 1, 1, 0]

句子B:[1, 2, 2, 1, 1, 2, 1]

注:这里为了演示方便简单用出现的次数来作为词频向量,实际上生产上一般不会这么干,一般会利用 TF-IDF 算法来生成词频向量,本文不作展开,感兴趣的读者可以自行研究

于是问题表现为了如何在空间中计算这两个向量的相似度了,我们可以把这两个向量认为是两条线段,从原点[0, 0, xxx],指向这两点的线段,这两个线段形成了一个夹角,夹角越小,说明这两个向量越相似,如何知道这两个夹角的大小呢,计算它们的余弦值(cosθ)即可,如果值越接近 1, 说明 θ 越小,两个向量就越接近,文本也就越相似

微信的原创保护机制到底是如何实现的?

于是问题转化为了如何计算 cosθ 的值,回忆下大学的数据公式,其值计算如下微信的原创保护机制到底是如何实现的?

于是我们可以根据以上公式计算出句子 A 和句子 B 的 cosθ 值为:

微信的原创保护机制到底是如何实现的?

高达 93.8% 的相似度!这与实际情况吻合,既然使用余弦定理就可以计算文章的相似性,那为啥还要搞出 simhash 这样的算法呢,细心的朋友不难发现它的缺点,计算余弦的过程涉及到很多的乘法开方等计算,n 个分词最终转化后就是 n 维向量,一篇文章的分词是非常多的,也就意味着这个 n 是非常大的,所以计算余弦是非常耗时的,肯定无法应用于 Google 这样需要海量网页判重的场景。

由此分析可知余弘定理计算主要性能瓶颈在于文章转化后的高维度向量,高维度所需的计算量较复杂,那能否考虑降维呢,即把 n 维降低到 k 维(k 远小于 n)甚至是一维,维度越小,计算量就越小,接下来我们就来看看如何利用随机投影实现数据降维。

基于随机投影来实现空间向量的降维

向量点积含义

随机投影的基础方法,是向量点积运算。所以理解随机投影的基础,是理解向量点积运算的含义。

设二维空间内有两个向量,则其点积(也叫内积)定义为以下实数:

微信的原创保护机制到底是如何实现的?

点积运算

表示的是两个投影积,一个是

上的投影长度:

微信的原创保护机制到底是如何实现的?

一个是 OB 在其本身的投影长则为 |OB|,

如果我们把

看作是新空间的坐标轴,那么点 A 在新空间的坐标是

微信的原创保护机制到底是如何实现的?

假设有如下两个向量微信的原创保护机制到底是如何实现的?

那么点 A 以向量

所在直线为坐标轴的空间中,坐标为 a.b=7*1+3* (-1)=4,发现了吗,此时点 A 在新空间中的坐标由 2 维降到了 1 维,实际上向量点积不光可以实现二维降一维,也可以实现从 M 维降到 K  维。只要基于高斯分布(即正态分布),在原向量空间中找到一个 k 维向量

微信的原创保护机制到底是如何实现的?

就可以让原来任意一个在 M 维空间的向量 M 通过点积 M  ⋅ R 将其降维到 K 维,Johnson–Lindenstrauss 引理指出:在欧式空间中的若干点,经过相同的映射后进入新的空间,它们仍然会保持原来的相对位置,也就是说原来向量之间的夹角在向量降维映射到新空间后依然可以认为基本不变,这也就意味着降维后不会对文本的相似度计算产生影响

随机投影降维离散化—-基于随机投影的局部敏感哈希

通过随机投影法,确实实现了高维度降到低维度的目标,但降维后生成的向量坐标很可能是 float 型的,不利于存储,而且在计算比如余弦时,需要 float * float 的计算,我们知道浮点型计算是比较耗性能的,所以有人就提出能否对这些 float 的连续型坐标离散化,这样就解决了存储,计算的难点。

在将数据映射到降维后的新空间后,我们将落在坐标轴负轴的维度(该维度取值为负数),统一赋值为 0(或者 -1,使用 -1 的话 是将映射后的词语放置在整个空间中,而不是某一个象限,这样可以让数据点分布得更均匀一点),表示数据与对应随机向量夹角大于 90 度。类似的,我们将落在坐标轴非负轴的维度,统一赋值为 1。这样原始数据就被映射到了一个离散的新空间里。

这种离散化的数据映射方法,就是我们常说的基于随机投影的局部敏感哈希,经过离散化后,原来在空间中接近的数据点依然是相似或相同的,更重要的是经过离散化后转化为了 0,1 二进制数字,计算速度大大提高!

基于随机投影的局部敏感哈希,也是随机投影 hash 的一种,通过上述映射规则,将原空间向量进行了离散化降维

随机超平面 hash

知道了什么是基于随机投影的局部敏感哈希, 也就不难理解随机超平面 hash 了,它也是随机投影 hash 离散化的变种,对于一个 n 维向量 v,如果要得到一个由 0,1 组成的 f 位签名(f  远小于 n),它的算法如下:

  1. 随机产生 f 个 n 维的向量 r1,…rf;
  2. 对每一个向量 ri,如果 v 与 ri 的点积大于 0(说明在此向量划分的空间是相似的),则最终签名的第 i 位为 1,否则为 0。

这个算法相当于随机产生了 f 个 n 维超平面,每个超平面将向量 v 所在的空间一分为二,v 在这个超平面上方则得到一个 1,否则得到一个 0,然后将得到的 f 个 0 或 1 组合起来成为一个 f 维的签名

微信的原创保护机制到底是如何实现的?如图所示,随机在空间里划几个超平面,就可以把数据分到不同空间里,比如中间这个小三角的区域就可以赋值为110

每个降维后的 f 维签名,就是文章的最终签名!通过这样的解释相信大家不难理解通过异或比较位数的不同来判断文章的相似度的几何意义:位数不同,代表其在相应超平面上不相似

simhash 原理及实现

为啥前面花这么大力气介绍引出随机超平面 hash 呢,因为 simhash 就是基于超平面 hash 演变而来的,可以说理解了超平面 hash 也就理解了 simhash,接下来我们看看 simhash 的生成流程:

simhash 的生成划分为五个步骤:分词->hash->加权->合并->降维

  1. 分词:  这一步可以余弦定理的 1~4 步类似,首先,判断文本分词,形成这个文章的特征单词。然后,形成去掉噪音词的单词序列。最后,为每个分词加上权重。我们假设权重分为5个级别(1~5),比如:“ 美国“51区”雇员称内部有9架飞碟,曾看见灰色外星人 ” ==> 分词后为 “ 美国(4) 51区(5) 雇员(3) 称(1) 内部(2) 有(1) 9架(3) 飞碟(5) 曾(1) 看见(3) 灰色(4) 外星人(5)”,括号里是代表单词在整个句子里重要程度,数字越大越重要,为了方便解释,以下我们假设文档只有「美国」和「51区」这两个分词。

  2. hash: 通过 hash 算法把每个词变成 hash 值,比如“美国”通过 hash 算法计算为 100101,“51区”通过 hash 算法计算为 101011。这样,我们的字符串就变成了一串串数字,此 hash 值我们称为这些词对应的独热编码,然后再将 0 转为 -1,这样美国的「100101」编码为了「1-1-11-11」,51区的编码为「1-11-111」,将 0 转为 -1 的目的是将映射后的词语放置在整个空间中,而不是某一个象限,这样可以让数据点分布得更均匀一点,与随机超平面hash相比,这里使用了一个“不随机”的超平面,将空间进行了分割。

  3. 加权: 通过 2 步骤的 hash 生成结果,需要按照单词的权重形成加权数字串,比如「美国」的hash值为「1-1-11-11」,通过加权(权重参见步骤一得出的各个词语的权重值)计算(相乘)为「4 -4 -4 4 -4 4」;「51区」的 hash 值为「1-11-111」,通过加权计算为 「5 -5 5 -5 5 5」,得到的各向量即表征了这个文档

  4. 合并: 把上面各个单词算出来的序列值累加,变成只有一个序列串。比如 “美国”的 “4 -4 -4 4 -4 4”,“51区”的 “ 5 -5 5 -5 5 5”, 把每一位进行累加, “4+5 -4+-5 -4+5 4+-5 -4+5 4+5” ==》 “9 -9 1 -1 1 9”。

  5. 降维: 把第 4 步算出来的 「9 -9 1 -1 1 9」变成 0 1 串,形成我们最终的 simhash 签名。如果每一位大于 0 记为 1,小于 0 记为 0。最后算出结果为:「1 0 1 0 1 1」,这里采用了随机超平面 hash 的离散化方法,得到文本的最终表示

相信细心的你不难发现在第二步和第五步可以看到随机超平面的身影,也就是说并没有产生直接的随机超平面向量来映射,是间接产生的,如果想找到直接的超平面向量 R 来生成最后的签名也不难,我们就假设文档只有「美国」,「51区」这两个特征词,由第一,二步可知其文档向量为 d = (4, 5),hash 后的编码为 100101,101011,我们注意到第二步hash会做一层编码转换, 1 不变, 0 转为 -1

 100101  ----> 1-1-11-11
 101011  ----> 1-11-111

再用逗号隔开,使其成为了特征词对应的映射向量

「美国」对应的映射向量:(1, -1, -1, 1, -1, 1)
「51区」对应的映射向量:(1, -1, 1, -1, 1, 1)

再把上述每个特征词对应向量的第 i 位取出来组成 ri 向量,如下

r1 = (1, 1), r2 = (-1, -1), r3 = (-1, 1),  r4 = (1, -1), r5 = (-1, 1), r6 = (1, 1)

再回顾下随机 hash 超平面算法的第二步:

 对每一个向量 ri,如果 v 与 ri 的点积大于 0,则最终签名的第 i 位为 1,否则为 0。

将文档向量 d = (4, 5) 与上述 r1…r5 每一个向量相乘,可得结果为

(9, -9, 1, -1, 1, 9)  ---->   (1 , 0, 1, 0, 1, 1)

与 simhash 生成的完全一致!所以我们说 simhash 是从超平面 hash 算法演变更来的。

一般 simhash 生成的签名为 64 位,只要两个签名不同的位数少于等于 3 位我们就认为两个文章相似,这种使用不同进制位个数来计算两者差异的方式我们也叫汉明距离

simhash 查询优化

生成了 64 位的签名,然后就通过计算签名的异或来查询文章的相似度吗?too young too naive! 对于 Google 网页去重来说,可能会有几十亿的网页内容,那每次判重都需要使用签名进行几十亿的异或比较,这谁顶得住啊,那该如何优化呢?答案是利用抽屉原理进行优化存储。

什么是抽屉原理?把三个苹果放进四个抽屉里,必然有一个是空的

我们注意到判断文章相似的条件 ,对于签名为 64 位的 simhash 签名,只要位数少于等于 3 位即可判断为相似,这样的话我们可以把 64 位的签名分成四份,每份 16 位,如果相似,那必然有一份是完全相同的。

微信的原创保护机制到底是如何实现的?

我们可以把签名用 K-V 的形式进行存储, K 为其中的一部分,V 为剩余的 3 部分,先比较 K 是否精确匹配相同,如果匹配,再比较 V 部分的相似度,那么这四部分哪一部分应该为 K 呢,由于我们不知道哪一部分是精确匹配的,所以每一部分都应该为 K,剩余的部分为 V,以文本 1 为例,它应该设计成如下方式进行存储,这样保证不会有遗漏

微信的原创保护机制到底是如何实现的?

以下是查询库

微信的原创保护机制到底是如何实现的?

那么用这样的方式来存储到底提升了多少速度,我们一起来算笔帐。

假设数据库中有 2^30 条数据,也就是差不多 10 亿条数据:

  • 如果不用抽屉原理,则需要进行 10 亿次的比较
  • 如果使用抽屉原理
    • 首先先进行 K 的比较,由于是 K-V 也就是 hash 存储,所以 K 比较时间复杂度是 0(1),可以忽略不计,
    • K 如果精确匹配,把所有对应的 V 取出来即可,那么 V 可能有多少数据?因为 K 最多可能有 2^16位,所以 V 最多有 2^(30-16) = 2^14 位,
    • 由于最多进行 4 次 K 的比较,所以最多会进行 4 * 2^14 = 65536,约 6 万次比较

可以看到利用抽屉原理比较次数从 10 亿次降到了 6 万次!查询性能大大提升,当然了天下没有免费的午餐,由于数据复制了四份,存储空间也增大了 4 倍,这就是典型的以空间换时间。

simhash 缺点

simhash 比较适合海量长文上,短文本准确度上不高,因为用来度量长文本相似的汉明距离阈值为 3,但是短文本中,相似文本之间的汉明距离通常是大于 3 的。

所以你会发现在公众号后台如果你要标原创,字数必须大于 300,也是这个原因

微信的原创保护机制到底是如何实现的?

总结

理解 simhash 的关键在于理解超平面随机 hash,使用它可以实现向量从高维度到低维度的降维。网上有很多讲 simhash 的的文章,但大多把降维这个具体过程给跳过了,看得是让人一头雾头,所以笔者查阅了大量资料希望能帮助大家理解这一流程,希望大家能有收获,如果想对 simhash 有更深入的理解,可以查阅文末一堆的参考链接,都非常棒!

巨人的肩膀

  • https://www.cnblogs.com/shaosks/p/9121774.html
  • 局部敏感哈希算法及其思想的讨论:https://my.oschina.net/u/4367429/blog/3261406
  • http://www.ruanyifeng.com/blog/2013/03/cosine_similarity.html
  • https://zhuanlan.zhihu.com/p/81026564
  • http://www.hanting.tech/2017/05/23/simhash.html
  • https://zhuanlan.zhihu.com/p/92155250
  • https://blog.csdn.net/sunny_ss12/article/details/46949315
  • https://cloud.tencent.com/developer/article/1189493
  • https://www.cnblogs.com/sddai/p/10088007.html
  • 彻底弄懂LSH之simHash算法: https://www.cnblogs.com/hxsyl/p/4518506.html
  • 海量短文本场景下的去重算法:https://www.iyunying.org/seo/dataanalysis/152232.html
  • 海量数据相似度计算之simhash和海明距离: https://cloud.tencent.com/developer/article/1390215

 

·END·

来源:码海

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

转自:https://mp.weixin.qq.com/s/kAEEmHA_3NzYg_5AmjEiVA

短URL服务的设计以及实现


想必大家也经常收到垃圾短信吧…短信中的链接一般都是短链接,类似于下图这样:

短URL服务的设计以及实现

为什么这里面的URL都是短的呢?有什么好处呢?怎么做到的呢?

短URL的好处

  1. 短信和许多平台(微博)有字数限制 ,太长的链接加进去都没有办法写正文了.

  2. 好看。 比起一大堆不知所以的参数,短链接更加简洁友好.

  3. 方便做一些统计。 你点了链接会有人记录然后分析的.

  4. 安全。 不暴露访问参数.

这就是为什么我们现在收到的垃圾短信大多数都是短URL的原因了.

那么短URL是怎么做到的呢?

短URL基础原理

短URL从生成到使用分为以下几步.

  1. 有一个服务,将要发送给你的长URL对应到一个短URL上.例如www.baidu.com -> www.t.cn/1

  2. 把短URL拼接到短信等的内容上发送.

  3. 用户点击短URL,浏览器用301/302进行重定向,访问到对应的长URL.

  4. 展示对应的内容.

本文主要集中于第一步,即如何将一个长URL对应到短URL上.

服务设计

如果你在往长短URL真实的对应关系上想,那么就走远了.

最理想的情况是: 我们用一种算法,对每一个长URL,唯一的转换成短URL.还能保持反向转换的能力.

但是这是不可能的,如果有这样的算法,世界上的所有压缩算法都可以原地去世了.

正确的思路是建立一个发号器,每次有一个新的长URL进来,我们就增加一,并且将新的数值返回.第一个来的URL返回”www.x.cn/0“,第二个返回”www.x.cn/1“.

接下来以QA形式写几个小问题:

对应关系如何存储?

这个对应数据肯定是要落盘的,不能每次系统重启就重新排号,所以可以采用mysql等数据库来存储.而且如果数据量小且qps低,直接使用数据库的自增主键就可以实现.

如何保证长短链接一一对应?

按照上面的发号器策略,是不能保证长短链接的一一对应的,你连续用同一个URL请求两次,结果值都是不一样的.

为了实现长短链接一一对应,我们需要付出很大的空间代价,尤其是为了快速响应,我们可以需要在内存中做一层缓存,这样子太浪费了.

但是可以实现一些变种的,来实现部分的一一对应, 比如将最近/最热门的对应关系存储在K-V数据库中,这样子可以节省空间的同时,加快响应速度.

短URL的存储

我们返回的短URL一般是将数字转换成32进制,这样子可以更加有效的缩短URL长度,那么32进制的数字对计算机来说只是字符串,怎么存储呢?直接存储字符串对等值查找好找,对范围查找等太不友好了.

其实可以直接存储10进制的数字,这样不仅占用空间少,对查找的支持较好,同时还可以更加方便的转换到更多/更少的进制来进一步缩短URL.

高并发

如果直接存储在MySQL中,当并发请求增大,对数据库的压力太大,可能会造成瓶颈,这时候是可以有一些优化的.

缓存

上面保证长短链接一一对应中也提到过缓存,这里我们是为了加快程序处理速度.可以将热门的长链接(需要对长链接进来的次数进行计数),最近的长链接(可以使用redis保存最近一个小时的)等等进行一个缓存,保存在内存中或者类似redis的内存数据库中,如果请求的长URL命中了缓存,那么直接获取对应的短URL进行返回,不需要再进行生成操作.

批量发号

每一次发号都需要访问一次MySQL来获取当前的最大号码,并且在获取之后更新最大号码,这个压力是比较大的.

我们可以每次从数据库获取10000个号码,然后在内存中进行发放,当剩余的号码不足1000时,重新向MySQL请求下10000个号码.在上一批号码发放完了之后,批量进行写入.

这样可以将对数据库持续的操作移到代码中进行,并且异步进行获取和写入操作,保证服务的持续高并发.

分布式

上面设计的系统是有单点的,那就是发号器是个单点,容易挂掉.

可以采用分布式服务,分布式的话,如果每一个发号器进行发号之后都需要同步给其他发号器,那未必也太麻烦了.

换一种思路,可以有两个发号器,一个发单号,一个发双号,发号之后不再是递增1,而是递增2.

类比可得,我们可以用1000个服务,分别发放0-999尾号的数字,每次发号之后递增1000.这样做很简单,服务互相之间基本都不用通信,做好自己的事情就好了.

实现

由于我懒得写JDBC代码,更懒得弄Mybatis,所以代码中使用到MySQL的地方都使用了Redis.

package util;

import redis.clients.jedis.Jedis;

/**
 * Created by pfliu on 2019/06/23.
 */
public class ShortURLUtil {


    private static final String SHORT_URL_KEY = "SHORT_URL_KEY";
    private static final String LOCALHOST = "http://localhost:4444/";
    private static final String SHORT_LONG_PREFIX = "short_long_prefix_";
    private static final String CACHE_KEY_PREFIX = "cache_key_prefix_";
    private static final int CACHE_SECONDS = 1 * 60 * 60;

    private final String redisConfig;
    private final Jedis jedis;

    public ShortURLUtil(String redisConfig) {
        this.redisConfig = redisConfig;
        this.jedis = new Jedis(this.redisConfig);
    }

    public String getShortURL(String longURL, Decimal decimal) {
        // 查询缓存
        String cache = jedis.get(CACHE_KEY_PREFIX + longURL);
        if (cache != null) {
            return LOCALHOST + toOtherBaseString(Long.valueOf(cache), decimal.x);
        }

        // 自增
        long num = jedis.incr(SHORT_URL_KEY);
        // 在数据库中保存短-长URL的映射关系,可以保存在MySQL中
        jedis.set(SHORT_LONG_PREFIX + num, longURL);
        // 写入缓存
        jedis.setex(CACHE_KEY_PREFIX + longURL, CACHE_SECONDS, String.valueOf(num));
        return LOCALHOST + toOtherBaseString(num, decimal.x);
    }

    /**
     * 在进制表示中的字符集合
     */
    final static char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
            '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
            'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y',
            'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'};

    /**
     * 由10进制的数字转换到其他进制
     */
    private String toOtherBaseString(long n, int base) {
        long num = 0;
        if (n < 0) {
            num = ((long) 2 * 0x7fffffff) + n + 2;
        } else {
            num = n;
        }
        char[] buf = new char[32];
        int charPos = 32;
        while ((num / base) > 0) {
            buf[--charPos] = digits[(int) (num % base)];
            num /= base;
        }
        buf[--charPos] = digits[(int) (num % base)];
        return new String(buf, charPos, (32 - charPos));
    }

    enum Decimal {
        D32(32),
        D64(64);

        int x;

        Decimal(int x) {
            this.x = x;
        }
    }


    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            System.out.println(new ShortURLUtil("localhost").getShortURL("www.baidudu.com", Decimal.D32));
            System.out.println(new ShortURLUtil("localhost").getShortURL("www.baidu.com", Decimal.D64));
        }
    }
}

·END·


如果您喜欢本文,欢迎点击右上角,把文章分享到朋友圈~~

作者:呼延十

来源:https://juejin.im/post/6844903873950269454

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!