两种数据序列化方案性能对比:Msgpack和Json

专业名词zkbhj 发表了文章 • 0 个评论 • 56 次浏览 • 2019-01-23 11:00 • 来自相关话题

    MessagePack(简写msgpack)是一个高效的二进制序列化格式。它让你像JSON一样可以在各种语言之间交换数据。但是它比JSON更快、更小。小的整数会被编码成一个字节,短的字符串仅仅只需要比它的长度多一字节的大小。之前在lua脚本中使用过msgpack,因为有大量数据要入redis,而考虑到内存开销,使用了压缩比更大的msgpack。因为msgpack是一个二进制格式,所以没法像json后的字符串一样可直观地查看数据。
 
    msgpack的官网地址:http://pecl.php.net/package/msgpack 里面有各PHP版本windows下的dll扩展,也有源码包供linux下编译,所以像lua这样的脚本语言可以直接使用。msgpack和json_encode都是序列化存储数据,那么msgpack的效率与json的效率相比的话到底怎么样呢?看下面这个简单的对比程序:
//msgpack与json的性能对比
//1,数据拼凑
$data =array(
'youku' => '优酷视频', 'pptv' => 'PPTV', 'sohu' => '搜狐视频', 'qiyi' => '奇艺视频', 'letv' => '乐视视频',
'tencent' => '腾讯视频', 'sina' => '新浪视频', 'tudou' => '土豆视频', 'm1905' => '电影网', 'cntv' => 'CNTV',
);
$data = json_encode($data);
$newArr = array();
for($i = 1; $i<=500; $i++) //修改此处$i的最大值以控制数据的大小
{
$newArr [] =$data;
}

//数据大小对比
$msg_data = msgpack_pack($newArr);
echo "使用msgpack处理后大小:".strlen($msg_data);
$json_data = json_encode($newArr);
echo "<br>使用json处理后大小:".strlen($json_data);
echo "<br>msgpack处理后大小与json处理后大小比为1:".round((strlen($json_data)/strlen($msg_data)),2);

//计算1000次msgpack压缩用时
$time = microtime(true);
for($i = 1; $i<1000; $i++){
msgpack_pack($newArr);
}
echo '<br>1000次msgpack操作用时:'.(microtime(true)- $time);

//计算1000次json_encode压缩用时
$time1 = microtime(true);
for($i = 1; $i<1000; $i++){
json_encode($newArr);
}
echo '<br>1000次json_encode操作用时:'.(microtime(true)- $time1); 程序过程没什么可说的了,就是先拼凑了一个数组数据(用$i来控制它的大小)。然后对比对这个数组的处理用时,如果$i很小,假如为2,得到的结果如下:
使用msgpack处理后大小:609
使用json处理后大小:751
msgpack处理后大小与json处理后大小比为1:1.23
1000次msgpack操作用时:0.05400013923645
1000次json_encode操作用时:0.03600001335144 从上面的结果来看,msg_pack的效率根本不如json_encode的效率,只是msg_pack的压缩率大些而已。而当我把$i改大点,比如改到500后,结果就完全反转了:
使用msgpack处理后大小:152003
使用json处理后大小:187501
msgpack处理后大小与json处理后大小比为1:1.23
1000次msgpack操作用时:0.68599987030029
1000次json_encode操作用时:2.2000000476837     经过多次测试,最后做两个总结如下:    1,msg_pack的压缩效率比json_encode大是毫无疑问,但压缩比我这只看到提高了20%左右,可能和数据类型有关系,这个值只供参考。


    2,在数据量较小的情况下,msg_pack的效率不如json_encode.而在数据量较大时,msg_pack的效率就远大于json_encode。

    3,和数据序列化一样,对数据的反序列化上,也是数量量大时,msg_unpack的效率远大于json_decode.

    下面是进行反序列化时的结果:
1000次msgpack操作用时:0.80399990081787
1000次json_encode操作用时:2.6389999389648 查看全部
    MessagePack(简写msgpack)是一个高效的二进制序列化格式。它让你像JSON一样可以在各种语言之间交换数据。但是它比JSON更快、更小。小的整数会被编码成一个字节,短的字符串仅仅只需要比它的长度多一字节的大小。之前在lua脚本中使用过msgpack,因为有大量数据要入redis,而考虑到内存开销,使用了压缩比更大的msgpack。因为msgpack是一个二进制格式,所以没法像json后的字符串一样可直观地查看数据。
 
    msgpack的官网地址:http://pecl.php.net/package/msgpack 里面有各PHP版本windows下的dll扩展,也有源码包供linux下编译,所以像lua这样的脚本语言可以直接使用。msgpack和json_encode都是序列化存储数据,那么msgpack的效率与json的效率相比的话到底怎么样呢?看下面这个简单的对比程序:
//msgpack与json的性能对比
//1,数据拼凑
$data =array(
'youku' => '优酷视频', 'pptv' => 'PPTV', 'sohu' => '搜狐视频', 'qiyi' => '奇艺视频', 'letv' => '乐视视频',
'tencent' => '腾讯视频', 'sina' => '新浪视频', 'tudou' => '土豆视频', 'm1905' => '电影网', 'cntv' => 'CNTV',
);
$data = json_encode($data);
$newArr = array();
for($i = 1; $i<=500; $i++) //修改此处$i的最大值以控制数据的大小
{
$newArr [] =$data;
}

//数据大小对比
$msg_data = msgpack_pack($newArr);
echo "使用msgpack处理后大小:".strlen($msg_data);
$json_data = json_encode($newArr);
echo "<br>使用json处理后大小:".strlen($json_data);
echo "<br>msgpack处理后大小与json处理后大小比为1:".round((strlen($json_data)/strlen($msg_data)),2);

//计算1000次msgpack压缩用时
$time = microtime(true);
for($i = 1; $i<1000; $i++){
msgpack_pack($newArr);
}
echo '<br>1000次msgpack操作用时:'.(microtime(true)- $time);

//计算1000次json_encode压缩用时
$time1 = microtime(true);
for($i = 1; $i<1000; $i++){
json_encode($newArr);
}
echo '<br>1000次json_encode操作用时:'.(microtime(true)- $time1);
 程序过程没什么可说的了,就是先拼凑了一个数组数据(用$i来控制它的大小)。然后对比对这个数组的处理用时,如果$i很小,假如为2,得到的结果如下:
使用msgpack处理后大小:609
使用json处理后大小:751
msgpack处理后大小与json处理后大小比为1:1.23
1000次msgpack操作用时:0.05400013923645
1000次json_encode操作用时:0.03600001335144
 从上面的结果来看,msg_pack的效率根本不如json_encode的效率,只是msg_pack的压缩率大些而已。而当我把$i改大点,比如改到500后,结果就完全反转了:
使用msgpack处理后大小:152003
使用json处理后大小:187501
msgpack处理后大小与json处理后大小比为1:1.23
1000次msgpack操作用时:0.68599987030029
1000次json_encode操作用时:2.2000000476837
     经过多次测试,最后做两个总结如下:    1,msg_pack的压缩效率比json_encode大是毫无疑问,但压缩比我这只看到提高了20%左右,可能和数据类型有关系,这个值只供参考。


    2,在数据量较小的情况下,msg_pack的效率不如json_encode.而在数据量较大时,msg_pack的效率就远大于json_encode。

    3,和数据序列化一样,对数据的反序列化上,也是数量量大时,msg_unpack的效率远大于json_decode.

    下面是进行反序列化时的结果:
1000次msgpack操作用时:0.80399990081787
1000次json_encode操作用时:2.6389999389648

服务保障经验谈之服务熔断

架构思想zkbhj 发表了文章 • 0 个评论 • 58 次浏览 • 2018-12-18 10:56 • 来自相关话题

什么是服务熔断?

熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。

这种牺牲局部,保全整体的措施就叫做熔断。

如果不采取熔断措施,我们的系统会怎样呢?我们来看一个栗子。

当前系统中有A,B,C三个服务,服务A是上游,服务B是中游,服务C是下游。它们的调用链如下:





 
一旦下游服务C因某些原因变得不可用,积压了大量请求,服务B的请求线程也随之阻塞。线程资源逐渐耗尽,使得服务B也变得不可用。紧接着,服务A也变为不可用,整个调用链路被拖垮。




像这种调用链路的连锁故障,叫做雪崩。

正所谓刮骨疗毒,壮士断腕。在这种时候,就需要我们的熔断机制来挽救整个系统。熔断机制的大体流程和刚才所讲的考试策略如出一辙:





 
这里需要解释两点:

1.开启熔断

在固定时间窗口内,接口调用超时比率达到一个阈值,会开启熔断。进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的默认方法,达到服务降级的效果。


2.熔断回复

熔断不可能是永久的。当经过了规定时间之后,服务将从熔断状态回复过来,再次接受调用方的远程调用。
 
更多服务降级熔断限流,参考:https://www.cnblogs.com/raosha ... .html 查看全部
什么是服务熔断?

熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。

这种牺牲局部,保全整体的措施就叫做熔断。

如果不采取熔断措施,我们的系统会怎样呢?我们来看一个栗子。

当前系统中有A,B,C三个服务,服务A是上游,服务B是中游,服务C是下游。它们的调用链如下:

20180507103456708.png

 
一旦下游服务C因某些原因变得不可用,积压了大量请求,服务B的请求线程也随之阻塞。线程资源逐渐耗尽,使得服务B也变得不可用。紧接着,服务A也变为不可用,整个调用链路被拖垮。
20180507103502138.png

像这种调用链路的连锁故障,叫做雪崩

正所谓刮骨疗毒,壮士断腕。在这种时候,就需要我们的熔断机制来挽救整个系统。熔断机制的大体流程和刚才所讲的考试策略如出一辙:

20180507103507143.png

 
这里需要解释两点:

1.开启熔断

在固定时间窗口内,接口调用超时比率达到一个阈值,会开启熔断。进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的默认方法,达到服务降级的效果。


2.熔断回复

熔断不可能是永久的。当经过了规定时间之后,服务将从熔断状态回复过来,再次接受调用方的远程调用。
 
更多服务降级熔断限流,参考:https://www.cnblogs.com/raosha ... .html

一点点学习Linux:如何添加crontab计划任务?

服务器zkbhj 发表了文章 • 0 个评论 • 64 次浏览 • 2018-11-13 11:53 • 来自相关话题

crond 是linux用来定期执行程序的命令。当安装完成操作系统之后,默认便会启动此任务调度命令。crond命令每分锺会定期检查是否有要执行的工作,如果有要执行的工作便会自动执行该工作。可以用以下的方法启动、关闭这个服务:
/sbin/service crond start //启动服务

/sbin/service crond stop //关闭服务

/sbin/service crond restart //重启服务

/sbin/service crond reload //重新载入配置1.linux任务调度的工作主要分为以下两类:

系统执行的工作:系统周期性所要执行的工作,如备份系统数据、清理缓存

个人执行的工作:某个用户定期要做的工作,例如每隔10分钟检查邮件服务器是否有新信,这些工作可由每个用户自行设置。


2.crontab命令选项:

cron服务提供crontab命令来设定cron服务的,以下是这个命令的一些参数与说明:

