lanffy

lanffy 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 lanffy.github.io/ 编辑
编辑

只有想不到的,没有技术实现不了的.

个人动态

lanffy 收藏了文章 · 1月6日

如何校验 email 地址以提高邮件送达率

背景

在发送 email 的时候,如果邮件收件人是一个不存在的 email 账号、或者收件人账号存在问题、收件箱无法接收 email, 那么 email server 就会将该无法接收的信息响应回来, 这种情况称之为 bounce email,对应的衡量指标是 bounce 率。bounce email 是影响邮件送达率(email delivery rate)的一个重要因素。根据 Sendgrid 统计结果, bounce 率在 5% 以上,送达率为71%;但如果 bounce 率在2%或以下,平均送达率可以提高到91%。

目前我们平台每个月邮件发送量在千万封左右,包括通知类和营销类邮件,其中 marketing campaigns 占了大部分。 因为 marketing campaigns 会让客户自定义 contacts,这部分是潜在 bounce email 的一个风险,所以在发送邮件前检测收件人 email 地址是否可送达,过滤掉其中的垃圾和无效的 email 地址,可以有效减少 bounce rate。这篇文章我们会详细介绍如何通过校验 email 地址以及最佳实践 , 来提高邮件送达率。

为什么 Bounce 影响 email 送达率

上面 Sendgrid 对 bounce email 的统计数据, 可以较明显地看出 bounce 率和送达率的相关关系。但其中的相关性不仅仅只是 bounce 占了总发送邮件数的比例大才影响送达率,而是 bounce 率高会进而影响到正常用户的邮件送达。

每一个 email 账号都有一个发件人信誉的分数(reputation),来帮助收件人的 email 服务提供商(ESP)决定邮件的质量。分数越高,邮件的送达率也会越高,反之亦然。如果频繁的 bounce ,会导致收件的 Email Server “质疑” 发件人邮箱账号是否为真实账号,当到达一定程度, 该 sender 账号会被列入各种 ESP 的垃圾邮件索引,最后发送给其他用户就会被 blocked。并且 bounce 会影响发件人 domain 和 ip 的 reputation。

所以 email bounces 可以说是 marketing campaigns 的一个“噩梦”。校验 email 地址有助于将邮件发送给正确的收件人,同时使 email 帐户保持可用状态,并提高 reputation。对业务来说,也会提升 email campaign 的质量。

如何校验 email 地址

完整的 email 地址的校验过程主要包括以下4个维度:

  1. 语法检查
  2. 检查是否为一次性邮箱(disposable)
  3. 确认 domain 对应 DNS 记录是否存在
  4. Ping 收件人邮箱

语法检查

拼写的语法错误是 email 地址检查最常见的问题之一。 根据常用的 email 地址正则表达式,可以确认出地址是否有格式问题。一般检查的表达式类似于 abc@aftership.com, 包括3个部分: local part 、@分隔符 和 domain。

较重点检查的是 local part 部分,由以下3部分组成:

  • 字母数字 – A 到 Z(包括大小写), 0-9
  • 可打印字符 – !#$%&'*+-/=?^_~`
  • 一个标点符号. – 但是 local part 不能以 . 开头或结尾、或者连续使用,比如 example..dot@aftership.com是一个非法的 email 地址。

不同的 email 服务提供商对 local part 有不同的规定,这里 mailgun 提供了一份常见 ESP 的 校验规则

domain 跟对域名的命名约定是一致的:包括只使用字母数字和-符号且不能连续使用。

除了根据正则表达式对 email 地址做检查,还需要考虑的一些点是 IETF 标准non-ASCII domains

检查是否为一次性邮箱

一次性邮箱是指那些小段时间内有效的 email 地址,被称作 disposable email。 disposable email 是一个正确语法的地址,且可以接收和发送 email, 正常只能用一次,一般是用来注册新账号绕过登录、发送恶意邮件等场景。

常见的 disposable email 提供商包括 Nada 和 Mailinator 等。识别它们的方法是判断 domain 是否为disposable domain。目前开源社区有维护一些实时更新的 disposable domain 列表, 通过在列表里搜索 domian 的方式快速过滤掉 diposable email。

确认 domain 对应 DNS 记录是否存在

DNS 查询是指向 DNS 服务器请求 DNS 记录的过程。DNS 记录包括多种 domain 记录,这里我们主要确认 MX record(_mail exchanger record, 邮件交换记录_)。该解析记录用来指定 email server 接收特定域名的 email。举个例子,我们对 aftership.com 查询 DNS 记录如下:
image

可以看到 aftership.com对应有4条 MX 记录。MX 记录存在表示 domain 对应的 ESP 是存在, 否则不是一个有效的 email 地址。

Ping 收件人邮箱

确认完 MX record 记录存在, 可以通过与 SMTP server 建立连接,来完成对 email 地址有效性的校验。如上一步所示,MX records 一般会有多条(_有的 SMTP server 会设置 record 的权重值_),SMTP server 的地址是: MX 记录 + SMTP Relay 端口。 Ping 收件人邮箱的原理是使用 SMTP ,连接到其中有效的 SMTP server,并请求收件人地址,请求后 server 会有响应, 根据响应信息来判断地址是否存在。

如果 SMTP server 响应 250 OK, 那么这个 email 地址是可能就是一个有效地址;如果返回 550-5.1.1 类似错误那么就是一个无效的地址。

example@aftership.com 这个 email 地址为例, 下面是一个完整的 SMTP 连接的验证过程。
image