crontab -u //设定某个用户的cron服务,一般root用户在执行这个命令的时候需要此参数

crontab -l //列出某个用户cron服务的详细内容

crontab -r //删除某个用户的cron服务

crontab -e //编辑某个用户的cron服务

比如说root查看自己的cron设置:crontab -u root -l

再例如,root想删除fred的cron设置:crontab -u fred -r

在编辑cron服务时,编辑的内容有一些格式和约定,输入:crontab -u root -e

进入vi编辑模式,编辑的内容一定要符合下面的格式:*/1 * * * * ls >> /tmp/ls.txt

3.cron文件语法 分 小时 日 月 星期 命令

0-59 0-23 1-31 1-12 0-6 command (取值范围,0表示周日一般一行对应一个任务)4.记住几个特殊符号的含义:

"*"代表取值范围内的数字,

"/"代表"每",

"-"代表从某个数字到某个数字,

","分开几个离散的数字
 
5.举几个例子
5 * * * * ls //指定每小时的第5分钟执行一次ls命令

30 5 * * * ls //指定每天的 5:30 执行ls命令

30 7 8 * * ls //指定每月8号的7:30分执行ls命令

30 5 8 6 * ls //指定每年的6月8日5:30执行ls命令

30 6 * * 0 ls //指定每星期日的6:30执行ls命令[注:0表示星期天,1表示星期1,以此类推,也可以用英文来表示,sun表示星期天,mon表示星期一等。]

30 3 10,20 * * ls //每月10号及20号的3:30执行ls命令[注:”,”用来连接多个不连续的时段]

25 8-11 * * * ls //每天8-11点的第25分钟执行ls命令[注:”-”用来连接连续的时段]

*/15 * * * * ls //每15分钟执行一次ls命令 [即每个小时的第0 15 30 45 60分钟执行ls命令 ]

30 6 */10 * * ls //每个月中,每隔10天6:30执行一次ls命令[即每月的1、11、21、31日是的6:30执行一次ls命令。 ]

50 7 * * * root run-parts /etc/cron.daily //每天7:50以root 身份执行/etc/cron.daily目录中的所有可执行文件[ 注:run-parts参数表示,执行后面目录中的所有可执行文件。 ]6.新增调度任务可用两种方法:

a.在命令行输入: crontab -e 然后添加相应的任务,wq存盘退出。

b.直接编辑/etc/crontab 文件,即vi /etc/crontab,添加相应的任务。 查看全部
crond 是linux用来定期执行程序的命令。当安装完成操作系统之后,默认便会启动此任务调度命令。crond命令每分锺会定期检查是否有要执行的工作,如果有要执行的工作便会自动执行该工作。可以用以下的方法启动、关闭这个服务:
/sbin/service crond start //启动服务

/sbin/service crond stop //关闭服务

/sbin/service crond restart //重启服务

/sbin/service crond reload //重新载入配置
1.linux任务调度的工作主要分为以下两类:


系统执行的工作:系统周期性所要执行的工作,如备份系统数据、清理缓存

个人执行的工作:某个用户定期要做的工作,例如每隔10分钟检查邮件服务器是否有新信,这些工作可由每个用户自行设置。



2.crontab命令选项:

cron服务提供crontab命令来设定cron服务的,以下是这个命令的一些参数与说明:

crontab -u //设定某个用户的cron服务,一般root用户在执行这个命令的时候需要此参数

crontab -l //列出某个用户cron服务的详细内容

crontab -r //删除某个用户的cron服务

crontab -e //编辑某个用户的cron服务

比如说root查看自己的cron设置:crontab -u root -l

再例如,root想删除fred的cron设置:crontab -u fred -r

在编辑cron服务时,编辑的内容有一些格式和约定,输入:crontab -u root -e

进入vi编辑模式,编辑的内容一定要符合下面的格式:*/1 * * * * ls >> /tmp/ls.txt

3.cron文件语法
  分     小时      日       月       星期      命令

0-59 0-23 1-31 1-12 0-6 command (取值范围,0表示周日一般一行对应一个任务)
4.记住几个特殊符号的含义:

"*"代表取值范围内的数字,

"/"代表"每",

"-"代表从某个数字到某个数字,

","分开几个离散的数字
 
5.举几个例子
5       *       *       *      *     ls              //指定每小时的第5分钟执行一次ls命令

30 5 * * * ls //指定每天的 5:30 执行ls命令

30 7 8 * * ls //指定每月8号的7:30分执行ls命令

30 5 8 6 * ls //指定每年的6月8日5:30执行ls命令

30 6 * * 0 ls //指定每星期日的6:30执行ls命令[注:0表示星期天,1表示星期1,以此类推,也可以用英文来表示,sun表示星期天,mon表示星期一等。]

30 3 10,20 * * ls //每月10号及20号的3:30执行ls命令[注:”,”用来连接多个不连续的时段]

25 8-11 * * * ls //每天8-11点的第25分钟执行ls命令[注:”-”用来连接连续的时段]

*/15 * * * * ls //每15分钟执行一次ls命令 [即每个小时的第0 15 30 45 60分钟执行ls命令 ]

30 6 */10 * * ls //每个月中,每隔10天6:30执行一次ls命令[即每月的1、11、21、31日是的6:30执行一次ls命令。 ]

50 7 * * * root run-parts /etc/cron.daily //每天7:50以root 身份执行/etc/cron.daily目录中的所有可执行文件[ 注:run-parts参数表示,执行后面目录中的所有可执行文件。 ]
6.新增调度任务可用两种方法:

a.在命令行输入: crontab -e 然后添加相应的任务,wq存盘退出。

b.直接编辑/etc/crontab 文件,即vi /etc/crontab,添加相应的任务。

类设计的六大基本原则

架构思想zkbhj 发表了文章 • 0 个评论 • 68 次浏览 • 2018-11-12 20:12 • 来自相关话题

一.单一职责原则

Single Responsibility Principle, 简称SRP。

定义:There should never be more than one reason for a class to change.

应该有且仅有一个原因引起类的变更。
 
二.里氏替换原则

Liskov Substitution Principle, 简称LSP。

定义:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

(所有引用基类的地方必须能透明地使用其子类的对象)
 
三.依赖倒置原则

Dependence Inversion Principle, 简称DIP

定义:High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.

翻译过来,包含三层含义:

1.高层模块不应该依赖低层模块,两者都应该依赖其抽象。2.抽象不应该依赖细节。3.细节应该依赖抽象。

精简的定义: 面向接口编程。
 
四.接口隔离原则:
接口--这里指用interface关键字定义的接口。
定义:
1.Clients should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口)
2.The dependency of one class to anther one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上)

概括:建立单一接口,不要建立臃肿庞大的接口。通俗来讲:接口尽量细化,同时接口中的方法尽量少。
 
五.迪米特法则
Law of Demeter, LOD。又称最少知识原则(Least Knowledge Principle, LKP)。
通俗来讲:一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没有关系,那是你的事情,我就调用你提供的public方法,其他一概不关心。
 
六.开闭原则
Software entities like classes, modules and functions should be open for extension but closed for modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭) 查看全部
一.单一职责原则

Single Responsibility Principle, 简称SRP。

定义:There should never be more than one reason for a class to change.

应该有且仅有一个原因引起类的变更。
 
二.里氏替换原则

Liskov Substitution Principle, 简称LSP。

定义:Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

(所有引用基类的地方必须能透明地使用其子类的对象)
 
三.依赖倒置原则

Dependence Inversion Principle, 简称DIP

定义:High level modules should not depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details.Details should depend upon abstractions.

翻译过来,包含三层含义:

1.高层模块不应该依赖低层模块,两者都应该依赖其抽象。2.抽象不应该依赖细节。3.细节应该依赖抽象。

精简的定义: 面向接口编程。
 
四.接口隔离原则:
接口--这里指用interface关键字定义的接口。
定义:
1.Clients should not be forced to depend upon interfaces that they don't use.(客户端不应该依赖它不需要的接口)
2.The dependency of one class to anther one should depend on the smallest possible interface.(类间的依赖关系应该建立在最小的接口上)

概括:建立单一接口,不要建立臃肿庞大的接口。通俗来讲:接口尽量细化,同时接口中的方法尽量少。
 
五.迪米特法则
Law of Demeter, LOD。又称最少知识原则(Least Knowledge Principle, LKP)。
通俗来讲:一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没有关系,那是你的事情,我就调用你提供的public方法,其他一概不关心。
 
六.开闭原则
Software entities like classes, modules and functions should be open for extension but closed for modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭)

#原创#API接口设计要考虑的几个重要原则和方法总结

架构思想zkbhj 发表了文章 • 0 个评论 • 127 次浏览 • 2018-11-12 11:04 • 来自相关话题

这里想和大家讨论的是在后台接口设计过程中,还有哪些方面需要考虑,以及还有哪些优秀的技术实践方案可以借鉴。

 【规范和最佳实践】

1.合理的接口命名;

接口的命名必须规范优雅,在未看到接口文档时,就可以根据接口的URL明白接口的功能是什么?

如下面的例子://好的接口命名示例
/customer/cert/search.json

//不好的接口命名示例
/customer/info/get.json
2.入参和出参的规范化定义,有统一的风格;

一个项目内的所有接口,必须有统一的风格,统一返回格式,约定业务层错误编码,每个编码可以携带明确的错误信息。出入参字段含义明确,采用统一的命名规范,如驼峰命名等。返回格式统一采用json格式。举一个例子:{
"status": "failure",
"error_code": 100003,
"error_message": "未获取到用户信息",
"data":
}status标识接口是否逻辑处理成功;error_code为不同类型错误信息对应的唯一错误码,error_message为错误信息的简要描述信息(注意某些数据或者信息是否可直接展示给用户),data则为需要返回给调用方的数据信息。
 
另外,每个参数一定要有明确且固定的数据格式,int就是int,string就是string,array就是array,object就是object,因为对于一些对数据类型要求比较严格的使用方,不明确的数据格式返回,可能会造成不可预知的错误。
 
下面给大家列一下Json里的六种基本数据格式: 

Number:整数或浮点数
String:字符串
Boolean:true 或 false
Array:数组包含在方括号中
Object:对象包含在大括号{}中
Null:空类型

 
3.接口的功能定义要具备单一性;

单一性是指接口要做的事情应该是一个比较单一的事情,比如登陆接口,登陆完成应该只是返回登陆成功以后一些用户信息如uid即可,但很多人为了减少接口交互,返回一大堆额外的数据。

但有时候对于一些内部系统接口来讲,为了实现通用性,可能会提供一些通用的查询接口,即在同一个接口内返回尽可能多的信息,但也不建议这么做,至少不是一个好的实践;


4.明确接口支持的协议;

接口要明确所支持的协议,是POST/GET/PUT/DELETE等的哪一个。一般来讲,同一个接口而言,尽量只支持一种协议,并且在接口被调用时,如果参数传递非接口定义协议,要明确提示返回错误信息,这样可以减少很多类似于“为啥我调接口参数都对还调不通”的问题的沟通成本。同时,严格的协议规范也可以避免一些意料之外的问题出现。例如:您请求的资源不支持 http 方法“POST
5.是否支持幂等;

这是作为一个接口而言,很需要明确的一点,尤其是在一些特殊的应用场景下,是否支持幂等是需要首先明确的。比如下面这个例子:

发放卡券的接口:/coupon/card/handOut.json POST

这是一个卡券系统里发放租金卡的接口,支持POST协议传参。由于很多种原因,同一类卡券被某个人领取时,都可能会产生接口被调用不支持一次的情形,比如网络抖动、用户快速点击、甚至是恶意刷接口等,我们希望,对于“同一个调用”,我们给用户返回的结果应该是一致的,这就是幂等。实现幂等的方式有几种,比如卡券系统就是通过生成订单号的形式完成的接口幂等;


6、充分考虑接口的可扩展性,避免做大而全的接口;

要根据实际业务场景定义接口,充分考虑接口的可扩展性。比如自如的APP首页数据接口,我们可以设计成整个首页就一个大接口,但是假如这样,未来再次改版APP,我们可能就需要完全重新写这个接口,但假如我们按模块区分接口,可能我们仅需要开发新增加的模块的接口,对于以前有的,在数据结构不调整只做样式改变的需求里,就可以减少工作量的开发。
另外这么做还有一个优点,尤其对移动端作用尤为突出,由于移动端对带宽有限,所以,接口中尽量不要返回无用的信息,只返回真正需要的数据,进而减少由于过多的数据量影响处理速度,最重要的是影响传输效率。


7、接口里尽量不做客户端可以处理的逻辑,减少服务端压力;

接口主要是提供给客户端数据的,对于能够在客端完成的逻辑处理,尽量由客端来处理(当然APP比较特殊,如果改动这部分逻辑需要发版,还是需要放在后端),进而减轻后端服务器的压力,让后端接口更加“专心”做好数据服务;


8、清晰的日志分类、记录以及归档规范;

接口日志很重要,无论对于追溯问题还是解决bug,都有着举足轻重的作用。所以,好的日志规范,是一个很好的习惯。日志主要有info和error两种(warning一般不做记录,或者很少用到),info日志一般用于记录现场,用来追溯问题;error日志一般用于协作我们查找bug,定位代码问题。

另外日志也需要定期做归档处理,防止机器磁盘被日志文件大量占用。

更高级的,可以接入一些日志搜集和分析工具,如ELK等,将日志信息持久化存储以及可视化展示,更加方便的对日志信息进行使用。


9、版本控制:
 
这一点,对于接口来讲,非常重要。在实际的场景中,维护多个版本是非常常见的事情,在系统的迭代升级过程中,无可避免的会增减返回参数及入参,修改返回数据的结构,甚至会废弃原有接口改为新的数据接口。所以,为了不影响老版本应用的正常使用,大部分应用后台都会针对性的维护多个接口版本。
一般来讲,有2中常用的方式:

1.每个接口有各自的版本,一般为接口添加个version的参数。 
2.整个接口系统有统一的版本,一般在URL中添加版本号,比如 http://api.zkbhj.com/v2。 

 

【性能和高可用】

1、接口的平均响应时长、支持的并发数、TPS;

这个很重要,无论是我们自己设计的接口,还是我们在使用第三方提供的接口时,我们都需要明确接口的平均响应时长,因为这直接关系到你系统的安全性问题!如果在接口调用时,没有设置合理的超时时间甚至都没有设置超时时间,那么一旦所以依赖的接口出现问题甚至服务不可用时,对你调用方系统来讲,将是致命的,雪崩式的系统崩溃,很大一部分就是由于这个原因造成的。所以为了不害人害己,设计以及最终提供的接口,一定要提供一个明确的接口平均响应时长,而且要在接口文档中写明并强烈建议接口调用方设置合理的超时时间,防止由于接口超时而造成雪崩式的连锁反应。


2、数据库和缓存的选择;

为了提高接口性能,合理的选择数据库和缓存很是总要。一般情况下,关系型存储我们一般都会选择MySQL数据库,缓存一般都选择Redis。当然,MySQL数据库的分库分表,加索引用事务、读写分离等,redis作为缓存使用时的缓存时长、缓存数据类型等,都有他们的使用原则和最佳实践,这里不做赘述。我们这里只讨论在何种场景下要使用缓存。比如查询类型的接口,如果要查询的数据并不是实时性要求很高的接口,那我们可以进行缓存处理,比如APP首页接口,一般都是CMS里面配置的一些图文信息,我们有必要做缓存处理。当然可能里面有一部分数据是需要实时的,比如自如寓的管家信息,那我们可以把这一部分内容做实时的处理。
 

3、限流、熔断和降级;

对于一些特殊的应用场景,比如抢红包、秒杀等,要对接口进行限流处理,方式短时间内的高并发请求将接口搞死;

接口熔断和降级,是为了解决系统不被拖死,不影响核心业务流程而采取的措施,比如获取用户信息列表,实时获取用户头像和昵称的接口暂时不可用(比如根据设置,10个请求里6个以上都超时,则判定为服务不可用,触发熔断机制),我们可以主动放弃调用(熔断),只返回核心数据uid等,昵称和头像暂时返回默认数据(降级);


4、消除单点,负载均衡;

对于任何一个接口服务,我们至少要有2台机器对外提供服务,禁止单点服务,单点一旦出问题,会直接造成服务不可用;

对于访问量很大的API服务,为了提供更加快速的接口响应,我们往往不是单台机器提供服务,而是有多台机器组成一个分布式集群对外提供服务。这个时候就会涉及到负载均衡,比如我们就会由nginx来做负载均衡,根据一定的策略机制,将接口请求平均的分发到不同的应用机器上进行处理和响应。进而提高接口的性能。


5、是否有第三方服务接口?

如果接口依赖了第三方服务接口,能用缓存就用缓存。这样可以进一步降低由于第三方接口不稳定给我们自己系统造成的波动。当然,也有一些第三方接口无法做缓存,比如就是要实时进行身份验证等,这个时候,超时时间的设置就尤为重要!


6、能异步处理的异步处理:

其实有很多场景下,一个接口里面的很多逻辑是可以异步处理的。举个例子:

比如用户注册场景,用户注册成功之后会给客户的邮箱发送一封激活邮件。常规的逻辑流程应该是,前段提交用户信息到注册接口,注册接口做各种校验,校验通过后,发送邮件,发送成功后,返回给前端告诉用户注册成功,请进入邮箱激活账号。其实,这个流程里的“发送邮件”就是可以拿出来异步处理的部分,当校验通过而且注册完成之后,我们把发送邮件这件事抛出去,交给另外一个就负责发邮件的任务进行处理(如我们现在有的补偿队列,或者是发一个MQ消息),然后直接返回给用户注册成功。这样,注册接口的平均响应时间一定会比第一种方案提高很多。


7、更高要求的高可用,可以采用异地机房部署;

在物理地域上就分开部署,两地同时崩溃的概率还是比较低的;


8、监控和报警;

在对接口建立高效的监控和报警机制,能够及时发现问题并通知到相应的人员进行第一时间的处理和跟进。


【稳定和安全】

1、身份验证;

在一些接口场景中,是要依赖于用户身份的,比如通过token还实现用户身份的验证;


2、接口防抓取和串改数据;

防止数据被轻易抓取到,我们可以采用https作为接口的网络传输协议,进而保证数据包不被轻易的就抓取和分析。即使这种情况下,依然被抓取到,我们还可以对传输的数据进行我们自己的加密处理,比如用对称加密算法AES或者非对称加密算法RSA,亦或是我们系统内部自己商定好的加密算法,对数据进行加密处理,这样,即使抓取到数据包,也很难分析出数据的原始信息。

对于防止数据被串改,可以使用sign验签,进一步防止接口参数被串改的可能性。


3、防刷;

接口防刷会有一些策略,根据实际的应用场景进行选择,比如增加图形验证码、接入智能验证码、时间戳限制单位时间内的调用次数、ip限制等;

另外监控很重要,及时发现异常的调用,进行封禁处理;


【其他】

1、是否需要支持跨域;

这一点是针对于H5提供接口时需要考虑的,因为一般情况下,实际的应用和接口所在的域并不是同一个域,基于浏览器的安全策略,对于XHR请求来讲,是不允许进行跨域请求的,所以,一般提供给H5的接口要支持跨域请求。当然解决跨域的方法也不在本次讨论的范围之内,目前主流的方式就是在服务器配置的header头信息中增加两项参数。



2、基于H5提供接口的一些安全性问题;

比如常见的CSRF攻击,我们可以在接口里验证 HTTP Referer字段、x-requested-with字段、header中增加token等,从一定程度上提高被CSRF攻击的门槛。 
 
3、在代码结构层面,尽量和其他部分分开;
 
API集中由同一个系统“模块”提供,尽量不要和页面等其他功能混合开发。例如下面的项目分层模式就是一个较好的实践方案:





 
即所有API接口均分布在api内部,不与pc(PC站页面)、mobile(M站页面)等混合在一起。 

4、文档:
 
好的接口,还有一项优点,就是会有为之配套的接口文档。如果希望降低接口文档的维护成本等,也可以使用开源的第三方自动化接口文档工具,比如swagger等。 查看全部
这里想和大家讨论的是在后台接口设计过程中,还有哪些方面需要考虑,以及还有哪些优秀的技术实践方案可以借鉴。

 【规范和最佳实践】

1.合理的接口命名;

接口的命名必须规范优雅,在未看到接口文档时,就可以根据接口的URL明白接口的功能是什么?

如下面的例子:
//好的接口命名示例
/customer/cert/search.json

//不好的接口命名示例
/customer/info/get.json

2.入参和出参的规范化定义,有统一的风格;

一个项目内的所有接口,必须有统一的风格,统一返回格式,约定业务层错误编码,每个编码可以携带明确的错误信息。出入参字段含义明确,采用统一的命名规范,如驼峰命名等。返回格式统一采用json格式。举一个例子:
{
"status": "failure",
"error_code": 100003,
"error_message": "未获取到用户信息",
"data":
}
status标识接口是否逻辑处理成功;error_code为不同类型错误信息对应的唯一错误码,error_message为错误信息的简要描述信息(注意某些数据或者信息是否可直接展示给用户),data则为需要返回给调用方的数据信息。
 
另外,每个参数一定要有明确且固定的数据格式,int就是int,string就是string,array就是array,object就是object,因为对于一些对数据类型要求比较严格的使用方,不明确的数据格式返回,可能会造成不可预知的错误。
 
下面给大家列一下Json里的六种基本数据格式: 


Number:整数或浮点数
String:字符串
Boolean:true 或 false
Array:数组包含在方括号中
Object:对象包含在大括号{}中
Null:空类型


 
3.接口的功能定义要具备单一性;

单一性是指接口要做的事情应该是一个比较单一的事情,比如登陆接口,登陆完成应该只是返回登陆成功以后一些用户信息如uid即可,但很多人为了减少接口交互,返回一大堆额外的数据。

但有时候对于一些内部系统接口来讲,为了实现通用性,可能会提供一些通用的查询接口,即在同一个接口内返回尽可能多的信息,但也不建议这么做,至少不是一个好的实践;