首先 telnet 连上收件人的 SMTP server, 通过 ehlo 标识发件人的身份,mail 设置 email 的发件人,最后 rcpt 设置 email 的收件人, rcpt只能设置 SMTP domain 的 email 地址, 否则分类器(SMTP rewriter)会重写邮件中的电子邮件地址,使其与主 SMTP 地址相匹配 。如果 rcpt 没有拒绝该请求,表明 SMTP server 校验通过该地址,将会把收件人添加到邮件列表。下图是一张使用 SMTP 协议发送 email 的全流程图:
image
SMTP Ping 收件人地址的方法,是整个 email 地址校验过程可能最有效的一环,SMTP server 能帮你确认收件人是否存在和可达。需要注意到这里所说的“可能”,比如 example@aftership.com其实是一个无效的地址, rcpt 响应250,email 地址不一定是可达的。

这里又涉及一个概念,是 Catch-All Email Server,也叫 accept-all server,指那些被配置为接受所有发送到 domain 的 email 的 SMTP server,无论指定的 email 地址是否存在。catch-all 会将错误地址重定向到一个通用的邮箱,以定期审查,但是带来的问题是提高 bounce 率的风险且 catch-all 地址无法被正确校验。( 比如 Gmail 是一个 catch-all email server )
所以 Ping 收件人邮箱来校验地址有效性, 需要确保对方的 SMTP server 是非 catch-all email server, rcpt命令响应250,才能说明地址是 deliverable,否则无法校验可达性。

更多关于连上 SMTP 服务器后校验过程的其他细节,比如为什么是用 rcpt 而不是其他命令来验证地址,可以参考 Email box pinging

什么时候需要校验 email 地址

校验 email 地址可以不是一个经常性的过程, 建议有下面几种情况时必须进行校验:

  1. 新增的 email 地址: 正如上面提到的, 在进行 marketing campaign 时必须对 contacts 新增的收件人列表校验 email 地址,过滤无效和非法收件人账号
  2. 超过一个月未重新校验过的 email 地址
  3. bounce 率达到或者超过 2%: 设置 bounce 率阈值来确保邮件送达率, 提高 sender 的 reputation
  4. 统计到的 email 事件,email 被打开的概率比较低

本地验证 emil 地址 vs 使用第三方 email 验证服务

经过以上步骤来完整地校验 email 列表,哪怕只有一个地址的验证也要多花不少时间。但是也可以不必进行手动验证,因为有许多第三方的 email 校验服务,一般有提供 API 来完成对地址的校验。调研了几个类似服务(比如 emailchecker),它们提供的功能主要包括以下几点:

  1. domain 校验
  2. 单个 email 地址校验
  3. 批量 email 地址校验
  4. 语法检查
  5. SMTP 校验 (Ping收件人邮箱)
  6. 提供校验 API
  7. bounce email 检测
  8. GDPR 数据保护

所以这两种验证方案哪一个是更好的选择呢?本地验证 email 地址无疑是首选, 因为自行校验其实更快,更好地支持批量校验邮件列表;要注意的是很多较好的第三方验证服务是付费的,在线验证时需要确认服务是否有 GDPR 数据保护以确保不会与第三方共享用户个人数据,或者存在安全问题,但是第三方校验不会有各种限制(多数 ISP 禁止在 25 端口上建立出站连接),且不存在影响 IP 段 和域名 reputation 的风险。

email-verifier

如果是本地验证 email 地址,目前社区其实有一些开源的验证 email 地址的工具, 其中 stars 数最多是 trumail 项目,它提供了地址校验 API。但是这个项目有两个问题, 一是校验慢,性能有些问题;二是不支持 disposable domain 的校验,且该项目 archived, 已不再维护。

在开发和维护 Messages Platform 上,作为平台方,我们除了对业务提供高可用、简单易接入的 email 消息通道服务外,降低 bounce 率和提高邮件送达率也是我们重要的指标之一。所以我们需要有一个高效的邮件校验服务,过滤非法 email 地址(平台邮件平均发送量在1000+w封/月),以提高送达率。基于我们的技术栈是 Go, 在调研了 git 社区其他开源的 email 验证工具后,发现 Go 项目对 email verifier 这一块建设是相对缺失,暂时还没有一个既提供多维度的 email 检查(包括 diposable domain, 和 Role Account 等)且校验地址可达性的工具。

由于 trumail 已不再维护,所以我们内部实现了一个新的 email 校验库 – email-verifier, 目前已经在线上环境上运行。对比 trumail, 校验 email 地址的效率更加明显,检查维度更多。

相比于现有其他的 email 地址校验工具, 我们提供高性能、多维度、高准确性的免费 email 地址校验方案,来解决在不同场景下对 email 地址校验的痛点问题。 期待 email-verifier 也能在更好地帮助到社区解决类似问题。

总结

本文主要从 bounce email 引入,详细介绍了如何在不发送邮件情况下来校验 email 地址,同时给出合适时间点校验 email 地址的几个建议;对比本地校验和第三方校验服务两者的优缺点以及为什么我们会选择自建校验服务的原因。最后是我们在这一过程中,基于校验原理孵化的一个检测工具。

一般来说, Marketing Campaigns 展开之后,肯定会遇到 bounce email 影响 campaigns 质量的问题,这个时候在发邮件前校验地址有效性的优点就不难理解:一是提高邮件送达率;二是维护和提高 sender 账号的 reputation,对业务方和平台方都是必要的。

参考

查看原文

lanffy 收藏了文章 · 2020-04-09

如何准备 Java 初级和高级的技术面试?

作者:hsm_computer
https://www.cnblogs.com/JavaA...

本人最近几年一直在做java后端方面的技术面试官,而在最近两周,又密集了面试了一些java初级和高级开发的候选人,在面试过程中,我自认为比较慎重,遇到问题回答不好的候选人,我总会再三从不同方面提问,只有当反复确认能力不行才会下结论。

相反,如果候选人给我的印象不错,我也会从多个角度来衡量,以免招进会说但不会干活的“大忽悠”。

其实倒也不是我故意要为难候选人,毕竟入职后就是同事,但面试官的职责使然,而且,如果资深的面试官一般也这样。