4.明确接口支持的协议;

接口要明确所支持的协议,是POST/GET/PUT/DELETE等的哪一个。一般来讲,同一个接口而言,尽量只支持一种协议,并且在接口被调用时,如果参数传递非接口定义协议,要明确提示返回错误信息,这样可以减少很多类似于“为啥我调接口参数都对还调不通”的问题的沟通成本。同时,严格的协议规范也可以避免一些意料之外的问题出现。例如:
您请求的资源不支持 http 方法“POST

5.是否支持幂等;

这是作为一个接口而言,很需要明确的一点,尤其是在一些特殊的应用场景下,是否支持幂等是需要首先明确的。比如下面这个例子:

发放卡券的接口:/coupon/card/handOut.json POST

这是一个卡券系统里发放租金卡的接口,支持POST协议传参。由于很多种原因,同一类卡券被某个人领取时,都可能会产生接口被调用不支持一次的情形,比如网络抖动、用户快速点击、甚至是恶意刷接口等,我们希望,对于“同一个调用”,我们给用户返回的结果应该是一致的,这就是幂等。实现幂等的方式有几种,比如卡券系统就是通过生成订单号的形式完成的接口幂等;


6、充分考虑接口的可扩展性,避免做大而全的接口;

要根据实际业务场景定义接口,充分考虑接口的可扩展性。比如自如的APP首页数据接口,我们可以设计成整个首页就一个大接口,但是假如这样,未来再次改版APP,我们可能就需要完全重新写这个接口,但假如我们按模块区分接口,可能我们仅需要开发新增加的模块的接口,对于以前有的,在数据结构不调整只做样式改变的需求里,就可以减少工作量的开发。
另外这么做还有一个优点,尤其对移动端作用尤为突出,由于移动端对带宽有限,所以,接口中尽量不要返回无用的信息,只返回真正需要的数据,进而减少由于过多的数据量影响处理速度,最重要的是影响传输效率。


7、接口里尽量不做客户端可以处理的逻辑,减少服务端压力;

接口主要是提供给客户端数据的,对于能够在客端完成的逻辑处理,尽量由客端来处理(当然APP比较特殊,如果改动这部分逻辑需要发版,还是需要放在后端),进而减轻后端服务器的压力,让后端接口更加“专心”做好数据服务;


8、清晰的日志分类、记录以及归档规范;

接口日志很重要,无论对于追溯问题还是解决bug,都有着举足轻重的作用。所以,好的日志规范,是一个很好的习惯。日志主要有info和error两种(warning一般不做记录,或者很少用到),info日志一般用于记录现场,用来追溯问题;error日志一般用于协作我们查找bug,定位代码问题。

另外日志也需要定期做归档处理,防止机器磁盘被日志文件大量占用。

更高级的,可以接入一些日志搜集和分析工具,如ELK等,将日志信息持久化存储以及可视化展示,更加方便的对日志信息进行使用。


9、版本控制:
 
这一点,对于接口来讲,非常重要。在实际的场景中,维护多个版本是非常常见的事情,在系统的迭代升级过程中,无可避免的会增减返回参数及入参,修改返回数据的结构,甚至会废弃原有接口改为新的数据接口。所以,为了不影响老版本应用的正常使用,大部分应用后台都会针对性的维护多个接口版本。
一般来讲,有2中常用的方式:


1.每个接口有各自的版本,一般为接口添加个version的参数。 
2.整个接口系统有统一的版本,一般在URL中添加版本号,比如 http://api.zkbhj.com/v2。 


 

【性能和高可用】

1、接口的平均响应时长、支持的并发数、TPS;

这个很重要,无论是我们自己设计的接口,还是我们在使用第三方提供的接口时,我们都需要明确接口的平均响应时长,因为这直接关系到你系统的安全性问题!如果在接口调用时,没有设置合理的超时时间甚至都没有设置超时时间,那么一旦所以依赖的接口出现问题甚至服务不可用时,对你调用方系统来讲,将是致命的,雪崩式的系统崩溃,很大一部分就是由于这个原因造成的。所以为了不害人害己,设计以及最终提供的接口,一定要提供一个明确的接口平均响应时长,而且要在接口文档中写明并强烈建议接口调用方设置合理的超时时间,防止由于接口超时而造成雪崩式的连锁反应。


2、数据库和缓存的选择;

为了提高接口性能,合理的选择数据库和缓存很是总要。一般情况下,关系型存储我们一般都会选择MySQL数据库,缓存一般都选择Redis。当然,MySQL数据库的分库分表,加索引用事务、读写分离等,redis作为缓存使用时的缓存时长、缓存数据类型等,都有他们的使用原则和最佳实践,这里不做赘述。我们这里只讨论在何种场景下要使用缓存。比如查询类型的接口,如果要查询的数据并不是实时性要求很高的接口,那我们可以进行缓存处理,比如APP首页接口,一般都是CMS里面配置的一些图文信息,我们有必要做缓存处理。当然可能里面有一部分数据是需要实时的,比如自如寓的管家信息,那我们可以把这一部分内容做实时的处理。
 

3、限流、熔断和降级;

对于一些特殊的应用场景,比如抢红包、秒杀等,要对接口进行限流处理,方式短时间内的高并发请求将接口搞死;

接口熔断和降级,是为了解决系统不被拖死,不影响核心业务流程而采取的措施,比如获取用户信息列表,实时获取用户头像和昵称的接口暂时不可用(比如根据设置,10个请求里6个以上都超时,则判定为服务不可用,触发熔断机制),我们可以主动放弃调用(熔断),只返回核心数据uid等,昵称和头像暂时返回默认数据(降级);


4、消除单点,负载均衡;

对于任何一个接口服务,我们至少要有2台机器对外提供服务,禁止单点服务,单点一旦出问题,会直接造成服务不可用;

对于访问量很大的API服务,为了提供更加快速的接口响应,我们往往不是单台机器提供服务,而是有多台机器组成一个分布式集群对外提供服务。这个时候就会涉及到负载均衡,比如我们就会由nginx来做负载均衡,根据一定的策略机制,将接口请求平均的分发到不同的应用机器上进行处理和响应。进而提高接口的性能。


5、是否有第三方服务接口?

如果接口依赖了第三方服务接口,能用缓存就用缓存。这样可以进一步降低由于第三方接口不稳定给我们自己系统造成的波动。当然,也有一些第三方接口无法做缓存,比如就是要实时进行身份验证等,这个时候,超时时间的设置就尤为重要!


6、能异步处理的异步处理:

其实有很多场景下,一个接口里面的很多逻辑是可以异步处理的。举个例子:

比如用户注册场景,用户注册成功之后会给客户的邮箱发送一封激活邮件。常规的逻辑流程应该是,前段提交用户信息到注册接口,注册接口做各种校验,校验通过后,发送邮件,发送成功后,返回给前端告诉用户注册成功,请进入邮箱激活账号。其实,这个流程里的“发送邮件”就是可以拿出来异步处理的部分,当校验通过而且注册完成之后,我们把发送邮件这件事抛出去,交给另外一个就负责发邮件的任务进行处理(如我们现在有的补偿队列,或者是发一个MQ消息),然后直接返回给用户注册成功。这样,注册接口的平均响应时间一定会比第一种方案提高很多。


7、更高要求的高可用,可以采用异地机房部署;

在物理地域上就分开部署,两地同时崩溃的概率还是比较低的;


8、监控和报警;

在对接口建立高效的监控和报警机制,能够及时发现问题并通知到相应的人员进行第一时间的处理和跟进。


【稳定和安全】

1、身份验证;

在一些接口场景中,是要依赖于用户身份的,比如通过token还实现用户身份的验证;


2、接口防抓取和串改数据;

防止数据被轻易抓取到,我们可以采用https作为接口的网络传输协议,进而保证数据包不被轻易的就抓取和分析。即使这种情况下,依然被抓取到,我们还可以对传输的数据进行我们自己的加密处理,比如用对称加密算法AES或者非对称加密算法RSA,亦或是我们系统内部自己商定好的加密算法,对数据进行加密处理,这样,即使抓取到数据包,也很难分析出数据的原始信息。

对于防止数据被串改,可以使用sign验签,进一步防止接口参数被串改的可能性。


3、防刷;

接口防刷会有一些策略,根据实际的应用场景进行选择,比如增加图形验证码、接入智能验证码、时间戳限制单位时间内的调用次数、ip限制等;

另外监控很重要,及时发现异常的调用,进行封禁处理;


【其他】

1、是否需要支持跨域;

这一点是针对于H5提供接口时需要考虑的,因为一般情况下,实际的应用和接口所在的域并不是同一个域,基于浏览器的安全策略,对于XHR请求来讲,是不允许进行跨域请求的,所以,一般提供给H5的接口要支持跨域请求。当然解决跨域的方法也不在本次讨论的范围之内,目前主流的方式就是在服务器配置的header头信息中增加两项参数。



2、基于H5提供接口的一些安全性问题;

比如常见的CSRF攻击,我们可以在接口里验证 HTTP Referer字段、x-requested-with字段、header中增加token等,从一定程度上提高被CSRF攻击的门槛。 
 
3、在代码结构层面,尽量和其他部分分开;
 
API集中由同一个系统“模块”提供,尽量不要和页面等其他功能混合开发。例如下面的项目分层模式就是一个较好的实践方案:

QQ截图20181112160646.jpg

 
即所有API接口均分布在api内部,不与pc(PC站页面)、mobile(M站页面)等混合在一起。 

4、文档:
 
好的接口,还有一项优点,就是会有为之配套的接口文档。如果希望降低接口文档的维护成本等,也可以使用开源的第三方自动化接口文档工具,比如swagger等。

为什么Linux的Crond最小只支持分钟级别的定时任务?Crontab工作机制探究

服务器zkbhj 发表了文章 • 0 个评论 • 120 次浏览 • 2018-10-17 15:21 • 来自相关话题

在开发过程中,经常会有用到计划任务的时候。一般来讲,常见的大部分需求也很好实现,Linux的定时任务简单地配置到crontab里面就可以了,设置方法也很简单,如下所示,6个参数依次表示为“分、时、日、月、周和具体任务”(示例为每天凌晨3点执行freshclam工作)。
[root@student ~]# crontab -l
0 3 * * * freshclam然而,从5个时间参数来看最小是精确到某一分钟执行。如果要想实现秒级的定时任务,那么该怎么做呢? 
首先,我们先探究下Linux的Crond工作的机制是怎么样的。
 cron 是一个可以用来根据时间、日期、月份、星期的组合来调度对重复任务的执行的守护进程。

cron 假定系统持续运行。如果当某任务被调度时系统不在运行,该任务就不会被执行。

要使用 cron 服务,你必须安装了 vixie-cron RPM 软件包,而且必须在运行crond 服务。要判定该软件包是否已安装,使用 rpm -q vixie-cron 命令。要判定该服务是否在运行,使用 /sbin/service crond status 命令。
 
配置
 