写到这里,恐怕会吓到一些想要面试的朋友,能力强和能力弱都会被多问,那怎么办?

这就是本文将要讲到的主题:如何准备Java初级和高级的技术面试。

一. 换位思考下,如果你是面试官,你会怎么做

1. 只能通过简历和面试来衡量,别无他法。如果某位大牛确认能力很行,但面试时无法充分地自证能力,那对不起了,过不了,现实就这样。

2. 如果面试官由于能力不行,招进来一个大忽悠,那估计会被领导骂。而且再也不会被让面试了,给领导的印象就不好了。所以不能评主观印象,而是会有些客观标准,具体而言,就是从多个方面问些题目,答好答坏就看候选人的。

其实一些题目都差不多,但不同能力的面试官问问题的切入点和渐进程度会不同,而且有经验的面试官会挖掘候选人的优势,并能从候选人的说辞中判断候选人是真懂还是忽悠。 

二. 总体上说下准备面试的几个方面点

记得之前考政治,某个大题10分,分5个点,每个点的标准答案不多,也就一两句话。比较取巧的做法是,涵盖点要全,每个点无需多说,但要说到点子上。相反,如果在某个点做得再多,其它点没覆盖到,只能拿这个点的分。

同理,在面试时,应当综合准备 java Core,数据库,框架,分布式等方面的题目。根据我面试的结果,我发现不少候选人走了弯路,他们或者干脆不准备,准备时可能方法不到位,单准备一个方面。比如只准备了算法题,在这方面回答很好,但其它方面就一无所知了。

所以说,没有所谓的一定能成功的面试秘籍,但有可以帮助提升成功率的准备方法。

切记,面试前一定得准备,否则成功的可能性很低,准备时,得综合看各方面的点。至于每个点要到什么程度,后文会讲到。 

三. 架构方面需要准备的点

初级开发而言,需要让面试官感觉出如下的要点。

1. 熟悉SSM架构,至少在项目里做过。

这个的说法是,介绍项目时,用一个业务流程来说spring mvc如何做的。

2. 知道Spring MVC中的细节,比如@Autowired的用法,如何把url映射到Controller上,ModelAndView对象返回的方式等。

3. 最好结合项目的用法,说下你是怎么用AOP,拦截器的,比如说可以通过拦截器拦截非法请求,怎么用 AOP输出日志等。

4. 关于ORM方面,不限用过哪种,但得知道一对一,一多多,多对多等的用法,以及cascade和inverse的用法。

5. 最好知道声明式事务的做法。

如果你要应聘高级开发,那在上述基础上,最好了解如下的知识点:

  • Spring Bean的周期
  • 最好能通过阅读源代码,说下IOC,AOP以及Spring MVC的工作流程,推荐阅读:史上最全 69 道 Spring 面试题和答案
  • 最好能结合反射,说下IOC等的实现原理
  • Spring Boot和Spring Cloud的一些知识点

四. 数据库方面需要准备的点

不少候选人会看很多SQL的技巧,比如select该怎么写,insert又该怎么写,但仅限于此,不会再准备其它的。

这样就很吃亏,因为面试官会认为,哪怕是初级开发,SQL语句也该会写,所以这块不会多问,而会问如下方面的问题。

1. 索引怎么建的,怎么用的?比如我建好了一个索引,在where 语句里写 name like '123%'会不会走索引,怎么情况下不该建索引,哪些语句不会走索引。

2. 除了索引之外,你有过哪些SQL优化方面的经验,比如分库分表,或通过执行计划查看SQL的优化点。这最好是能结合你做的项目实际来讲。

这里,我面试下来,大概有70%的候选人只知道基本SQL的写法,所以哪怕你是只有理论经验,会说一些优化点,也是非常有利的。

这块对于高级开发而言,更得了解优化方面的技能。推荐阅读: 37 个 MySQL 数据库小技巧,不看别后悔!

五. Java Core方面需要准备的点

这块是基础,其实很多问的问题,候选人一定会在项目里用到,但很少能说好说全。

这块主要会从集合,多线程,异常处理流程以及JVM虚拟机这些方面来问。

集合方面:

1. hashcode有没有重写过?在什么场景下需要重写。如果可以,结合hash表的算法,说下hashmap的实现原理。

对于高级开发而言,最好通过ConcurrentHashMap来说明下并发方面的底层实现代码。

2. ArrayList,LinkedList的差别,比如一个基于数组,一个基于链表,它们均是线程不安全的,ArrayList的扩容做法等。

对于高级而言,最好看下底层的代码。

3. Set如何实现防重的,比如TreeSet和HashSet等。

4. Collection的一些方法,比如比较方法,包装成线程安全的方法等。

5. 可能有些面试官会问,如何通过ArrayList实现队列或堆栈,这个可以准备下。

多线程方面,其实在项目里不怎么会用到,但会问如下的问题:

1. synchronized和可重入锁的差别,然后可能会顺便问下信号量等防并发的机制。

2. 在线程里该如何返回值,其实就是callable runnable 区别。

3. 一定得通过ThreadLocal或volatile关键字,来说明线程的内存模型。

4. 线程池方面,会用,了解些常用参数

线程方面,可能问得比较多的就是并发机制,如果是高级开发,可能会问得深些。

虚拟机方面

1. 结构图和流程可以大致说下。

2. 一定得了解针对堆的垃圾回收机制,具体而言,可以画个图,说下年轻代年老代等。

3. 说下垃圾回收的流程,然后针对性地说下如何在代码中优化内存性能。

4. 最好说下如果出现了OOM异常,该怎么排查?如何看Dump文件。

5. GC的一些概念,比如强弱软引用,finalize方法等,这些可以准备下。 在Java技术栈微信公众号后台回复:Java,可以阅读Java核心技术知识点。

六. 算法,设计模式等,其实是虚的

这块好准备,不过话说哪怕这些没回答好,但能证明有相关技能的项目经验,一般也会让过。   

不过在这块,不少候选人就本末倒置了,比如就准备算法,设计模式,刚才提到的框架,数据库和Java Core方面就不准备了。这样很吃亏,就好比考政治只复习了一个点,其它一点也不准备。    

七. 我面试的感受&听到哪类回答就能证明候选人比较资深

1. 大多数的候选人(大概7成)直接就来了,不做任何准备。要知道,面试和项目其实有些脱节,哪怕项目做得再好,不做准备照样通不过,只要我确认过这类人确实无法达标,我拒掉他们没任何心理负担,谁让他们不准备?

2. 还有些候选人态度很好,明显准备过,但没准备到位,比如像刚才所说,只准备了算法,或者在Java Core方面,只看了集合方面的面试题。对于这些同学,哪怕是过了,我也会感到惋惜,毕竟如果面试好些的话,工资也能更高些,至于哪些过不了的,我敢说,如果他们准备过,估计就不是这个结果了。

其实我也知道,人无完人,哪怕我自己去面试,也不可能面面俱到,所以,我不会要求候选人什么问题都能回答出,甚至大多答错也没关系,只要能证明自己的能力即可通过面试。

我也和不少面试官交流过,根据我们的经验,如果候选人能说出如下的知识点,即能证明他在这个领域比较资深了,在这块,我可能就不会过多地问问题了。  

架构方面

1. 能证明自己可以干活(这不难),同时能结合底层代码说出IOC,AOP或Spring MVC的流程,只要能说出一个即可。或者能说出拦截器,Controller等的高级用法。

2. 能证明自己有Spring Boot或Spring Cloud的经验,比如能说出些Spring Cloud组件的用法。

3. 如果能证明自己有分布式开发的经验,那最好了,其实这不难证明,比如能说出服务的包是放在多台机器上(大多数公司其实都这样),而且能说出如何部署,如何通过nginx等做到负载均衡。

数据库方面,其实讲清楚一个问题即可:如何进行SQL调优,比如通过索引,看执行计划即可,如果有其它的优化点,说清楚即可。

Java Core方面,这里给出些诀窍:

1. 能结合ConcurrentHashMap的源代码,说出final,volatile,transient的用法,以及在其中如何用Lock对象防止写并发。

2. 结合一个项目实际,说下设计模式的实践。

3. 多线程方面,能说出Lock或volatile等高级知识点的用法。

4. 这块最取巧:说下GC的流程,以及如何通过日志和Dump文件排查OOM异常,如果再高级些的话,说下如何在代码中优化内存代码。    

诀窍点归结成一个:能结合源代码或项目实际,说出些比较资深的问题。在java技术栈微信公众号后台回复:多线程,可以阅读Java多线程技术文章。推荐阅读:史上最全 Java 多线程面试题及答案

八. 本文的侧重点

本文的侧重点是:

  1. 面试一定得准备 (重要的话说三遍,这里已经超过3遍了)
  2. 如何全面充分地准备。

至于为什么要写这个文章?我得不停地总结我作为面试官的技巧,这样我在面试中也能更高效更准确地招到合适的人才。

推荐去我的博客阅读更多:

1.Java JVM、集合、多线程、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、后端、架构、阿里巴巴等大厂最新面试题

生活很美好,明天见~

查看原文

lanffy 关注了专栏 · 2020-03-31

赵帅虎

Go语言高级用法等

关注 142

lanffy 关注了用户 · 2020-03-31

oscarzhao @oscarzhao

关注 85

lanffy 关注了用户 · 2020-03-12

沈唁 @sy_records

欢迎关注我的个人公众号:沈唁志

关注 96

lanffy 收藏了文章 · 2019-11-20

PHP7 serialize_precision 配置不当导致 json_encode() 浮点小数溢出错误

感谢 @地狱星星:
原因已找到, 该现象只出现在PHP 7.1+版本上
建议使用默认值 serialize_precision = -1 即可
参考: https://wiki.php.net/rfc/prec...
----------------------------------------------------------------------------------

事情是这样的,项目里发现一个奇怪的现象,json_encode一个带浮点价格的数据, 出现溢出, 比如:

<?php
echo json_encode(277.2);
// 输出结果为: 277.199999999999989

这明显是不能接受的, 数据虽然很接近, 但毕竟已经变更了
下意识地认为这是php的一个bug, 不能准确地json序列化一个浮点小数
这个问题google了半天竟然也无果, 然后我跟同事说, 你们json_encode数据里有小数的时候, 记得先number_format()转化成字符串.

自己又写了个fix方法, 遍历数据, 把is_float()的数据都用number_format()转化了

function json_encode_pre($d, $depth=128, $level=0){
    if($level>$depth) return $d;
    if(is_array($d)){
        foreach ($d as $i => $v) { $d[$i] = json_encode_pre($v, $depth, $level+1); }
        return $d;
    }
    if(is_float($d)){
        # 测试发现number_format有效数字低于18(保守取16)时,不会溢出
        $p = 16 - strlen(intval($d));
        $f = number_format($d, $p);
        if($p>1){ $f = preg_replace('/0+$/','', $d); }
        return $d;
    }
    return $d;
}

echo number_format(277.2, 14); // 当18位有效数字处理(277.2已有4位有效数字)
// 277.199999999999989
echo number_format(277.2, 12); // 当16位
// 277.2000000000000
echo json_encode(json_encode_pre(277.2));
// "277.2"

看到这结果时, 便猜测是有效数字位数的问题导致了溢出, PHP怎么会有这么蠢的设计呢?这是我当时的想法.

然后想着是不是能从源码下手, 于是查了下php源码, 发现这段代码:

static inline void php_json_encode_double(smart_str *buf, double d, int options) /* {{{ */
{
    size_t len;
    char num[PHP_DOUBLE_MAX_LENGTH];

    php_gcvt(d, (int)PG(serialize_precision), '.', 'e', num);
    len = strlen(num);
    if (options & PHP_JSON_PRESERVE_ZERO_FRACTION && strchr(num, '.') == NULL && len < PHP_DOUBLE_MAX_LENGTH - 2) {
        num[len++] = '.';
        num[len++] = '0';
        num[len] = '\0';
    }
    smart_str_appendl(buf, num, len);
}

是的, 竟然发现了配置项serialize_precision, 这个配置我当初的理解是serialize()方法用的, 而我当初尝试修改过配置项precision, 然并卵, 原来json_encode会用到serialize_precision, 于是修改php.ini, 把它设为16, 原来是17还是18忘了.

<?php
echo json_encode(277.2);
// 输出结果为: 277.2

如果你想重现我的问题, 很简单, 把serialize_precision设为20或更大, 至于这个有效数字最大值是多少才不会溢出, 我还不知道跟什么有关系, 希望能有大神指点了.
我是通过改配置数值, 出现溢出的值再减去2来用的.

附上配置项说明, 从官方说明上看, 很难想像是跟json_encode是有关系的.

; php.ini

; When floats & doubles are serialized store serialize_precision significant
; digits after the floating point. The default value ensures that when floats
; are decoded with unserialize, the data will remain the same.
serialize_precision = 16

; The number of significant digits displayed in floating point numbers.
; http://php.net/precision
precision = 16

另外源码里有个json_encode的选项JSON_PRESERVE_ZERO_FRACTION, 这个的意思是如果是个是个整数, 是否保留小数点和0, 来看测试结果:

<?php
echo json_encode(277.0);
// 277
echo json_encode(277.0, JSON_PRESERVE_ZERO_FRACTION);
// 277.0
查看原文

lanffy 赞了文章 · 2019-11-20

PHP7 serialize_precision 配置不当导致 json_encode() 浮点小数溢出错误

感谢 @地狱星星:
原因已找到, 该现象只出现在PHP 7.1+版本上
建议使用默认值 serialize_precision = -1 即可
参考: https://wiki.php.net/rfc/prec...
----------------------------------------------------------------------------------

事情是这样的,项目里发现一个奇怪的现象,json_encode一个带浮点价格的数据, 出现溢出, 比如:

<?php
echo json_encode(277.2);
// 输出结果为: 277.199999999999989

这明显是不能接受的, 数据虽然很接近, 但毕竟已经变更了
下意识地认为这是php的一个bug, 不能准确地json序列化一个浮点小数
这个问题google了半天竟然也无果, 然后我跟同事说, 你们json_encode数据里有小数的时候, 记得先number_format()转化成字符串.

自己又写了个fix方法, 遍历数据, 把is_float()的数据都用number_format()转化了

function json_encode_pre($d, $depth=128, $level=0){
    if($level>$depth) return $d;
    if(is_array($d)){
        foreach ($d as $i => $v) { $d[$i] = json_encode_pre($v, $depth, $level+1); }
        return $d;
    }
    if(is_float($d)){
        # 测试发现number_format有效数字低于18(保守取16)时,不会溢出
        $p = 16 - strlen(intval($d));
        $f = number_format($d, $p);
        if($p>1){ $f = preg_replace('/0+$/','', $d); }
        return $d;
    }
    return $d;
}

echo number_format(277.2, 14); // 当18位有效数字处理(277.2已有4位有效数字)
// 277.199999999999989
echo number_format(277.2, 12); // 当16位
// 277.2000000000000
echo json_encode(json_encode_pre(277.2));
// "277.2"

看到这结果时, 便猜测是有效数字位数的问题导致了溢出, PHP怎么会有这么蠢的设计呢?这是我当时的想法.

然后想着是不是能从源码下手, 于是查了下php源码, 发现这段代码:

static inline void php_json_encode_double(smart_str *buf, double d, int options) /* {{{ */
{
    size_t len;
    char num[PHP_DOUBLE_MAX_LENGTH];

    php_gcvt(d, (int)PG(serialize_precision), '.', 'e', num);
    len = strlen(num);
    if (options & PHP_JSON_PRESERVE_ZERO_FRACTION && strchr(num, '.') == NULL && len < PHP_DOUBLE_MAX_LENGTH - 2) {
        num[len++] = '.';
        num[len++] = '0';
        num[len] = '\0';
    }
    smart_str_appendl(buf, num, len);
}

是的, 竟然发现了配置项serialize_precision, 这个配置我当初的理解是serialize()方法用的, 而我当初尝试修改过配置项precision, 然并卵, 原来json_encode会用到serialize_precision, 于是修改php.ini, 把它设为16, 原来是17还是18忘了.

<?php
echo json_encode(277.2);
// 输出结果为: 277.2

如果你想重现我的问题, 很简单, 把serialize_precision设为20或更大, 至于这个有效数字最大值是多少才不会溢出, 我还不知道跟什么有关系, 希望能有大神指点了.
我是通过改配置数值, 出现溢出的值再减去2来用的.

附上配置项说明, 从官方说明上看, 很难想像是跟json_encode是有关系的.

; php.ini

; When floats & doubles are serialized store serialize_precision significant
; digits after the floating point. The default value ensures that when floats
; are decoded with unserialize, the data will remain the same.
serialize_precision = 16

; The number of significant digits displayed in floating point numbers.
; http://php.net/precision
precision = 16

另外源码里有个json_encode的选项JSON_PRESERVE_ZERO_FRACTION, 这个的意思是如果是个是个整数, 是否保留小数点和0, 来看测试结果:

<?php
echo json_encode(277.0);
// 277
echo json_encode(277.0, JSON_PRESERVE_ZERO_FRACTION);
// 277.0
查看原文

赞 11 收藏 12 评论 4