cron 的主配置文件是 /etc/crontab,它包括下面几行:SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
HOME=/
# run-parts
01 * * * * root run-parts /etc/cron.hourly
02 4 * * * root run-parts /etc/cron.daily
22 4 * * 0 root run-parts /etc/cron.weekly
42 4 1 * * root run-parts /etc/cron.monthly前四行是用来配置 cron 任务运行环境的变量。SHELL 变量的值告诉系统要使用哪个 shell 环境(在这个例子里是 bash shell);PATH 变量定义用来执行命令的路径。cron 任务的输出被邮寄给MAILTO 变量定义的用户名。如果 MAILTO 变量被定义为空白字符串(MAILTO=""),电子邮件就不会被寄出。HOME 变量可以用来设置在执行命令或脚本时使用的主目录。

/etc/crontab 文件中的每一行都代表一项任务,它的格式是:
minute hour day month dayofweek command 

minute — 分钟,从 0 到 59 之间的任何整数

hour — 小时,从 0 到 23 之间的任何整数

day — 日期,从 1 到 31 之间的任何整数(如果指定了月份,必须是该月份的有效日期)

month — 月份,从 1 到 12 之间的任何整数(或使用月份的英文简写如 jan、feb 等等)

dayofweek — 星期,从 0 到 7 之间的任何整数,这里的 0 或 7 代表星期日(或使用星期的英文简写如 sun、mon 等等)

command — 要执行的命令(命令可以是ls /proc >> /tmp/proc 之类的命令,也可以是执行你自行编写的脚本的命令。)

 
在以上任何值中,星号(*)可以用来代表所有有效的值。譬如,月份值中的星号意味着在满足其它制约条件后每月都执行该命令。

整数间的短线(-)指定一个整数范围。譬如,1-4 意味着整数 1、2、3、4。

用逗号(,)隔开的一系列值指定一个列表。譬如,3, 4, 6, 8 标明这四个指定的整数。

正斜线(/)可以用来指定间隔频率。在范围后加上 /<integer> 意味着在范围内可以跳过 integer。譬如,0-59/2 可以用来在分钟字段定义每两分钟。间隔频率值还可以和星号一起使用。例如,*/3 的值可以用在月份字段中表示每三个月运行一次任务。