lanffy 发布了文章 · 2019-11-07

Elasticsearch系列七:常见用法手册

前面几篇文章介绍了搜索引擎ElasticSearch的内部原理,这篇文章总结了在ElasticSearch使用过程中常见的用法。

1、查看集群信息

ElasticSearch 查看集群健康信息,常用命令如下:

1.1、查看集群状态

ElasticSearch查看集群状态命令:

curl 'localhost:9200/_cat/health?v'

其中,status为绿色表示一切正常, 黄色表示所有的数据可用但是部分副本还没有分配,红色表示部分数据因为某些原因不可用.

1.2、查看索引分片情况

ElasticSearch查看索引分片情况命令:

curl 'localhost:9200/_cat/shards?v'

可以看到例子中有两个索引分别是:people和product,他们各有一个主分片和一个尚未分配的副分片。

1.3、查看节点信息

ElasticSearch查看节点信息命令:

  • 主节点:curl 'localhost:9200/_cat/master?v'
  • 所有节点:curl 'localhost:9200/_cat/nodes?v'

2、索引相关命令

2.1、创建索引

ElasticSearch创建索引命令。

7.0版本去掉了type的概念。

2.1.1、7.0之前版本创建索引

curl -X PUT 'localhost:9200/product' -H 'Content-Type: application/json' -d '
{
  "mappings": {
    "type_name": {
      "properties": {
        "price": {
          "type": "integer"
        },
        "name": {
          "type": "text"
        }
      }
    }
  }
}'

2.1.2、7.0及其之后的版本创建索引

curl -X PUT 'localhost:9200/product' -H 'Content-Type: application/json' -d '
{
  "mappings": {
      "properties": {
        "price": {
          "type": "integer"
        },
        "name": {
          "type": "text"
        }
      }
    }
}'

2.2、查询索引

ElasticSearch查询所有的索引命令。

curl 'localhost:9200/_cat/indices?v'

可以看到上面的例子中,集群中有两个索引。他们的名字分别为:people、product。且分别有一个主分片和副分片。

2.3、查询索引结构

2.3.1、查看所有的索引mapping数据结构

ElasticSearch查询索引mapping结构命令:

curl 'localhost:9200/_mapping?pretty'

上面的例子查询了索引people和product的mapping数据结构。

2.3.2、查看指定的索引mapping数据结构

2.3.2.1、7.0之前版本

查看索引名为indexName且type名为typeName的索引mapping数据结构:

curl -XGET "127.0.0.1:9200/indexName/typeName/_mapping?pretty"

2.3.2.2、7.0及其之后版本

查看索引名为indexName的索引mapping数据结构:

curl -XGET "127.0.0.1:9200/indexName/_mapping?pretty"

2.4、给索引添加字段

ElasticSearch在索引中添加字段命令

2.4.1、7.0之前版本

curl -XPOST "127.0.0.1:9200/indexName/typeName/_mapping?pretty" -H 'Content-Type: application/json' -d '{
 "typeName": {
            "properties": {
                 "tags":{
                    "type":"text"
               }
            }
        }
}'
上面的例子,表示给索引名为indexName且type名为typeName的mapping数据结构添加tags字段

2.4.2、7.0及其之后版本

curl -XPOST "127.0.0.1:9200/product/_mapping?pretty" -H 'Content-Type: application/json' -d '{
    "properties": {
         "tags":{
            "type":"text"
       }
    }
}'
上面的例子,表示给索引名为product的mapping数据结构添加tags字段

2.5、索引中删除字段

ElasticSearch现在不支持

2.6、删除索引

ElasticSearch 删除某个索引命令。

curl -X DELETE 'localhost:9200/product'

上面的例子表示删除名为product的索引。

3、数据文档搜索查询相关命令

下面提到的数据即索引中的文档。

3.1、写入数据命令

ElasticSearch写入数据命令.

7.0版本去掉了type的概念。

3.1.1、7.0之前版本写入数据

curl -X PUT 'localhost:9200/product/type_name/1' -H 'Content-Type: application/json' -d '
{
    "price":1,
    "name":"富士山苹果"
}'

3.1.2、7.0及其之后的版本写入数据

curl -X PUT 'localhost:9200/product/_doc/1' -H 'Content-Type: application/json' -d '
{
    "price":1,
    "name":"富士山苹果"
}'

3.2、搜索查询数据命令

ElasticSearch搜索数据命令

3.2.1、7.0之前版本搜索数据

3.2.1.1、主键搜索

curl -X GET 'localhost:9200/product/type_name/1?pretty'

3.2.1.2、关键字搜索
curl -X GET 'localhost:9200/product/_search?pretty' -H 'Content-Type: application/json' -d '
{
  "query": {
    "match": {
      "name": "苹果"
    }
  },
  "from":0,
  "size":1
}'

3.2.2、7.0及其之后的版本搜索数据

3.2.2.1、主键搜索

curl -X GET 'localhost:9200/product/_doc/1?pretty'

3.2.2.2、关键字搜索
curl -X GET 'localhost:9200/product/_search?pretty' -H 'Content-Type: application/json' -d '
{
  "query": {
    "match": {
      "name": "苹果"
    }
  },
  "from":0,
  "size":1
}'

3.2.3、对多值的字段搜索

查询name包含“苹果”且tags有“苹果”或者“富士山”的文档数据:

curl -X GET 'localhost:9200/product/_search?pretty' -H 'Content-Type: application/json' -d '{
    "query":{
        "bool":{
            "must":[
                {
                    "bool":{
                        "should":[
                            {"match":{"name":"苹果"}},
                            {"terms": {"tags": ["苹果","富士山"]}}
                        ]
                    }
                }
            ]
        }
    },
    "sort":{
        "_score":{"order":"desc"}
    },
    "from":0,
    "size":10
}'

3.2.4、多字段联合查询

查询name包含“苹果”且price为1的文档数据:

curl -X GET 'localhost:9200/product/_search?pretty' -H 'Content-Type: application/json' -d '{
    "query":{
        "bool":{
            "must":[
                {
                    "bool":{
                        "should":[
                            {"match":{"name":"苹果"}},
                            {"match":{"price":1}}
                        ]
                    }
                }
            ]
        }
    },
    "sort":{
        "_score":{"order":"desc"}
    },
    "from":0,
    "size":10
}'

3.3、更新数据命令

ElasticSearch中更新数据命令:

curl -X PUT 'localhost:9200/product/_doc/1' -H 'Content-Type: application/json' -d '
{
    "price":1,
    "name":"富士山苹果",
    "tags":["富士山","名牌","苹果"]
}'

3.4、删除数据

ElasticSearch中删除文档数据命令:

3.4.1、7.0之前版本

删除一个文档

curl -XDELETE 'http://localhost:9200/indexName/typeName/1'

上面的例子表示删除索引名为indexName且type名为typeName的索引中文档ID为1的文档。

3.4.2、7.0及其之后的版本

curl -XDELETE 'http://localhost:9200/indexName/_doc/1'

4、ElasticSearch中分词器分析器在命令行中的用法

ElasticSearch支持不同的分词插件,在下面的例子中我们使用了analysis-ik分词插件。

通过ElasticSearch的API接口,可以分析不同分词器的分词结果1。具体的步骤如下:

4.1、添加两个字符类型的字段,并指定不同的分词器:

curl -XPOST "127.0.0.1:9200/product/_mapping?pretty" -H 'Content-Type: application/json' -d '{
    "properties": {
        "pNameIkMaxWord":{
            "type":"text",
            "analyzer":"ik_max_word"
        },
        "pNameIkSmart":{
            "type":"text",
            "analyzer":"ik_smart"
        }
    }
}'

4.2、使用ik_max_word分词分析

curl -XPOST 'http://localhost:9200/product/_analyze?pretty' -H 'Content-Type: application/json' -d '
{
    "field": "pNameIkMaxWord",
    "text": "中华人民共和国国歌"
}'

分词结果如下:

{
  "tokens" : [
    {
      "token" : "中华人民共和国",
      "start_offset" : 0,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "中华人民",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "中华",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "华人",
      "start_offset" : 1,
      "end_offset" : 3,
      "type" : "CN_WORD",
      "position" : 3
    },
    {
      "token" : "人民共和国",
      "start_offset" : 2,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 4
    },
    {
      "token" : "人民",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 5
    },
    {
      "token" : "共和国",
      "start_offset" : 4,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 6
    },
    {
      "token" : "共和",
      "start_offset" : 4,
      "end_offset" : 6,
      "type" : "CN_WORD",
      "position" : 7
    },
    {
      "token" : "国",
      "start_offset" : 6,
      "end_offset" : 7,
      "type" : "CN_CHAR",
      "position" : 8
    },
    {
      "token" : "国歌",
      "start_offset" : 7,
      "end_offset" : 9,
      "type" : "CN_WORD",
      "position" : 9
    }
  ]
}

4.3、使用ik_smart分词分析

curl -XPOST 'http://localhost:9200/product/_analyze?pretty' -H 'Content-Type: application/json' -d '
{
    "field": "pNameIkSmart",
    "text": "中华人民共和国国歌"
}'

分词结果如下:

{
  "tokens" : [
    {
      "token" : "中华人民共和国",
      "start_offset" : 0,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "国歌",
      "start_offset" : 7,
      "end_offset" : 9,
      "type" : "CN_WORD",
      "position" : 1
    }
  ]
}

系列文章

  1. ElasticSearch系列一:源码编译和Debug环境搭建
  2. ElasticSearch系列二:启动过程详解
  3. Elasticsearch系列三:创建索引过程详解
  4. Elasticsearch系列四:搜索过程详解
  5. Elasticsearch系列五:搜索相关性排序算法详解
  6. Elasticsearch系列六:ES中的倒排索引
  7. Elasticsearch系列七:常见用法手册

  1. https://elasticsearch.cn/arti...
查看原文

赞 0 收藏 0 评论 0

lanffy 收藏了文章 · 2019-10-12

使用 Docker 搭建 Laravel 本地环境