开头为井号(#)的行是注释,不会被处理。

如你在 /etc/crontab 文件中所见,它使用run-parts 脚本来执行 /etc/cron.hourly、/etc/cron.daily、/etc/cron.weekly和/etc/cron.monthly 目录中的脚本,这些脚本被相应地每小时、每日、每周、或每月执行。这些目录中的文件应该是 shell 脚本。

如果某 cron 任务需要根据调度来执行,而不是每小时、每日、每周、或每月地执行,它可以被添加到 /etc/cron.d 目录中。该目录中的所有文件使用和 /etc/crontab 中一样的语法。# record the memory usage of the system every monday
# at 3:30AM in the file /tmp/meminfo
30 3 * * mon cat /proc/meminfo >> /tmp/meminfo
# run custom script the first day of every month at 4:10AM
10 4 1 * * /root/scripts/backup.sh根用户以外的用户可以使用 crontab 工具来配置 cron 任务。所有用户定义的 crontab 都被保存在/var/spool/cron 目录中,并使用创建它们的用户身份来执行。要以某用户身份创建一个 crontab 项目,登录为该用户,然后键入crontab -e 命令,使用由 VISUAL 或 EDITOR环境变量指定的编辑器来编辑该用户的 crontab。该文件使用的格式和/etc/crontab 相同。当对 crontab 所做的改变被保存后,该 crontab 文件就会根据该用户名被保存,并写入文件/var/spool/cron/username 中。

cron 守护进程每分钟都检查 /etc/crontab 文件、etc/cron.d/ 目录、以及/var/spool/cron 目录中的改变。如果发现了改变,它们就会被载入内存。这样,当某个 crontab 文件改变后就不必重新启动守护进程了。
 
 控制对 cron 的使用
 
/etc/cron.allow 和/etc/cron.deny 文件被用来限制对 cron 的使用。这两个使用控制文件的格式都是每行一个用户。两个文件都不允许空格。如果使用控制文件被修改了,cron 守护进程(crond)不必被重启。使用控制文件在每次用户添加或删除一项 cron 任务时都会被读取。

无论使用控制文件中的规定如何,根用户都总是可以使用 cron。

如果 cron.allow 文件存在,只有其中列出的用户才被允许使用 cron,并且cron.deny 文件会被忽略。

如果 cron.allow 文件不存在,所有在cron.deny 中列出的用户都被禁止使用 cron。
 
启动和停止服务
 
要启动 cron 服务,使用 /sbin/service crond start 命令。要停止该服务,使用/sbin/service crond stop 命令。推荐你在引导时启动该服务。

由于Cron 是Linux的内置服务,可以用以下的方法启动、关闭这个服务:/sbin/service crond start //启动服务
/sbin/service crond stop //关闭服务
/sbin/service crond restart //重启服务
/sbin/service crond reload //重新载入配置
要实现秒级的计划任务,可以使用开源项目:Quartz
详细:http://www.quartz-scheduler.org/
 参考文章:
https://blog.csdn.net/wenqiang1208/article/details/74853519
https://blog.csdn.net/foxman209/article/details/6759920?utm_source=blogxgwz4
  查看全部
在开发过程中,经常会有用到计划任务的时候。一般来讲,常见的大部分需求也很好实现,Linux的定时任务简单地配置到crontab里面就可以了,设置方法也很简单,如下所示,6个参数依次表示为“分、时、日、月、周和具体任务”(示例为每天凌晨3点执行freshclam工作)。
[root@student ~]# crontab -l
0 3 * * * freshclam
然而,从5个时间参数来看最小是精确到某一分钟执行。如果要想实现秒级的定时任务,那么该怎么做呢? 
首先,我们先探究下Linux的Crond工作的机制是怎么样的。
 cron 是一个可以用来根据时间、日期、月份、星期的组合来调度对重复任务的执行的守护进程。

cron 假定系统持续运行。如果当某任务被调度时系统不在运行,该任务就不会被执行。

要使用 cron 服务,你必须安装了 vixie-cron RPM 软件包,而且必须在运行crond 服务。要判定该软件包是否已安装,使用 rpm -q vixie-cron 命令。要判定该服务是否在运行,使用 /sbin/service crond status 命令。
 
配置
 
cron 的主配置文件是 /etc/crontab,它包括下面几行:
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
HOME=/
# run-parts
01 * * * * root run-parts /etc/cron.hourly
02 4 * * * root run-parts /etc/cron.daily
22 4 * * 0 root run-parts /etc/cron.weekly
42 4 1 * * root run-parts /etc/cron.monthly
前四行是用来配置 cron 任务运行环境的变量。SHELL 变量的值告诉系统要使用哪个 shell 环境(在这个例子里是 bash shell);PATH 变量定义用来执行命令的路径。cron 任务的输出被邮寄给MAILTO 变量定义的用户名。如果 MAILTO 变量被定义为空白字符串(MAILTO=""),电子邮件就不会被寄出。HOME 变量可以用来设置在执行命令或脚本时使用的主目录。

/etc/crontab 文件中的每一行都代表一项任务,它的格式是:
minute         hour         day         month         dayofweek         command
 


minute — 分钟,从 0 到 59 之间的任何整数

hour — 小时,从 0 到 23 之间的任何整数

day — 日期,从 1 到 31 之间的任何整数(如果指定了月份,必须是该月份的有效日期)

month — 月份,从 1 到 12 之间的任何整数(或使用月份的英文简写如 jan、feb 等等)

dayofweek — 星期,从 0 到 7 之间的任何整数,这里的 0 或 7 代表星期日(或使用星期的英文简写如 sun、mon 等等)

command — 要执行的命令(命令可以是ls /proc >> /tmp/proc 之类的命令,也可以是执行你自行编写的脚本的命令。)


 
在以上任何值中,星号(*)可以用来代表所有有效的值。譬如,月份值中的星号意味着在满足其它制约条件后每月都执行该命令。

整数间的短线(-)指定一个整数范围。譬如,1-4 意味着整数 1、2、3、4。

用逗号(,)隔开的一系列值指定一个列表。譬如,3, 4, 6, 8 标明这四个指定的整数。

正斜线(/)可以用来指定间隔频率。在范围后加上 /<integer> 意味着在范围内可以跳过 integer。譬如,0-59/2 可以用来在分钟字段定义每两分钟。间隔频率值还可以和星号一起使用。例如,*/3 的值可以用在月份字段中表示每三个月运行一次任务。

开头为井号(#)的行是注释,不会被处理。

如你在 /etc/crontab 文件中所见,它使用run-parts 脚本来执行 /etc/cron.hourly、/etc/cron.daily、/etc/cron.weekly和/etc/cron.monthly 目录中的脚本,这些脚本被相应地每小时、每日、每周、或每月执行。这些目录中的文件应该是 shell 脚本。

如果某 cron 任务需要根据调度来执行,而不是每小时、每日、每周、或每月地执行,它可以被添加到 /etc/cron.d 目录中。该目录中的所有文件使用和 /etc/crontab 中一样的语法。
# record the memory usage of the system every monday
# at 3:30AM in the file /tmp/meminfo
30 3 * * mon cat /proc/meminfo >> /tmp/meminfo
# run custom script the first day of every month at 4:10AM
10 4 1 * * /root/scripts/backup.sh
根用户以外的用户可以使用 crontab 工具来配置 cron 任务。所有用户定义的 crontab 都被保存在/var/spool/cron 目录中,并使用创建它们的用户身份来执行。要以某用户身份创建一个 crontab 项目,登录为该用户,然后键入crontab -e 命令,使用由 VISUAL 或 EDITOR环境变量指定的编辑器来编辑该用户的 crontab。该文件使用的格式和/etc/crontab 相同。当对 crontab 所做的改变被保存后,该 crontab 文件就会根据该用户名被保存,并写入文件/var/spool/cron/username 中。

cron 守护进程每分钟都检查 /etc/crontab 文件、etc/cron.d/ 目录、以及/var/spool/cron 目录中的改变。如果发现了改变,它们就会被载入内存。这样,当某个 crontab 文件改变后就不必重新启动守护进程了。
 
 控制对 cron 的使用
 
/etc/cron.allow 和/etc/cron.deny 文件被用来限制对 cron 的使用。这两个使用控制文件的格式都是每行一个用户。两个文件都不允许空格。如果使用控制文件被修改了,cron 守护进程(crond)不必被重启。使用控制文件在每次用户添加或删除一项 cron 任务时都会被读取。

无论使用控制文件中的规定如何,根用户都总是可以使用 cron。

如果 cron.allow 文件存在,只有其中列出的用户才被允许使用 cron,并且cron.deny 文件会被忽略。

如果 cron.allow 文件不存在,所有在cron.deny 中列出的用户都被禁止使用 cron。
 
启动和停止服务
 
要启动 cron 服务,使用 /sbin/service crond start 命令。要停止该服务,使用/sbin/service crond stop 命令。推荐你在引导时启动该服务。

由于Cron 是Linux的内置服务,可以用以下的方法启动、关闭这个服务:
/sbin/service crond start //启动服务
/sbin/service crond stop //关闭服务
/sbin/service crond restart //重启服务
/sbin/service crond reload //重新载入配置

要实现秒级的计划任务,可以使用开源项目:Quartz
详细:http://www.quartz-scheduler.org/
 参考文章:
https://blog.csdn.net/wenqiang1208/article/details/74853519
https://blog.csdn.net/foxman209/article/details/6759920?utm_source=blogxgwz4
 

政府发布的白皮书是什么?

专业名词zkbhj 发表了文章 • 0 个评论 • 191 次浏览 • 2018-09-25 19:28 • 来自相关话题

最近,我被一个老词汇所困扰,那就是在公司里常常见到的“白皮书”一词。与此对应的,我们还经常听到、看到“蓝皮书”、“红皮书”,还有“桔皮书”,等等,这些书都是什么意思,他们之间有区别吗?
和同事争论的时候,有的说白皮书就是产品介绍文档,有的说不是。那么在技术型公司里,白皮书究竟指的是什么呢?
为此,我到网上查找,中文的网页百度百科说:“白皮书”是政府或议会正式发表的以白色封面装帧的重要文件或报告书的别称。这多少让我有点失望,它并没有谈及商业上的白皮书。
后来又找到了维基百科,里面对白皮书是这样解释的,这多少让我找到了感觉。
一个白皮书(英文White paper或whitepaper)是一个权威性的报告或指南,常常阐述问题本身及其解决方法。白皮书被用来教育读者并帮助人们作出决定。常被用在政治、商业和技术领域。在商业领域,这个术语指的是作为市场或销售工具的商业文档。

政府白皮书

在英联邦国家,白皮书是议会文件的一种非正式名称,用来阐述政府政策;在英国,这些文件常被称为命令文件(Command Papers)。白皮书由政府发行,阐明在现时的问题上的政策,或建议采取的行动。尽管白皮书可能有时作为新法立法前的细节咨询,但是,透过白皮书,确实能看出政府有着清晰的意图:他们欲通过一部新法。
由欧洲委员会发表的白皮书,是欧盟在某一特定领域采取行动的建议文稿。他们有时紧随绿皮书发布其后,开展公众咨询过程。

这里有几个例子:
俄罗斯1号,关于俄罗斯布尔什维主义的报告汇编,1919年4月。这个报告汇编常被称作“白皮书”(The White Paper),是一本关于布尔什维克革命的电报信息的汇编,由英国在俄罗斯的官员撰写。丘吉尔白皮书,1922,计划为犹太人在巴勒斯坦设立一个国家1939年白皮书,呼吁建立统一的巴勒斯坦国,并限制犹太人移民和购买土地的能力。全民就业白皮书,1945。澳大利亚联邦承认让人民工作是国家的义务。国防白皮书,1964,导致创立并统一了现代化的加拿大军队。1966国防白皮书,取消了英国新航母和BAC TSR-2战术打击飞机。In Place of Strife, 1969 (后被弃)减少工会的权利。1969白皮书,1969(后被弃),在加拿大废弃“印第安人法”, 印第安人象加拿大其他少数民族一样被承认是这个国家的原住民,而不是特殊一族。白皮书,1966,美国国家研究理事会文件,导致在美国开发紧急医疗服务。
商业白皮书

自上世纪90年代早期,白皮书一词也用来指商业性的文件,作为市场宣传和销售的工具。这类白皮书阐明:某类技术或产品为解决特定领域问题带来的好处。
这类白皮书几乎总是用作市场沟通,目的是推销公司的解决方案和产品。作为市场宣传的工具,这些文件将突出对公司有利的信息。这样的白皮书常被用来发现销售线索、标榜领先地位、争取业务单子或纯粹是为了教育客户。

有三种主要的商业白皮书:
业务-利益:为某一特定的技术或方法争取业务定单。技术:描述一个技术是如何工作的。上面两者的混合:在一个文件里,将宏观的商业利益和微观的技术细节结合在一起。 查看全部
最近,我被一个老词汇所困扰,那就是在公司里常常见到的“白皮书”一词。与此对应的,我们还经常听到、看到“蓝皮书”、“红皮书”,还有“桔皮书”,等等,这些书都是什么意思,他们之间有区别吗?
和同事争论的时候,有的说白皮书就是产品介绍文档,有的说不是。那么在技术型公司里,白皮书究竟指的是什么呢?
为此,我到网上查找,中文的网页百度百科说:“白皮书”是政府或议会正式发表的以白色封面装帧的重要文件或报告书的别称。这多少让我有点失望,它并没有谈及商业上的白皮书。
后来又找到了维基百科,里面对白皮书是这样解释的,这多少让我找到了感觉。
一个白皮书(英文White paper或whitepaper)是一个权威性的报告或指南,常常阐述问题本身及其解决方法。白皮书被用来教育读者并帮助人们作出决定。常被用在政治、商业和技术领域。在商业领域,这个术语指的是作为市场或销售工具的商业文档。

政府白皮书

在英联邦国家,白皮书是议会文件的一种非正式名称,用来阐述政府政策;在英国,这些文件常被称为命令文件(Command Papers)。白皮书由政府发行,阐明在现时的问题上的政策,或建议采取的行动。尽管白皮书可能有时作为新法立法前的细节咨询,但是,透过白皮书,确实能看出政府有着清晰的意图:他们欲通过一部新法。
由欧洲委员会发表的白皮书,是欧盟在某一特定领域采取行动的建议文稿。他们有时紧随绿皮书发布其后,开展公众咨询过程。

这里有几个例子:
  • 俄罗斯1号,关于俄罗斯布尔什维主义的报告汇编,1919年4月。这个报告汇编常被称作“白皮书”(The White Paper),是一本关于布尔什维克革命的电报信息的汇编,由英国在俄罗斯的官员撰写。
  • 丘吉尔白皮书,1922,计划为犹太人在巴勒斯坦设立一个国家
  • 1939年白皮书,呼吁建立统一的巴勒斯坦国,并限制犹太人移民和购买土地的能力。
  • 全民就业白皮书,1945。澳大利亚联邦承认让人民工作是国家的义务。
  • 国防白皮书,1964,导致创立并统一了现代化的加拿大军队。
  • 1966国防白皮书,取消了英国新航母和BAC TSR-2战术打击飞机。
  • In Place of Strife, 1969 (后被弃)减少工会的权利。
  • 1969白皮书,1969(后被弃),在加拿大废弃“印第安人法”, 印第安人象加拿大其他少数民族一样被承认是这个国家的原住民,而不是特殊一族。
  • 白皮书,1966,美国国家研究理事会文件,导致在美国开发紧急医疗服务。

商业白皮书

自上世纪90年代早期,白皮书一词也用来指商业性的文件,作为市场宣传和销售的工具。这类白皮书阐明:某类技术或产品为解决特定领域问题带来的好处。
这类白皮书几乎总是用作市场沟通,目的是推销公司的解决方案和产品。作为市场宣传的工具,这些文件将突出对公司有利的信息。这样的白皮书常被用来发现销售线索、标榜领先地位、争取业务单子或纯粹是为了教育客户。

有三种主要的商业白皮书:
  • 业务-利益:为某一特定的技术或方法争取业务定单。
  • 技术:描述一个技术是如何工作的。
  • 上面两者的混合:在一个文件里,将宏观的商业利益和微观的技术细节结合在一起。

JWT:完全前后端分离的项目如何做用户身份验证更安全?看这篇就够了!

前端开发zkbhj 发表了文章 • 0 个评论 • 281 次浏览 • 2018-09-19 14:48 • 来自相关话题

在前后端分离开发时为什么需要用户认证呢?原因是由于HTTP协定是不储存状态的(stateless),这意味着当我们透过帐号密码验证一个使用者时,当下一个request请求时它就把刚刚的资料忘了。于是我们的程序就不知道谁是谁,就要再验证一次。所以为了保证系统安全,我们就需要验证用户否处于登录状态。

传统方式

前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个 token 和对应的用户id到数据库或Session中,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。

但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。

在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。

httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为 cookie 默认被发了出去。

另外,如果将验证信息保存在数据库中,后端每次都需要根据token查出用户id,这就增加了数据库的查询和存储开销。若把验证信息保存在session中,有加大了服务器端的存储压力。那我们可不可以不要服务器去查询呢?如果我们生成token遵循一定的规律,比如我们使用对称加密算法来加密用户id形成token,那么服务端以后其实只要解密该token就可以知道用户的id是什么了。不过呢,我只是举个例子而已,要是真这么做,只要你的对称加密算法泄露了,其他人可以通过这种加密方式进行伪造token,那么所有用户信息都不再安全了。恩,那用非对称加密算法来做呢,其实现在有个规范就是这样做的,就是我们接下来要介绍的 JWT。

Json Web Token(JWT)

JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:

简洁(Compact)

可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快

自包含(Self-contained)

负载中包含了所有用户所需要的信息,避免了多次查询数据库


JWT 组成





Header 头部

头部包含了两部分,token 类型和采用的加密算法
 {
"alg": "HS256",
"typ": "JWT"
}它会使用 Base64 编码组成 JWT 结构的第一部分,如果你使用Node.js,可以用Node.js的包base64url来得到这个字符串。

Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

Payload 负载

这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。
 {
"iss": "lion1ou JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "lion1ou@163.com"
}同样的,它会使用 Base64 编码组成 JWT 结构的第二部分

Signature 签名

前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

三个部分通过.连接在一起就是我们的 JWT 了,它可能长这个样子,长度貌似和你的加密算法和私钥有关系。eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s

其实到这一步可能就有人会想了,HTTP 请求总会带上 token,这样这个 token 传来传去占用不必要的带宽啊。如果你这么想了,那你可以去了解下 HTTP2,HTTP2 对头部进行了压缩,相信也解决了这个问题。

签名的目的

最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

信息暴露

在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?

是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。

因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。

JWT 使用




 
首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

和Session方式存储id的差异

Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。具体是否采用,需要在不同场景下用数据说话。

单点登录

Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:www.taobao.com,nv.taobao.com,nz.taobao.com,login.taobao.com。所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。

总结

JWT的主要作用在于(一)可附带用户信息,后端直接通过JWT获取相关信息。(二)使用本地保存,通过HTTP Header中的Authorization位提交验证。但其实关于JWT存放到哪里一直有很多讨论,有人说存放到本地存储,有人说存 cookie。个人偏向于放在本地存储,如果你有什么意见和看法欢迎提出。

参考文档:
https://segmentfault.com/a/1190000005783306 
https://ruiming.me/authentication-of-frontend-backend-separate-application/ 
 
总结和摘录自:
https://blog.csdn.net/kevin_lc ... 46723 查看全部
在前后端分离开发时为什么需要用户认证呢?原因是由于HTTP协定是不储存状态的(stateless),这意味着当我们透过帐号密码验证一个使用者时,当下一个request请求时它就把刚刚的资料忘了。于是我们的程序就不知道谁是谁,就要再验证一次。所以为了保证系统安全,我们就需要验证用户否处于登录状态。

传统方式

前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个 token 和对应的用户id到数据库或Session中,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。

但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。

在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。

httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为 cookie 默认被发了出去。

另外,如果将验证信息保存在数据库中,后端每次都需要根据token查出用户id,这就增加了数据库的查询和存储开销。若把验证信息保存在session中,有加大了服务器端的存储压力。那我们可不可以不要服务器去查询呢?如果我们生成token遵循一定的规律,比如我们使用对称加密算法来加密用户id形成token,那么服务端以后其实只要解密该token就可以知道用户的id是什么了。不过呢,我只是举个例子而已,要是真这么做,只要你的对称加密算法泄露了,其他人可以通过这种加密方式进行伪造token,那么所有用户信息都不再安全了。恩,那用非对称加密算法来做呢,其实现在有个规范就是这样做的,就是我们接下来要介绍的 JWT。

Json Web Token(JWT)

JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:


简洁(Compact)

可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快

自包含(Self-contained)

负载中包含了所有用户所需要的信息,避免了多次查询数据库



JWT 组成

006tNc79gy1fbv54tfilmj31120b2wl9.jpg

Header 头部

头部包含了两部分,token 类型和采用的加密算法
 
{
"alg": "HS256",
"typ": "JWT"
}
它会使用 Base64 编码组成 JWT 结构的第一部分,如果你使用Node.js,可以用Node.js的包base64url来得到这个字符串。


Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。


Payload 负载

这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。
 
{
"iss": "lion1ou JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "lion1ou@163.com"
}
同样的,它会使用 Base64 编码组成 JWT 结构的第二部分

Signature 签名

前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

三个部分通过.连接在一起就是我们的 JWT 了,它可能长这个样子,长度貌似和你的加密算法和私钥有关系。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s

其实到这一步可能就有人会想了,HTTP 请求总会带上 token,这样这个 token 传来传去占用不必要的带宽啊。如果你这么想了,那你可以去了解下 HTTP2,HTTP2 对头部进行了压缩,相信也解决了这个问题。

签名的目的

最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

信息暴露

在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?

是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。

因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。

JWT 使用

006tNc79gy1fbv63pzqocj30pj0h8t9m.jpg
 
  1. 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
  2. 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。
  3. 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
  4. 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
  5. 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
  6. 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。


和Session方式存储id的差异

Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。具体是否采用,需要在不同场景下用数据说话。

单点登录

Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:www.taobao.com,nv.taobao.com,nz.taobao.com,login.taobao.com。所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。

总结

JWT的主要作用在于(一)可附带用户信息,后端直接通过JWT获取相关信息。(二)使用本地保存,通过HTTP Header中的Authorization位提交验证。但其实关于JWT存放到哪里一直有很多讨论,有人说存放到本地存储,有人说存 cookie。个人偏向于放在本地存储,如果你有什么意见和看法欢迎提出。

参考文档:
https://segmentfault.com/a/1190000005783306 
https://ruiming.me/authentication-of-frontend-backend-separate-application/ 
 
总结和摘录自:
https://blog.csdn.net/kevin_lc ... 46723

PHP升级7.2之后需要注意的“坑”

PHPzkbhj 发表了文章 • 0 个评论 • 285 次浏览 • 2018-09-12 20:25 • 来自相关话题

最近升级了PHP版本,从7.1升级到7.2,升级前版本:
PHP 7.1.14 (cli) (built: Feb 2 2018 08:42:59) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.1.14, Copyright (c) 1999-2018, by Zend Technologies
with Xdebug v2.6.0, Copyright (c) 2002-2018, by Derick Rethans升级后版本:
PHP 7.2.2 (cli) (built: Feb 24 2018 17:51:12) ( ZTS DEBUG )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.2.2, Copyright (c) 1999-2018, by Zend Technologies升级完成之后发现有几个框架在使用时都出现了问题,主要原因集中在7.2之后废弃了一些功能,下面列出几个常见的问题:

1、each函数已被废弃:

之前版本写法:<?php
$array = array();
each($array);

// Deprecated: The each() function is deprecated. This message will be suppressed on further calls在7.2版本中会提示过时,可以使用foreach替代each方法,也可以自己修改each方法替代:
<?php
function func_new_each(&$array){
$res = array();
$key = key($array);
if($key !== null){
next($array);
$res[1] = $res['value'] = $array[$key];
$res[0] = $res['key'] = $key;
}else{
$res = false;
}
return $res;
}
2、当传递一个无效参数时,count()函数将抛出warning警告:

之前版本写法<?php
count('');

// Warning: count(): Parameter must be an array or an object that implements Countable在7.2版本中将严格执行类型区分,参数类型不正确,将会出现警告,所以需要在使用count方法时注意参数的值,不过也可以通过自己修改方法来替代(不建议):
<?php
function func_new_count($array_or_countable,$mode = COUNT_NORMAL){
if(is_array($array_or_countable) || is_object($array_or_countable)){
return count($array_or_countable, $mode);
}else{
return 0;
}
}3、create_function被废弃,可以用匿名函数来代替:

之前版本写法:<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New anonymous function: $newfunc\n";
echo $newfunc(2, M_E) . "\n";
// outputs
// New anonymous function: lambda_1
// ln(2) + ln(2.718281828459) = 1.6931471805599

// Warning This function has been DEPRECATED as of PHP 7.2.0. Relying on this function is highly discouraged.在7.2版本中会有警告提示,可修改为匿名函数来替代:
<?php
$newfunc = function ($a,$b){
return "ln($a) + ln($b) = " . log($a * $b);
};
echo $newfunc(2, M_E) . "\n";以上就是升级之后暂时遇到的几个问题,其它相关修改可详看链家产品技术团队做的翻译及整理:PHP7.2 版本指南
https://mp.weixin.qq.com/s/60pohj2n7Pxba3G9vY92yg 查看全部
最近升级了PHP版本,从7.1升级到7.2,升级前版本:
PHP 7.1.14 (cli) (built: Feb  2 2018 08:42:59) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.1.14, Copyright (c) 1999-2018, by Zend Technologies
with Xdebug v2.6.0, Copyright (c) 2002-2018, by Derick Rethans
升级后版本:
PHP 7.2.2 (cli) (built: Feb 24 2018 17:51:12) ( ZTS DEBUG )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.2.2, Copyright (c) 1999-2018, by Zend Technologies
升级完成之后发现有几个框架在使用时都出现了问题,主要原因集中在7.2之后废弃了一些功能,下面列出几个常见的问题:

1、each函数已被废弃:

之前版本写法:
<?php
$array = array();
each($array);

// Deprecated: The each() function is deprecated. This message will be suppressed on further calls
在7.2版本中会提示过时,可以使用foreach替代each方法,也可以自己修改each方法替代:
<?php
function func_new_each(&$array){
$res = array();
$key = key($array);
if($key !== null){
next($array);
$res[1] = $res['value'] = $array[$key];
$res[0] = $res['key'] = $key;
}else{
$res = false;
}
return $res;
}

2、当传递一个无效参数时,count()函数将抛出warning警告:

之前版本写法
<?php
count('');

// Warning: count(): Parameter must be an array or an object that implements Countable
在7.2版本中将严格执行类型区分,参数类型不正确,将会出现警告,所以需要在使用count方法时注意参数的值,不过也可以通过自己修改方法来替代(不建议):
<?php
function func_new_count($array_or_countable,$mode = COUNT_NORMAL){
if(is_array($array_or_countable) || is_object($array_or_countable)){
return count($array_or_countable, $mode);
}else{
return 0;
}
}
3、create_function被废弃,可以用匿名函数来代替:

之前版本写法:
<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New anonymous function: $newfunc\n";
echo $newfunc(2, M_E) . "\n";
// outputs
// New anonymous function: lambda_1
// ln(2) + ln(2.718281828459) = 1.6931471805599

// Warning This function has been DEPRECATED as of PHP 7.2.0. Relying on this function is highly discouraged.
在7.2版本中会有警告提示,可修改为匿名函数来替代:
<?php
$newfunc = function ($a,$b){
return "ln($a) + ln($b) = " . log($a * $b);
};
echo $newfunc(2, M_E) . "\n";
以上就是升级之后暂时遇到的几个问题,其它相关修改可详看链家产品技术团队做的翻译及整理:PHP7.2 版本指南
https://mp.weixin.qq.com/s/60pohj2n7Pxba3G9vY92yg

海量数据相似度计算算法:simhash和海明距离

架构思想zkbhj 发表了文章 • 0 个评论 • 95 次浏览 • 2018-09-12 16:04 • 来自相关话题

通过 采集系统 我们采集了大量文本数据,但是文本中有很多重复数据影响我们对于结果的分析。分析前我们需要对这些数据去除重复,如何选择和设计文本的去重算法?常见的有余弦夹角算法、欧式距离、Jaccard相似度、最长公共子串、编辑距离等。这些算法对于待比较的文本数据不多时还比较好用,如果我们的爬虫每天采集的数据以千万计算,我们如何对于这些海量千万级的数据进行高效的合并去重。最简单的做法是拿着待比较的文本和数据库中所有的文本比较一遍如果是重复的数据就标示为重复。看起来很简单,我们来做个测试,就拿最简单的两个数据使用Apache提供的 Levenshtein for 循环100w次计算这两个数据的相似度。代码结果如下:
String s1 = "你妈妈喊你回家吃饭哦,回家罗回家罗" ;
String s2 = "你妈妈叫你回家吃饭啦,回家罗回家罗" ;

long t1 = System.currentTimeMillis();

for (int i = 0; i < 1000000; i++) {
int dis = StringUtils .getLevenshteinDistance(s1, s2);
}

long t2 = System.currentTimeMillis();

System. out .println(" 耗费时间: " + (t2 - t1) + " ms ");耗费时间: 4266 ms

大跌眼镜,居然计算耗费4秒。假设我们一天需要比较100w次,光是比较100w次的数据是否重复就需要4s,就算4s一个文档,单线程一分钟才处理15个文档,一个小时才900个,一天也才21600个文档,这个数字和一天100w相差甚远,需要多少机器和资源才能解决。

为此我们需要一种应对于海量数据场景的去重方案,经过研究发现有种叫 local sensitive hash 局部敏感哈希 的东西,据说这玩意可以把文档降维到hash数字,数字两两计算运算量要小很多。查找很多文档后看到google对于网页去重使用的是simhash,他们每天需要处理的文档在亿级别,大大超过了我们现在文档的水平。既然老大哥也有类似的应用,我们也赶紧尝试下。simhash是由 Charikar 在2002年提出来的,参考 《Similarity estimation techniques from rounding algorithms》 。 介绍下这个算法主要原理,为了便于理解尽量不使用数学公式,分为这几步:
 

1、分词,把需要判断文本分词形成这个文章的特征单词。最后形成去掉噪音词的单词序列并为每个词加上权重,我们假设权重分为5个级别(1~5)。比如:“ 美国“51区”雇员称内部有9架飞碟,曾看见灰色外星人 ” ==> 分词后为 “ 美国(4) 51区(5) 雇员(3) 称(1) 内部(2) 有(1) 9架(3) 飞碟(5) 曾(1) 看见(3) 灰色(4) 外星人(5)”,括号里是代表单词在整个句子里重要程度,数字越大越重要。

2、hash,通过hash算法把每个词变成hash值,比如“美国”通过hash算法计算为 100101,“51区”通过hash算法计算为 101011。这样我们的字符串就变成了一串串数字,还记得文章开头说过的吗,要把文章变为数字计算才能提高相似度计算性能,现在是降维过程进行时。

3、加权,通过 2步骤的hash生成结果,需要按照单词的权重形成加权数字串,比如“美国”的hash值为“100101”,通过加权计算为“4 -4 -4 4 -4 4”;“51区”的hash值为“101011”,通过加权计算为 “ 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”。

 
整个过程图为:





 
大家可能会有疑问,经过这么多步骤搞这么麻烦,不就是为了得到个 0 1 字符串吗?我直接把这个文本作为字符串输入,用hash函数生成 0 1 值更简单。其实不是这样的,传统hash函数解决的是生成唯一值,比如 md5、hashmap等。md5是用于生成唯一签名串,只要稍微多加一个字符md5的两个数字看起来相差甚远;hashmap也是用于键值对查找,便于快速插入和查找的数据结构。不过我们主要解决的是文本相似度计算,要比较的是两个文章是否相识,当然我们降维生成了hashcode也是用于这个目的。看到这里估计大家就明白了,我们使用的simhash就算把文章中的字符串变成 01 串也还是可以用于计算相似度的,而传统的hashcode却不行。我们可以来做个测试,两个相差只有一个字符的文本串,“你妈妈喊你回家吃饭哦,回家罗回家罗” 和 “你妈妈叫你回家吃饭啦,回家罗回家罗”。

通过simhash计算结果为:

1000010010101101111111100000101011010001001111100001001011001011

1000010010101101011111100000101011010001001111100001101010001011

通过 hashcode计算为:

1111111111111111111111111111111110001000001100110100111011011110

1010010001111111110010110011101

大家可以看得出来,相似的文本只有部分 01 串变化了,而普通的hashcode却不能做到,这个就是局部敏感哈希的魅力。目前Broder提出的shingling算法和Charikar的simhash算法应该算是业界公认比较好的算法。在simhash的发明人Charikar的论文中并没有给出具体的simhash算法和证明,“量子图灵”得出的证明simhash是由随机超平面hash算法演变而来的。

现在通过这样的转换,我们把库里的文本都转换为simhash 代码,并转换为long类型存储,空间大大减少。现在我们虽然解决了空间,但是如何计算两个simhash的相似度呢?难道是比较两个simhash的01有多少个不同吗?对的,其实也就是这样,我们通过海明距离(Hamming distance)就可以计算出两个simhash到底相似不相似。两个simhash对应二进制(01串)取值不同的数量称为这两个simhash的海明距离。举例如下: 10101 和 00110 从第一位开始依次有第一位、第四、第五位不同,则海明距离为3。对于二进制字符串的a和b,海明距离为等于在a XOR b运算结果中1的个数(普遍算法)。

为了高效比较,我们预先加载了库里存在文本并转换为simhash code 存储在内存空间。来一条文本先转换为 simhash code,然后和内存里的simhash code 进行比较,测试100w次计算在100ms。速度大大提升。

未完待续:

1、目前速度提升了但是数据是不断增量的,如果未来数据发展到一个小时100w,按现在一次100ms,一个线程处理一秒钟 10次,一分钟 60 * 10 次,一个小时 60*10 *60 次 = 36000次,一天 60*10*60*24 = 864000次。 我们目标是一天100w次,通过增加两个线程就可以完成。但是如果要一个小时100w次呢?则需要增加30个线程和相应的硬件资源保证速度能够达到,这样成本也上去了。能否有更好的办法,提高我们比较的效率?

2、通过大量测试,simhash用于比较大文本,比如500字以上效果都还蛮好,距离小于3的基本都是相似,误判率也比较低。但是如果我们处理的是微博信息,最多也就140个字,使用simhash的效果并不那么理想。看如下图,在距离为3时是一个比较折中的点,在距离为10时效果已经很差了,不过我们测试短文本很多看起来相似的距离确实为10。如果使用距离为3,短文本大量重复信息不会被过滤,如果使用距离为10,长文本的错误率也非常高,如何解决?




  查看全部
通过 采集系统 我们采集了大量文本数据,但是文本中有很多重复数据影响我们对于结果的分析。分析前我们需要对这些数据去除重复,如何选择和设计文本的去重算法?常见的有余弦夹角算法、欧式距离、Jaccard相似度、最长公共子串、编辑距离等。这些算法对于待比较的文本数据不多时还比较好用,如果我们的爬虫每天采集的数据以千万计算,我们如何对于这些海量千万级的数据进行高效的合并去重。最简单的做法是拿着待比较的文本和数据库中所有的文本比较一遍如果是重复的数据就标示为重复。看起来很简单,我们来做个测试,就拿最简单的两个数据使用Apache提供的 Levenshtein for 循环100w次计算这两个数据的相似度。代码结果如下:
String s1 = "你妈妈喊你回家吃饭哦,回家罗回家罗" ;
String s2 = "你妈妈叫你回家吃饭啦,回家罗回家罗" ;

long t1 = System.currentTimeMillis();

for (int i = 0; i < 1000000; i++) {
int dis = StringUtils .getLevenshteinDistance(s1, s2);
}

long t2 = System.currentTimeMillis();

System. out .println(" 耗费时间: " + (t2 - t1) + " ms ");
耗费时间: 4266 ms

大跌眼镜,居然计算耗费4秒。假设我们一天需要比较100w次,光是比较100w次的数据是否重复就需要4s,就算4s一个文档,单线程一分钟才处理15个文档,一个小时才900个,一天也才21600个文档,这个数字和一天100w相差甚远,需要多少机器和资源才能解决。

为此我们需要一种应对于海量数据场景的去重方案,经过研究发现有种叫 local sensitive hash 局部敏感哈希 的东西,据说这玩意可以把文档降维到hash数字,数字两两计算运算量要小很多。查找很多文档后看到google对于网页去重使用的是simhash,他们每天需要处理的文档在亿级别,大大超过了我们现在文档的水平。既然老大哥也有类似的应用,我们也赶紧尝试下。simhash是由 Charikar 在2002年提出来的,参考 《Similarity estimation techniques from rounding algorithms》 。 介绍下这个算法主要原理,为了便于理解尽量不使用数学公式,分为这几步:
 


1、分词,把需要判断文本分词形成这个文章的特征单词。最后形成去掉噪音词的单词序列并为每个词加上权重,我们假设权重分为5个级别(1~5)。比如:“ 美国“51区”雇员称内部有9架飞碟,曾看见灰色外星人 ” ==> 分词后为 “ 美国(4) 51区(5) 雇员(3) 称(1) 内部(2) 有(1) 9架(3) 飞碟(5) 曾(1) 看见(3) 灰色(4) 外星人(5)”,括号里是代表单词在整个句子里重要程度,数字越大越重要。

2、hash,通过hash算法把每个词变成hash值,比如“美国”通过hash算法计算为 100101,“51区”通过hash算法计算为 101011。这样我们的字符串就变成了一串串数字,还记得文章开头说过的吗,要把文章变为数字计算才能提高相似度计算性能,现在是降维过程进行时。

3、加权,通过 2步骤的hash生成结果,需要按照单词的权重形成加权数字串,比如“美国”的hash值为“100101”,通过加权计算为“4 -4 -4 4 -4 4”;“51区”的hash值为“101011”,通过加权计算为 “ 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”。


 
整个过程图为:

simhash.png

 
大家可能会有疑问,经过这么多步骤搞这么麻烦,不就是为了得到个 0 1 字符串吗?我直接把这个文本作为字符串输入,用hash函数生成 0 1 值更简单。其实不是这样的,传统hash函数解决的是生成唯一值,比如 md5、hashmap等。md5是用于生成唯一签名串,只要稍微多加一个字符md5的两个数字看起来相差甚远;hashmap也是用于键值对查找,便于快速插入和查找的数据结构。不过我们主要解决的是文本相似度计算,要比较的是两个文章是否相识,当然我们降维生成了hashcode也是用于这个目的。看到这里估计大家就明白了,我们使用的simhash就算把文章中的字符串变成 01 串也还是可以用于计算相似度的,而传统的hashcode却不行。我们可以来做个测试,两个相差只有一个字符的文本串,“你妈妈喊你回家吃饭哦,回家罗回家罗” 和 “你妈妈叫你回家吃饭啦,回家罗回家罗”。

通过simhash计算结果为:

1000010010101101111111100000101011010001001111100001001011001011

1000010010101101011111100000101011010001001111100001101010001011

通过 hashcode计算为:

1111111111111111111111111111111110001000001100110100111011011110

1010010001111111110010110011101

大家可以看得出来,相似的文本只有部分 01 串变化了,而普通的hashcode却不能做到,这个就是局部敏感哈希的魅力。目前Broder提出的shingling算法和Charikar的simhash算法应该算是业界公认比较好的算法。在simhash的发明人Charikar的论文中并没有给出具体的simhash算法和证明,“量子图灵”得出的证明simhash是由随机超平面hash算法演变而来的。

现在通过这样的转换,我们把库里的文本都转换为simhash 代码,并转换为long类型存储,空间大大减少。现在我们虽然解决了空间,但是如何计算两个simhash的相似度呢?难道是比较两个simhash的01有多少个不同吗?对的,其实也就是这样,我们通过海明距离(Hamming distance)就可以计算出两个simhash到底相似不相似。两个simhash对应二进制(01串)取值不同的数量称为这两个simhash的海明距离。举例如下: 10101 和 00110 从第一位开始依次有第一位、第四、第五位不同,则海明距离为3。对于二进制字符串的a和b,海明距离为等于在a XOR b运算结果中1的个数(普遍算法)。

为了高效比较,我们预先加载了库里存在文本并转换为simhash code 存储在内存空间。来一条文本先转换为 simhash code,然后和内存里的simhash code 进行比较,测试100w次计算在100ms。速度大大提升。

未完待续:

1、目前速度提升了但是数据是不断增量的,如果未来数据发展到一个小时100w,按现在一次100ms,一个线程处理一秒钟 10次,一分钟 60 * 10 次,一个小时 60*10 *60 次 = 36000次,一天 60*10*60*24 = 864000次。 我们目标是一天100w次,通过增加两个线程就可以完成。但是如果要一个小时100w次呢?则需要增加30个线程和相应的硬件资源保证速度能够达到,这样成本也上去了。能否有更好的办法,提高我们比较的效率?

2、通过大量测试,simhash用于比较大文本,比如500字以上效果都还蛮好,距离小于3的基本都是相似,误判率也比较低。但是如果我们处理的是微博信息,最多也就140个字,使用simhash的效果并不那么理想。看如下图,在距离为3时是一个比较折中的点,在距离为10时效果已经很差了,不过我们测试短文本很多看起来相似的距离确实为10。如果使用距离为3,短文本大量重复信息不会被过滤,如果使用距离为10,长文本的错误率也非常高,如何解决?
simhash2.png