(原文地址:https://blog.tanteng.me/2017/...

Laravel 官方提供 Homestead 和 Valet 作为本地开发环境,Homestead 是一个官方预封装的 Vagrant Box,也就是一个虚拟机,但是跟 docker 比,它占用体积太大,启动速度慢,同时响应速度很慢,现在有了 docker 这种更好的方式,可以轻松方便的搭建整套 PHP 开发环境。

本文就介绍如何使用 docker 搭建 Laravel 本地环境。

安装 docker

首先安装 docker。

克隆 laradock

laradock 官方文档:http://laradock.io/

laradock github:https://github.com/laradock/l...

laradock 是一个包含全功能用于 docker 的 PHP 运行环境,使用 docker-compose 方式部署。(特别说明:它不仅用于 Laravel 环境搭建,而且支持所有其他 PHP 框架,它就是一整套 PHP 的环境。)

部署 PHP 环境

1.克隆 laradock

git clone https://github.com/Laradock/laradock.git

2.创建环境变量文件

cp env-example .env

3.直接用 docker-compose 运行需要启用的服务,如:

docker-compose up -d nginx mysql redis beanstalkd

这样就启动了所需的 PHP 运行环境,php-fpm 默认会运行,所以不需要指定。

图片描述

Laravel 配置文件

Laravel 配置文件需要注意的问题是,在 .env 文件中,mysql 和 redis 的地址需填写成这样,而不是 ip 地址形式:

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=tanteng.me
DB_USERNAME=root
DB_PASSWORD=root
 
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379

Nginx 配置

在本地通过域名方式访问站点,要将 host 中域名绑定到本地,同时还需要增加 nginx 配置。

图片描述

如图,在 laradock 项目的 nginx 文件夹下的 sites 目录下添加配置文件即可。

执行 composer

执行 composer 等操作,需要进入到 workspace 容器中进行,使用命令:

docker-compose exec workspace bash

进入到 workspace 容器,就可以进行 compose 命令等操作了。

具体使用上的问题请参加 laradock 官方文档,上面都有说明。

Docker 讲座介绍

Docker 是现在火热的一个容器技术,作为开发人员有必要掌握它的使用,无论你是用来搭建本地环境,还是部署应用。你将体会到使用 Docker 的便利。

一个规范的开发流程,首先要有一个完善的本地环境,以前使用 vagrant + Homestead 作为虚拟机,启动慢,占用空间大,而 Docker 更轻量级,启动速度快,更可以方便搭配不同的容器。不仅如此,docker 配置也可以用于生产环境,"Build once, Run anywhere, Configure once, Run anything".

本次讲座重点以搭建 PHP 环境为例,使用 Github 上一个项目 —— laradock,演示如何以 docker-compose 方式搭建全套本地 PHP 开发环境。

你将学习到:

Docker 基础知识
Dokerfile 语法
docker-compose.yml 语法
docker-compose 的使用
如何使用 laradock 搭建 PHP 环境

让我们开启 PHP 开发者的 Docker 之旅!

讲座报名地址:https://segmentfault.com/l/15...

查看原文

lanffy 发布了文章 · 2019-07-02

Elasticsearch中的倒排索引

前言

Elasticsearch创建索引流程一文中,介绍了ES创建索引的流程。再流程中是调用Lucene的接口来创建索引的。本篇文章主要介绍ES中的索引——倒排索引

分词

在创建索引之前,会对文档中的字符串进行分词。ES中字符串有两种类型,keyword和text。

  • keyword类型的字符串不会被分词,搜索时全匹配查询
  • text类型的字符串会被分词,搜索时是包含查询

不同的分词器对相同字符串分词的结果大有不同,选择不同的分词器对索引的创建有很大的影响

如拆分“中华人民共和国国歌”

  1. ik_max_word分词器: 最细粒度拆分,分词结果如下:

    • 中华人民共和国
    • 中华人民
    • 中华
    • 华人
    • 人民共和国
    • 人民
    • 共和国
    • 共和
    • 国国
    • 国歌
  2. ik_smart分词器: 最粗粒度的拆分,分词结果如下:

    • 中华人民共和国
    • 国歌

可见,再ES中创建索引,选择合适的分词器是很重要的。

单词-文档矩阵

-单词1单词2单词3单词4
文档1
文档2
文档3
文档4

该矩阵是表达单词和文档两者之间包含关系的概念模型。
从横向看,每行代表文档包含了哪些单词,比如文档1包含了单词1和单词3,而不包含其它单词。
从纵向看,每列代表了某个单词存在于哪些文档。比如单词1在文档1和文档4中出现过。

简单来说,索引就是实现“单词-文档矩阵”的具体数据结构,而倒排索引则是实现了这种数据结构的具体方式。

倒排索引

倒排索引由两部分构成:

  • 单词词典
  • 倒排列表

它的结构如下:

clipboard.png

单词词典

单词词典的特性:

  1. 是文档集合中所有单词的集合
  2. 它是保存索引的最小单位
  3. 其中记录着指向倒排列表的指针

单词词典的实现:

clipboard.png

单词词典有两种数据结构实现:B+树Hash表

B+树和Mysql索引结构中主键索引数据结构一样,这里就不再介绍了

哈希表的key是单词的hash值,值是单词的链表,因为hash算法会有冲突的情况发生,所以这里的值是一个集合,里面保存着相同hash值得单词以及改词指向倒排列表的指针

倒排列表

倒排列表特性:

  1. 记录出现过某个单词的文档列表
  2. 同时还记录单词在所有文档中的出现次数和偏移位置

倒排列表元素数据结构:$$(DocID;TF;<POS>)$$

其中:

  • DocID:出现某单词的文档ID
  • TF(Term Frequency):单词在该文档中出现的次数
  • POS:单词在文档中的位置

举例

有下面单个文档

-内容
文档1百度的年度目标
文档2AI技术生态部的年度目标
文档3AI市场的年度目标

则他们生成的倒排索引

单词ID单词逆向文档频率倒排列表(DocID;TF;<POS>)
1目标3(1;1;<3>),(2;1;<5>),(3;1;<4>)
2年度3(1;1;<2>),(2;1;<4>),(3;1;<3>)
3AI2(2;1;<1>),(3;1;<1>)
4技术1(2;1;<2>)
5生态1(2;1;<3>)
6市场1(3;1;<2>)

比如单词“年度”,单词ID为2,在三个文档中出现过,所以逆向文档频率为3,同时倒排索引中的元素也有三个:(1;1;<2>),(2;1;<4>),(3;1;<3>)。拿第一个元素(1;1;<2>)进行说明,他表示“年度”再文档ID为1的文档中出现过1次,出现的位置是第二个单词

倒排索引的搜索过程

直到了倒排索引的内部结构之后,就能很好理解倒排索引的搜索过程了,其内部搜索过程如下图所示:

clipboard.png

系列文章

  1. 搜索引擎ElasticSearch源码编译和Debug环境搭建
  2. 搜索引擎ElasticSearch的启动过程
  3. Elasticsearch创建索引流程
  4. Elasticsearch搜索过程详解
  5. Elasticsearch搜索相关性排序算法详解
  6. Elasticsearch中的倒排索引
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 107 次点赞
  • 获得 76 枚徽章 获得 6 枚金徽章, 获得 24 枚银徽章, 获得 46 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-06-09
个人主页被 1.7k 人浏览