├── images ├── 2_php_1.png ├── 2_php_2.png ├── 2_php_3.png ├── 2_php_4.png ├── 3_php_1.png ├── 3_php_2.png ├── 4_php_1.png ├── 4_php_2.png ├── 4_php_3.png ├── 5_php_1.png ├── 5_php_2.png ├── 8_php_1.png ├── 8_php_2.jpeg ├── 9_php_1.jpeg └── rabbitMQ.jpg ├── files └── phpdocker │ ├── php5-6-x.zip │ └── php7-2-x.zip ├── 00-架构 ├── README.md ├── 01-项目:架构-拆分.md ├── 03-消息队列-RabbitMQ.md └── 02-配置中心-Apollo.md ├── 01-规范 └── 01-版本库 Commit 规范.md ├── README.md ├── 03-SSO 单点登录.md ├── 08-我眼中的 RPC.md ├── 01-PHP 浮点数高精度运算.md ├── 06-三个水桶等分8升水的问题 -《算法的乐趣》.md ├── 04-PHP WEB 安全防御.md ├── 07-使用过Redis,我竟然还不知道Rdb.md ├── 05-PHP 缓存技术.md ├── 09-Composer 包开发居然这么简单.md └── 02-PHP 接口签名验证.md /images/2_php_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/2_php_1.png -------------------------------------------------------------------------------- /images/2_php_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/2_php_2.png -------------------------------------------------------------------------------- /images/2_php_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/2_php_3.png -------------------------------------------------------------------------------- /images/2_php_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/2_php_4.png -------------------------------------------------------------------------------- /images/3_php_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/3_php_1.png -------------------------------------------------------------------------------- /images/3_php_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/3_php_2.png -------------------------------------------------------------------------------- /images/4_php_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/4_php_1.png -------------------------------------------------------------------------------- /images/4_php_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/4_php_2.png -------------------------------------------------------------------------------- /images/4_php_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/4_php_3.png -------------------------------------------------------------------------------- /images/5_php_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/5_php_1.png -------------------------------------------------------------------------------- /images/5_php_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/5_php_2.png -------------------------------------------------------------------------------- /images/8_php_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/8_php_1.png -------------------------------------------------------------------------------- /images/8_php_2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/8_php_2.jpeg -------------------------------------------------------------------------------- /images/9_php_1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/9_php_1.jpeg -------------------------------------------------------------------------------- /images/rabbitMQ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/images/rabbitMQ.jpg -------------------------------------------------------------------------------- /files/phpdocker/php5-6-x.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/files/phpdocker/php5-6-x.zip -------------------------------------------------------------------------------- /files/phpdocker/php7-2-x.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xinliangnote/PHP/HEAD/files/phpdocker/php7-2-x.zip -------------------------------------------------------------------------------- /00-架构/README.md: -------------------------------------------------------------------------------- 1 | ## 文章指引 2 | 3 | 抛砖引玉,后期会慢慢整理 ... 4 | 5 | - [x] 01-项目架构 - 拆分 6 | - [x] 02-配置中心 - Apollo 7 | - [ ] 03-消息队列 - RabbitMQ 8 | - [ ] 04-数据查询 - ElasticSearch 9 | 10 | -------------------------------------------------------------------------------- /00-架构/01-项目:架构-拆分.md: -------------------------------------------------------------------------------- 1 | ## 项目拆分 2 | 3 | - controller 层 4 | 5 | 控制器层,验证提交的数据,格式化 service 层返回的数据。 6 | 7 | - service 层 8 | 9 | 业务层,不直接操作数据库,只写业务代码。 10 | 11 | - repository 层 12 | 13 | 数据库操作层,操作数据库,不写业务代码。 14 | 15 | - model 层 16 | 17 | 数据库的ORM,供 repository 调用。 18 | 19 | 20 | 21 | ## 架构拆分 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /01-规范/01-版本库 Commit 规范.md: -------------------------------------------------------------------------------- 1 | ## 版本库 2 | 3 | - svn 4 | - git 5 | 6 | 在提交备注时,一定要严格遵守规范,利人利己! 7 | 8 | ## 规范 9 | 10 | `(scope):` 11 | 12 | 比如: 13 | 14 | fix(首页模块):修复JS报错 15 | 16 | #### type 17 | 18 | - fix:修复了 xx bug 19 | - feat:新增了 xx 功能 20 | - test:测试 xx 问题,问题调试 21 | - style:变更了 xx 代码格式,修改注释备注之类的 22 | - docs:变更了 xx 文档 23 | - refactor:重构了 xx 功能 24 | 25 | #### scope 26 | 27 | 影响范围。 28 | 29 | 比如,xx 模块 30 | 31 | #### subject 32 | 33 | 简短描述,不超过60个字。 34 | 35 | ## 自动化 36 | 37 | - :white_large_square: svn 插件 38 | - :white_large_square: git 插件 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 项目介绍 2 | 3 | 项目地址:https://github.com/xinliangnote/PHP 4 | 5 | PHP 文章集锦。 6 | 7 | 持续更新... 8 | 9 | ## 项目结构 10 | 11 | ``` 12 | ├─ PHP 13 | │ ├─ 00-架构 14 | │ ├── 01-项目架构 - 拆分 15 | │ ├── 02-配置中心 - Apollo 16 | │ ├─ 01-规范 17 | │ ├── 01-版本库 Commit 规范 18 | │ ├─ 01-PHP 浮点数高精度运算 19 | │ ├─ 02-PHP 接口签名验证 20 | │ ├─ 03-SSO 单点登录 21 | │ ├─ 04-PHP WEB 安全防御 22 | │ ├─ 05-PHP 缓存技术 23 | │ ├─ 06-三个水桶等分8升水的问题 -《算法的乐趣》 24 | │ ├─ 07-使用过Redis,我竟然还不知道Rdb 25 | │ ├─ 08-我眼中的 RPC 26 | │ ├─ 09-Composer 包开发居然这么简单 27 | ``` 28 | 29 | 如果你发现本项目有内容上的错误,欢迎提交 issues 进行指正。 30 | 31 | ## 学习交流 32 | 33 | :star2: 关注微信公众号「新亮笔记」 34 | 35 | ![](https://github.com/xinliangnote/Go/blob/master/00-基础语法/images/qr.jpg) 36 | 37 | -------------------------------------------------------------------------------- /00-架构/03-消息队列-RabbitMQ.md: -------------------------------------------------------------------------------- 1 | ## RabbitMQ 2 | 3 | #### 简介 4 | 5 | RabbitMQ 是一个开源的消息代理和队列服务器,用来通过普通协议在完全不同的应用之间共享数据,RabbitMQ 是使用 Erlang 语言编写的,是基于 AMQP 协议的。 6 | 7 | `开源`、`性能优秀`、`稳定` 8 | 9 | 集群模式丰富,表达式配置,HA模式,镜像队列模型。 10 | 11 | 保证数据不丢失的前提做到高可靠行、可用性。 12 | 13 | AMQP:Advanced Message Queuing Protocol 高级消息队列协议。 14 | 15 | #### 流程图 16 | 17 | ![](https://github.com/xinliangnote/PHP/blob/master/images/rabbitMQ.jpg) 18 | 19 | #### 核心概念 20 | 21 | - Producer:消息生产者。 22 | 23 | - Broker:接受客户端的连接,实现 AMQP 实体服务。 24 | - Connection:连接,应用程序与 Broker 的网络连接。 25 | - Channel:网络信道,几乎所有的操作都在 Channel 中进行,Channel 是进行消息读写的通道。客户端可以建立多个 Channel ,每个 Channel 代表一个会话任务。 26 | - Message:消息,服务器和应用程序之间传送的数据,由 Properties 和 Body 总成。Properties 可以对消息进行修饰,比如消息的优先级、延迟等高级特性;Body 就是消息体的内容。 27 | - Virtual Host:虚拟地址,用于进行逻辑隔离,最上层的消息路由。一个 Virtual Host 里面可以有若干个 Exchange 和 Queue,同一个 Virtual Host 里面不能有相同名称的 Exchange 和 Queue。 28 | - Exchange:交换机,接受消息,根据路由键转发消息到绑定的队列。 29 | - Binding:Exchange 和 Queue 之间的虚拟链接,binding中可以包含 routing key。 30 | - Routing key:一个路由规则,虚拟机可用它来确定如何路由如何路由一个特定消息。 31 | - Queue:也称为 Message Queue,消息队列,保存消息并将它们转发给消费者。 32 | - Consumer:消息消费者。 33 | 34 | #### 保障 100% 消息投递成功设计方案 35 | 36 | - 消息入库,对消息状态进行打标记(发出一个标记,接收一个标记) 37 | - 消费者实现幂等性。 38 | 39 | #### PHP 40 | 41 | - AMQP 扩展:http://pecl.php.net/package/amqp 42 | - Kafka PHP Client:https://github.com/weiboad/kafka-php -------------------------------------------------------------------------------- /03-SSO 单点登录.md: -------------------------------------------------------------------------------- 1 | ## 概念 2 | 3 | SSO 英文全称 Single Sign On,单点登录。 4 | 5 | 在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统。 6 | 7 | 比如:淘宝网(www.taobao.com),天猫网(www.tmall.com),聚划算(ju.taobao.com),飞猪网(www.fliggy.com)等,这些都是阿里巴巴集团的网站。在这些网站中,我们在其中一个网站登录了,再访问其他的网站时,就无需再进行登录,这就是 SSO 的主要用途。 8 | 9 | ## 好处 10 | 11 | #### 用户角度 12 | 13 | 用户能够做到一次登录多次使用,无需记录多套用户名和密码,省心。 14 | 15 | #### 系统管理员角度 16 | 17 | 管理员只需维护好一个统一的账号中心就可以了,方便。 18 | 19 | #### 新系统开发角度 20 | 21 | 新系统开发时只需直接对接统一的账号中心即可,简化开发流程,省时。 22 | 23 | ## 技术实现 24 | 25 | #### 流程图 26 | 27 | ![](https://github.com/xinliangnote/PHP/blob/master/images/3_php_1.png) 28 | 29 | #### 流程介绍 30 | 31 | 如果没这个介绍,看上图肯定是懵懵的。 32 | 33 | 系统A和系统B都是前后端分离的,比如前端框架用的 React / Vue / Angular,都是通过 NPM 编译后独立部署的,前后端完全通过HTTP接口的方式进行交互,也有可能前后端项目的域名都不一样。 34 | 35 | SSO认证中心不是前后端分离的,就是前端代码和后端代码部署在一个项目中。 36 | 37 | 为什么用这两种情况呢? 38 | 39 | 其实就是为了,在流程图上出现这两种情况,这样的清楚了,后期改成任何一种就都清楚了。 40 | 41 | 试想一下: 42 | 43 | 三个系统都是前后端分离的情况,流程图应该怎么调整? 44 | 45 | 三个系统都不是前后端分离的情况,流程图应该怎么调整? 46 | 47 | #### 对外接口 48 | 49 | 系统A和系统B:用户退出接口。 50 | 51 | SSO 认证中心:用户退出接口和token验证接口。 52 | 53 | #### 登录 54 | 55 | 如上述流程图一致。 56 | 57 | 系统A和系统B:使用token认证登录。 58 | 59 | SSO 认证中心:使用会话认证登录。 60 | 61 | 前后端分离项目,登录使用token进行解决,前端每次请求接口时都必须传递token参数。 62 | 63 | #### 退出 64 | 65 | ![](https://github.com/xinliangnote/PHP/blob/master/images/3_php_2.png) 66 | 67 | 上图,表示的是从某一个系统退出的流程图。 68 | 69 | 退出,还可以从SSO认证中心退出,然后调取各个系统的用户退出接口。 70 | 71 | 当用户再进行操作的时候,就会跳转到SSO的登录界面。 72 | 73 | #### Token 生成方式 74 | 75 | 创建全局会话可以使用session,将session存储到redis中。 76 | 77 | 令牌的生成可以使用JWT。 78 | 79 | PHP JWT参考地址:https://github.com/lcobucci/jwt 80 | 81 | 当然还可以自定义token的生成方式。 82 | 83 | ## 小结 84 | 85 | 讲解了什么是SSO,以及SSO的用途与好处,同时根据流程图一步步进行梳理,基本上就可以实现了。 86 | 87 | 期间遇到任何问题,都可以关注公众号和我进行交流。 88 | 89 | ## 扩展 90 | 91 | #### SSO与OAuth的区别 92 | 93 | 谈到SSO很多人就想到OAuth,也有谈到OAuth想到SSO的,在这里我简单的说一下区别。 94 | 95 | 通俗的解释,SSO是处理一个公司内的不同应用系统之间的登录问题,比如阿里巴巴旗下有很多应用系统,我们只需要登录一个系统就可以实现不同系统之间的跳转。 96 | 97 | OAuth是不同公司遵循的一种授权方案,也是一种授权协议,通常都是由大公司提供,比如腾讯,微博。我们常用的QQ登录,微博登录等,使用OAuth的好处是可以使用其他第三方账号进行登录系统,减少了因用户懒,不愿注册而导致用户流失的风险。 98 | 99 | 现在一些支付业务也用OAuth,比如微信支付,支付宝支付。 100 | 101 | 还有一些开放平台也用OAuth,比如百度开放平台,腾讯开放平台。 102 | 103 | #### SSO与RBAC的关系 104 | 105 | 如果企业有多个管理系统,现由原来的每个系统都有一个登录,调整为统一登录认证。 106 | 107 | 那么每个管理系统都有权限控制,吸取统一登录认证的经验,我们也可以做一套统一的RBAC权限认证。 108 | -------------------------------------------------------------------------------- /08-我眼中的 RPC.md: -------------------------------------------------------------------------------- 1 | ## 什么是 RPC ? 2 | 3 | RPC 是一种框架或者说一种架构,主要目标就是让远程服务调用更简单、透明,**调用远程就像调用本地一样。** 4 | 5 | 百度百科解释: 6 | 7 | > RPC(Remote Procedure Call) - 远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。 8 | 9 | ## 什么情况下使用 RPC ? 10 | 11 | 如果我们开发简单的应用,业务流程简单、流量不大,根本用不着 RPC。 12 | 13 | 当我们的应用访问量增加和业务增加时,发现单机已无法承受,此时可以根据不同的业务(划分清楚业务逻辑)拆分成几个互不关联的应用,分别部署在不同的机器上,此时可能也不需要用到 RPC 。 14 | 15 | 随着我们的业务越来越多,应用也越来越多,应用与应用相互关联调用,发现有些功能已经不能简单划分开,此时可能就需要用到 RPC。 16 | 17 | 比如,我们开发电商系统,需要拆分出用户服务、商品服务、优惠券服务、支付服务、订单服务、物流服务、售后服务等等,这些服务之间都相互调用,这时内部调用最好使用 RPC ,同时每个服务都可以独立部署,独立上线。 18 | 19 | 也就说当我们的项目太大,需要解耦服务,扩展性强、部署灵活,这时就要用到 RPC ,主要解决了分布式系统中,服务与服务之间的调用问题。 20 | 21 | ## RPC 框架原理 22 | 23 | ![](https://github.com/xinliangnote/PHP/blob/master/images/8_php_1.png) 24 | 25 | RPC 架构主要包括三部分: 26 | 27 | - 服务注册中心(Registry),负责将本地服务发布成远程服务,管理远程服务,提供给服务消费者使用。 28 | - 服务提供者(Server),提供服务接口定义与服务实现类。 29 | - 服务消费者(Client),通过远程代理对象调用远程服务。 30 | 31 | 服务提供者启动后主动向服务注册中心(Registry)注册机器IP、端口以及提供的服务列表; 32 | 33 | 服务消费者启动时向服务注册中心(Registry)获取服务提供方地址列表。 34 | 35 | 服务注册中心(Registry)可实现负载均衡和故障切换。 36 | 37 | ## RPC 调用过程 38 | 39 | ![](https://github.com/xinliangnote/PHP/blob/master/images/8_php_2.jpeg) 40 | 41 | (1) 客户端(client)以本地调用方式调用服务; 42 | 43 | (2) 客户端存根(client stub)接收到调用后,负责将方法、参数等组装成能够进行网络传输的消息体(将消息体对象序列化为二进制); 44 | 45 | (3) 客户端通过 sockets 将消息发送到服务端; 46 | 47 | (4) 服务端存根(server stub)收到消息后进行解码(将消息对象反序列化); 48 | 49 | (5) 服务端存根(server stub)根据解码结果调用本地的服务; 50 | 51 | (6) 本地服务执行并将结果返回给服务端存根(server stub); 52 | 53 | (7) 服务端存根(server stub)将返回结果打包成消息(将结果消息对象序列化); 54 | 55 | (8) 服务端(server)通过 sockets 将消息发送到客户端; 56 | 57 | (9) 客户端存根(client stub)接收到结果消息,并进行解码(将结果消息发序列化); 58 | 59 | (10) 客户端(client)得到最终结果。 60 | 61 | RPC 就是要把 2、3、4、7、8、9 这些步骤都封装起来。 62 | 63 | ## RPC 优点 64 | 65 | - 跨语言(C++、PHP、Java、Python ...) 66 | - 协议私密,安全性较高 67 | - 数据传输效率高 68 | - 支持动态扩展 69 | 70 | ## RPC 缺点 71 | 72 | 一个完善的 RPC框架 开发难度大,需要的专业人员比较多,对初学者难度比较大。 73 | 74 | ## PHP RPC 有哪些? 75 | 76 | - Thrift:http://thrift.apache.org/ 77 | - gRPC:http://doc.oschina.net/grpc 78 | - Yar:https://www.php.net/manual/zh/book.yar.php 79 | - Swoole-RPC:https://wiki.swoole.com/wiki/page/683.html 80 | - Hprose:https://hprose.com/ 81 | 82 | ## 小结 83 | 84 | 这篇文章分享了,我认为的 RPC 是什么样的。 85 | 86 | 主要包括在什么场景下使用 RPC,RPC 的原理及调用过程,还有 RPC 的优缺点和 PHP 常用的 RPC 框架。 87 | 88 | 等我研究了具体某一种 RPC,再分享给大家。 89 | 90 | -------------------------------------------------------------------------------- /00-架构/02-配置中心-Apollo.md: -------------------------------------------------------------------------------- 1 | ## Apollo 2 | 3 | #### 简介 4 | 5 | Apollo 是协程框架部门研发的开源配置管理中心,能够集中化管理应用 不同环境、不同集群的配置。 6 | 7 | 配置修改后,能够实时推送给应用端。 8 | 9 | 支持多种客户端:`Java`、 `.Net`、 `Go`、 `Python`、 `NodeJS`、 `PHP`。 10 | 11 | 安装文档:https://github.com/ctripcorp/apollo/wiki/Quick-Start 12 | 13 | 官方文档:https://github.com/ctripcorp/apollo/wiki 14 | 15 | #### 核心参数 16 | 17 | - AppID:这个是创建之后生成的。 18 | - Cluster:选择某个集群的配置项。 19 | - NameSpaceNames:配置项的集合,默认为:application。 20 | - CacheDir:本地的缓存配置项的目录。 21 | - IP:不同的环境配置 IP,比如,测试环境,预上线环境,发布环境。 22 | 23 | #### Go 客户端 24 | 25 | agollo is a golang client for ctrip apollo config center 26 | 27 | 链接:https://github.com/philchia/agollo 28 | 29 | 案例:项目配置如下 30 | 31 | ```Go 32 | type Config struct { 33 | API_ADDRESS string `apollo:"API_ADDRESS" namespace:"application" default:""` 34 | } 35 | ``` 36 | 37 | 当项目启动时: 38 | 39 | 1、启动 Apollo : 40 | 41 | ```go 42 | func ApolloStart() { 43 | var AppID = "" 44 | var Cluster = "" 45 | var NameSpace = "" 46 | var CacheDir = "" 47 | var IP = "" 48 | config := agollo.Conf{ 49 | AppID : AppID, 50 | Cluster : Cluster, 51 | NameSpaceNames : NameSpace, 52 | CacheDir : CacheDir, 53 | IP : IP 54 | } 55 | agollo.StartWithConf(&config) 56 | } 57 | ``` 58 | 59 | 60 | 61 | 2、拉取 Apollo 配置: 62 | 63 | ```go 64 | func InitApolloConfig() { 65 | conf := Config 66 | v := reflect.ValueOf(conf).Elem() 67 | for i := 0; i < v.NumField(); i++ { 68 | fieldInfo := v.Type().Field(i) 69 | 70 | apolloKey := fieldInfo.Tag.Get("apollo") 71 | apolloNamespace := fieldInfo.Tag.Get("namespace") 72 | defaultValue := fieldInfo.Tag.Get("default") 73 | 74 | val := agollo.GetStringValueWithNameSpace(apolloNamespace, apolloKey, defaultValue) 75 | v.FieldByName(string(fieldInfo.Name)).Set(reflect.ValueOf(val)) 76 | } 77 | } 78 | ``` 79 | 80 | 81 | 82 | 3、订阅 Apollo 更新: 83 | 84 | ```Go 85 | func SubscribeToUpdate() { 86 | for _, v := range NameSpaceNames { 87 | agollo.SubscribeToNamespaces(v) 88 | } 89 | events := agollo.WatchUpdate() 90 | for { 91 | changeEvent := <-events 92 | // 更新本地配置 93 | updateLocalConfig(changeEvent) 94 | } 95 | } 96 | 97 | func updateLocalConfig(event *agollo.ChangeEvent) { 98 | conf := Config 99 | for apollo_k, apollo_v: range event.Changes { 100 | v := reflect.ValueOf(conf).Elem() 101 | for i := 0; i < v.NumField(); i++ { 102 | fieldInfo := v.Type().Field(i) 103 | 104 | apolloKey := fieldInfo.Tag.Get("apollo") 105 | apolloNamespace := fieldInfo.Tag.Get("namespace") 106 | 107 | if event.Namespace == apolloNamespace && apollo_k == apolloKey { 108 | v.FieldByName(string(fieldInfo.Name)).Set(reflect.ValueOf(apollo_v.NewValue)) 109 | } 110 | } 111 | } 112 | } 113 | ``` 114 | 115 | -------------------------------------------------------------------------------- /01-PHP 浮点数高精度运算.md: -------------------------------------------------------------------------------- 1 | ## 概述 2 | 3 | 记录下,工作中遇到的坑 ... 4 | 5 | 关于 PHP 浮点数运算,特别是金融行业、电子商务订单管理、数据报表等相关业务,利用浮点数进行加减乘除时,稍不留神运算结果就会出现偏差,轻则损失几十万,重则会有信誉损失,甚至吃上官司,我们一定要引起高度重视! 6 | 7 | ## 浮点数运算的“锅” 8 | 9 | ``` 10 | //加 11 | $a = 0.1; 12 | $b = 0.7; 13 | $c = intval(($a + $b) * 10); 14 | echo $c."
"; 15 | //输出:7 16 | 17 | //减 18 | $a = 100; 19 | $b = 99.98; 20 | $c = $a - $b; 21 | echo $c."
"; 22 | //输出:0.019999999999996 23 | 24 | //乘 25 | $a = 0.58; 26 | $b = 100; 27 | $c = intval($a * $b); 28 | echo $c."
"; 29 | //输出:57 30 | 31 | //除 32 | $a = 0.7; 33 | $b = 0.1; 34 | $c = intval($a / $b); 35 | echo $c."
"; 36 | //输出:6 37 | ``` 38 | 39 | 上面的结果,显然不是我们想要的! 40 | 41 | PHP 官方手册解释如下: 42 | 43 | > 浮点数的精度有限。尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16。非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递。永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。如果确实需要更高的精度,应该使用**任意精度数学函数** 或者 **gmp 函数**。 44 | 45 | 这里的关键在于,浮点数的小数用二进制的表示,过程如下: 46 | 47 | - 将小数乘以2,取整数部分表示第一位; 48 | - 将小数部分乘以2,取整数部分表示第二位; 49 | - 再将小数部分乘以2,取整数部分表示第三位; 50 | - ... 依次类推,直到小数部分为0; 51 | 52 | 例:0.58 53 | 54 | - 0.58 * 2 = 1.16 ---> 1 55 | - 0.16 * 2 = 0.32 ---> 0 56 | - 0.32 * 2 = 0.64 ---> 0 57 | - 0.64 * 2 = 1.28 ---> 1 58 | - 0.28 * 2 = 0.56 ---> 0 59 | - 0.56 * 2 = 1.12 ---> 1 60 | - 0.12 * 2 = 0.24 ---> 0 61 | - 0.24 * 2 = 0.48 ---> 0 62 | - 0.48 * 2 = 0.96 ---> 0 63 | - 0.96 * 2 = 1.92 ---> 1 64 | - ... 65 | 66 | 我们会得到一个无限循环的二进制小数: 67 | 68 | 0.1001010001... 69 | 70 | 小数部分出现循环,有限的二进制位无法准确的表示一个小数,这也就是小数运算出现误差的原因。 71 | 72 | 接下来给大家介绍 **任意精度数学函数**。 73 | 74 | ## 任意精度数学函数 75 | 76 | 对于任意精度的数学,PHP 提供了支持用字符串表示的任意大小和精度的数字的二进制计算。 77 | 78 | BCMath:BC 是 Binary Calculator 的缩写。 79 | 80 | 官方手册:http://php.net/manual/zh/book.bc.php 81 | 82 | 大家在使用前,请先确认是否已安装 bcmath。 83 | 84 | ``` 85 | //加 86 | $a = 0.1; 87 | $b = 0.7; 88 | $c = intval(bcadd($a, $b, 1) * 10); 89 | echo $c."
"; 90 | //输出:8 91 | 92 | //减 93 | $a = 100; 94 | $b = 99.98; 95 | $c = bcsub($a, $b, 2); 96 | echo $c."
"; 97 | //输出:0.02 98 | 99 | //乘 100 | $a = 0.58; 101 | $b = 100; 102 | $c = intval(bcmul($a, $b)); 103 | echo $c."
"; 104 | //输出:58 105 | 106 | //除 107 | $a = 0.7; 108 | $b = 0.1; 109 | $c = intval(bcdiv($a, $b)); 110 | echo $c."
"; 111 | //输出:7 112 | ``` 113 | 114 | 除了加减乘除,bcmath 还提供了以下方法: 115 | 116 | - bccomp 比较两个任意精度的数字 117 | - bcmod 对一个任意精度数字取模 118 | - bcpow 任意精度数字的乘方 119 | - bcpowmod 高精度数字乘方求模 120 | - bcscale 设置所有bc数学函数的默认小数点保留位数 121 | - bcsqrt 任意精度数字的二次方根 122 | 123 | ## 常用数值处理方案 124 | 125 | #### 舍去法取整(向下取整) 126 | 127 | ``` 128 | echo floor(5.1); 129 | //输出:5 130 | 131 | echo floor(8.8); 132 | //输出:8 133 | ``` 134 | 135 | #### 进一法取整(向上取整) 136 | 137 | ``` 138 | echo ceil(5.1); 139 | //输出:6 140 | 141 | echo ceil(8.8); 142 | //输出:9 143 | ``` 144 | 145 | #### 普通四舍五入法 146 | 147 | ``` 148 | echo round(5.1); 149 | //输出:5 150 | 151 | echo round(8.8); 152 | //输出:9 153 | 154 | //保留两位小数并且进行四舍五入 155 | echo round(5.123, 2); 156 | //输出:5.12 157 | 158 | echo round(8.888, 2); 159 | //输出:8.89 160 | 161 | //保留两位小数并且不进行四舍五入 162 | echo substr(round(5.12345, 3), 0, -1); 163 | //输出:5.12 164 | 165 | echo substr(round(8.88888, 3), 0, -1); 166 | //输出:8.88 167 | ``` 168 | 169 | #### 银行家舍入法 170 | 171 | 四舍六入五考虑,五后非空就进一,五后为空看奇偶,五前为偶应舍去,五前为奇要进一。 172 | 173 | 保留两位小数,例: 174 | 175 | - 1.2849 = 1.28 -> 四舍 176 | - 1.2866 = 1.29 -> 六入 177 | - 1.2851 = 1.29 -> 五后非空就进一 178 | - 1.2850 = 1.28 -> 五后为空看奇偶,五前为偶应舍去 179 | - 1.2750 = 1.28 -> 五后为空看奇偶,五前为奇要进一 180 | 181 | 实现代码如下: 182 | 183 | ``` 184 | echo round(1.2849, 2, PHP_ROUND_HALF_EVEN); 185 | //输出:1.28 186 | 187 | echo round(1.2866, 2, PHP_ROUND_HALF_EVEN); 188 | //输出:1.29 189 | 190 | echo round(1.2851, 2, PHP_ROUND_HALF_EVEN); 191 | //输出:1.29 192 | 193 | echo round(1.2850, 2, PHP_ROUND_HALF_EVEN); 194 | //输出:1.28 195 | 196 | echo round(1.2750, 2, PHP_ROUND_HALF_EVEN); 197 | //输出:1.28 198 | ``` 199 | 200 | 更多 round 使用说明,请查阅官方手册: 201 | 202 | http://php.net/manual/zh/function.round.php 203 | 204 | #### 数值格式化(千位分组) 205 | 206 | 应用于金额的展示,比如我们经常会看的银行卡余额。 207 | 208 | ``` 209 | echo number_format('10000.98', 2, '.', ','); 210 | //输出:10,000.98 211 | 212 | echo number_format('340888999', 2, '.', ','); 213 | //输出:340,888,999.00 214 | ``` 215 | 216 | ## 扩展 217 | 218 | #### MySQL 浮点型字段 219 | 220 | 在 MySQL 中,创建表字段时也有浮点数类型。 221 | 222 | 浮点数类型包括单精度浮点数(float)和双精度浮点数(double)。 223 | 224 | **同理,不建议使用浮点数类型!!!** 225 | 226 | 浮点数存在误差,当我们使用精度敏感的数据时,应该使用定点数(decimal)进行存储。 227 | 228 | ## 小结 229 | 230 | 通过浮点数精度的问题,了解到浮点数的小数用二进制的表示。 231 | 232 | 分享了用 **PHP 任意精度数学函数**,来进行高精度运算。 233 | 234 | 同时分享了常用数值处理方案,比如舍去法、进一法、四舍五入法、银行家舍入法、数值格式化 等。 235 | 236 | 最后,通过 PHP 的 float 联想到 MySQL 的 float。 237 | 238 | 以后,在使用浮点数运算的时候,一定要慎之又慎,细节决定成败。 -------------------------------------------------------------------------------- /06-三个水桶等分8升水的问题 -《算法的乐趣》.md: -------------------------------------------------------------------------------- 1 | ## 智力题目 2 | 3 | 有三个容积分别为3升、5升、8升的水桶,其中容积为8升的水桶中装满了水,容积为3升和容积为5升的水桶都是空的。三个水桶都没有刻度,现在需要将大水桶中的8升水等分成两份,每份都是4升水,附加条件是只能这三个水桶,不能借助其他辅助容器。 4 | 5 | “恩,是的,这是一个很经典的问题。” 6 | 7 | “然而,我们并不能想全,不信请继续往下看。” 8 | 9 | ## 答案 10 | 11 | ”废话不多说,直接看方法吧。“ 12 | 13 | #### 第一种(7步) 14 | 1. 将8L的水桶中的水,倒满5L的水桶,这时:8L水桶为3L、5L水桶为5L、3L水桶为0L 15 | 2. 将5L的水桶中的水,倒满3L的水桶,这时:8L水桶为3L、5L水桶为2L、3L水桶为3L 16 | 3. 将3L的水桶中的水,倒入8L的水桶,这时:8L水桶为6L、5L水桶为2L、3L水桶为0L 17 | 4. 将5L的水桶中的水,倒入3L的水桶,这时:8L水桶为6L、5L水桶为0L、3L水桶为2L 18 | 5. 将8L的水桶中的水,倒入5L的水桶,这时:8L水桶为1L、5L水桶为5L、3L水桶为2L 19 | 6. 将5L的水桶中的水,倒满3L的水桶,这时:8L水桶为1L、5L水桶为4L、3L水桶为3L 20 | 7. 将3L的水桶中的水,倒入8L的水桶,这时:8L水桶为4L、5L水桶为4L、3L水桶为0L 21 | 22 | #### 第二种(8步) 23 | 24 | 1. 将8L的水桶中的水,倒满3L的水桶,这时:8L水桶为5L、5L水桶为0L、3L水桶为3L 25 | 2. 将3L的水桶中的水,倒入5L的水桶,这时:8L水桶为5L、5L水桶为3L、3L水桶为0L 26 | 3. 将8L的水桶中的水,倒满3L的水桶,这时:8L水桶为2L、5L水桶为3L、3L水桶为3L 27 | 4. 将3L的水桶中的水,倒满5L的水桶,这时:8L水桶为2L、5L水桶为5L、3L水桶为1L 28 | 5. 将5L的水桶中的水,倒入8L的水桶,这时:8L水桶为7L、5L水桶为0L、3L水桶为1L 29 | 6. 将3L的水桶中的水,倒入5L的水桶,这时:8L水桶为7L、5L水桶为1L、3L水桶为0L 30 | 7. 将8L的水桶中的水,倒满3L的水桶,这时:8L水桶为4L、5L水桶为1L、3L水桶为3L 31 | 8. 将3L的水桶中的水,倒入5L的水桶,这时:8L水桶为4L、5L水桶为4L、3L水桶为0L 32 | 33 | 我相信答案肯定不止两个,到底有多少种答案? 34 | 35 | 带着这个疑问,我们来设计一个算法吧。 36 | 37 | ## 问题分析 38 | 39 | #### 人的思维 40 | 41 | 解决这个问题的关键是怎么通过倒水凑出确定的1升水或能容纳1升水的空间。 42 | 43 | 例如,当8L水桶或5L水桶或3L水桶有1L水时,都能快速倒出4L水。 44 | 45 | #### 计算机思维 46 | 47 | **“穷举法”** 48 | 49 | 水桶初始状态:8L水桶装满水,3L和5L的水桶为空。 50 | 水桶最终状态:3L水桶为空,5L和8L的水桶各4L水。 51 | 52 | 假设将每个状态下三个水桶中的水的体积作为status。 53 | 54 | 从 $status = array(8,0,0) 得到 $status = array(4,4,0)。 55 | 56 | 当然还会有一些限制: 57 | 58 | 1.各个水桶的都有最大值: 59 | 60 | 0 <= status[0] <= 8; 61 | 62 | 0 <= status[1] <= 5; 63 | 64 | 0 <= status[2] <= 3; 65 | 66 | 2.当前倒水之后各个水桶的状态,与历史倒水之后各个水桶的状态,不能相同。 67 | 68 | 3.当前水桶为空时,不能倒给其他水桶。 69 | 70 | 4.当前水桶为最大容积时,其他水桶不能再向这个水桶倒水。 71 | 72 | ## 程序代码(PHP) 73 | 74 | ``` 75 | /** 76 | * 三个水桶等分8升水的问题,代码示例(php) 77 | * @author 訢亮 78 | */ 79 | 80 | $bucket_limit = [8, 5, 3]; 81 | $bucket_value = [8, 0, 0]; 82 | $bucket = new Bucket($bucket_value, $bucket_limit, []); 83 | $result = $bucket->getResult(); 84 | 85 | echo "一共有 ".count($result)." 种倒水方法,方法如下:
";
 86 | print_r($result);
 87 | 
 88 | class Bucket
 89 | {
 90 | 
 91 |     static protected $_change_bucket_path = []; //倒水的过程记录
 92 | 
 93 |     protected $_bucket_values;  //每个水桶的当前容积
 94 |     protected $_bucket_limit;   //每个水桶的容积阈值
 95 |     protected $_history_status; //所有历史水桶容积状态的集合
 96 | 
 97 |     public function __construct($bucket_value = [], $bucket_limit = [], $history_status = [])
 98 |     {
 99 |         $this->_bucket_values  = $bucket_value;
100 |         $this->_bucket_limit   = $bucket_limit;
101 |         $this->_history_status = array_merge($history_status, [$this->_bucket_values]);
102 | 
103 |         $this->run();
104 |     }
105 | 
106 | 
107 |     public function run() {
108 |         for ($i=0; $i_bucket_values); $i++) {
109 |             for ($j=$i+1; $j_bucket_values); $j++) {
110 |                 $this->changeBucketValue($i, $j);
111 |                 $this->changeBucketValue($j, $i);
112 |             }
113 |         }
114 |     }
115 | 
116 |     public function getResult() {
117 |         return self::$_change_bucket_path;
118 |     }
119 | 
120 | 
121 |     /**
122 |      * 倒水
123 |      * @param int $target_idx  目标水桶(被倒水的水桶)
124 |      * @param int $current_idx 当前水桶(倒水的水桶)
125 |      * @return bool
126 |      */
127 | 
128 |     protected function changeBucketValue($target_idx = 0, $current_idx = 0) {
129 |         $value = $this->_bucket_values;
130 |         if ($target_idx == $current_idx ||
131 |             $this->_bucket_values[$current_idx] == 0 ||
132 |             $this->_bucket_values[$target_idx] == $this->_bucket_limit[$target_idx]
133 |         ) {
134 |             return false;
135 |         }
136 | 
137 |         if (($this->_bucket_limit[$target_idx] - $this->_bucket_values[$target_idx]) <= $this->_bucket_values[$current_idx]) {
138 |             $water = $this->_bucket_limit[$target_idx] - $this->_bucket_values[$target_idx];
139 |         } else {
140 |             $water = $this->_bucket_values[$current_idx];
141 |         }
142 | 
143 |         $value[$target_idx] += $water;
144 |         $value[$current_idx] -= $water;
145 | 
146 |         if ($value === [4,4,0]) {
147 |             self::$_change_bucket_path[] = array_merge($this->_history_status, [$value]);
148 |         } else {
149 |             if (!$this->checkBucketStatus($value)) {
150 |                 new Bucket($value, $this->_bucket_limit, $this->_history_status);
151 |             }
152 |         }
153 |     }
154 | 
155 |     /**
156 |      * 验证当前水桶状态是否存在过
157 |      * @param array $current_status 当前水桶状态
158 |      * @return bool
159 |      */
160 |     protected function checkBucketStatus($current_status = []) {
161 |         foreach ($this->_history_status as $k) {
162 |             if ($current_status === $k) {
163 |                 return true;
164 |             }
165 |         }
166 |         return false;
167 |     }
168 | }
169 | ```
170 | 
171 | ## 运行结果
172 | 
173 | 一共有 16 种倒水方法,方法如下:
174 | 
175 | ...
176 | 
177 | (16种方法,贴上去太长了,大家在本地尝试下,如需要源码,请关注公众号进行留言。)
178 | 
179 | ## 小结
180 | 
181 | 运行代码之后,一共找到了 16 种倒水的方法,最快的方法需要 7 个步骤。
182 | 
183 | “怎么样,是不是没想到会有这么多方法吧,去考考你身边的小伙伴吧。”
184 | 


--------------------------------------------------------------------------------
/04-PHP WEB 安全防御.md:
--------------------------------------------------------------------------------
  1 | ## 常见漏洞
  2 | 
  3 | ![](https://github.com/xinliangnote/PHP/blob/master/images/4_php_1.png)
  4 | 
  5 | 看到上图的漏洞是不是特别熟悉,如果不进行及时防御,就会产生蝴蝶效应。
  6 | 
  7 | 往下看,可能会找到你要的答案。
  8 | 
  9 | ## SQL注入攻击
 10 | 
 11 | #### 定义
 12 | 
 13 | SQL注入攻击是通过WEB表单提交,在URL参数提交或Cookie参数提交,将怀有恶意的“字符串”,提交给后台数据库,欺骗服务器执行恶意的SQL语句。
 14 | 
 15 | #### 案例
 16 | 
 17 | ```
 18 | //以用户登录为例,当验证用户名和密码是否正确时
 19 | $sql = "SELECT * FROM user WHERE 
 20 |         username = '".$_GET['username']."' AND 
 21 |         password = '".$_GET['password']."'";
 22 | ```
 23 | 用户恶意输入:
 24 | 
 25 | ```
 26 | $_GET['username'] = "' or 1=1 -- '";
 27 | $_GET['password'] = "123456";
 28 | ```
 29 | 
 30 | 注入后的Sql语句:
 31 | 
 32 | ```
 33 | $sql = "SELECT * FROM user WHERE username = '' 
 34 |         or 1=1 -- ''AND password = '123456'";
 35 | ```
 36 | 
 37 | 执行注入后的Sql语句,可以返回 user 表的全部数据。
 38 | 
 39 | 平时我们可以进行自测,比如使用单引号、双引号,如果是数字进行+1或-1。
 40 | 
 41 | SQL注入的危害很大,利用SQL注入可以进行,**拖库**、**删库**、**删表**、**UDF提权**、**读取文件**、*...* 
 42 | 
 43 | 推荐一个开源的自动化的SQL注入工具。
 44 | 
 45 | **SQLmap**:http://sqlmap.org/
 46 | 
 47 | - 支持各种数据库管理系统(MySql、Oracle、SQL Server、SQLite ... )。
 48 | - 支持自动识别密码哈希格式并通过字典破解密码哈希。
 49 | - 支持枚举用户、密码、哈希、权限、角色、数据库、数据表和列。
 50 | - 支持完全地下载某个数据库中的某个表、某个列。
 51 | - 支持在数据库管理系统中搜索指定的数据库名、表名或列名。
 52 | - 支持下载或上传文件。
 53 | - 支持执行任意命令并回现标准输出。
 54 | - 支持布尔型盲注、时间型盲注、基于错误信息的注入、联合查询注入和堆查询注入。
 55 | 
 56 | 尝试着利用工具,注入自己的项目,发现问题,然后解决问题。
 57 | 
 58 | SQL注入的危害,远比我们想象的要大!
 59 | 
 60 | #### 防御
 61 | 
 62 | 推荐解决方案是使用 **PDO** 或 **MySQLi** 的数据库扩展。
 63 | 
 64 | PHP官方文档中介绍,MySQL扩展自PHP 5.5.0起已废弃,并在自PHP7.0.0开始被移除。
 65 | 
 66 | 如果已经在用MySQL扩展了,可以对传入的每个参数做验证,并使用框架的ORM进行查询。
 67 | 
 68 | 另外:addslashes 和 mysql_real_escape_string 这种转义是不安全的!
 69 | 
 70 | ## XSS攻击
 71 | 
 72 | #### 定义
 73 | 
 74 | XSS攻击是一种经常出现在WEB应用中的计算机安全漏洞,通过WEB表单提交或在URL参数提交将代码植入在用户的使用页面上。
 75 | 
 76 | #### 分类
 77 | 
 78 | **存储型**
 79 | 
 80 | 注入的恶意代码存储在服务器上(常用于留言板、论坛帖子、CRM),受害者请求服务器获取信息的时候,这些恶意代码就被浏览器成功执行。
 81 | 
 82 | **反射型**
 83 | 
 84 | 注入的恶意代码没有存储在服务器上,通过引诱用户点击一个链接到目标网站进行实施攻击。
 85 | 
 86 | **DOM型**
 87 | 
 88 | 注入的恶意代码并未显式的包含在web服务器的响应页面中,但会被页面中的js脚本以变量的形式来访问到的方式来进行实施攻击。
 89 | 
 90 | #### 案例
 91 | 
 92 | 存储型:论坛帖子界面input输入框中,输入 `/>` 进行提交。
 93 | 
 94 | 反射型:在浏览器输入框中,输入 `/xxx.php?name=`
 95 | 
 96 | ```
 97 | //DOM型,代码举例
 98 | 
103 | ```
104 | 
105 | XSS的危害有很多,包括盗号,挂马,控制受害者机器想其他网站发起攻击 ...
106 | 
107 | 自测的方法,看见输入框就输入:`/>` 进行提交。
108 | 
109 | 推荐一个专门针对浏览器攻击的框架。
110 | 
111 | **BeEF** :https://beefproject.com/
112 | 
113 | #### 防御
114 | 
115 | 简单的防御可以对style、script、image、src、a等等不安全的因素进行过滤或转义。
116 | 
117 | 可以自己封装一个方法,也可以使用框架的自带方法,比如 xss_clean 。
118 | 
119 | 可以利用一些模板引擎避免XSS攻击,比如Laravel框架使用的Blade,还有twig,Smarty等。
120 | 
121 | 可以利用HTTP-only,将cookie设置成HTTP-only防止XSS攻击。
122 | 
123 | ```
124 | //设置Cookie
125 | setcookie($name, $value, $expire, $path, $domain, $secure, $httponly); 
126 | 
127 | //然后服务端通过使用 $_COOKIE 进行验证。
128 | ```
129 | 
130 | 可以使用Content Security Policy,它的实质就是白名单制度。
131 | 
132 | 入门教程请参考:http://www.ruanyifeng.com/blog/2016/09/csp.html
133 | 
134 | ## SSRF攻击
135 | 
136 | #### 定义
137 | 
138 | SSRF(Server-Side Request Forgery:服务器端请求伪造) 是攻击者伪造服务器端发起的请求,虽然攻击者无法从外网访问内网的系统,但是它通过注入恶意代码从服务端发起,通过服务端就再访问内网的系统,然后获取不该获取的数据。
139 | 
140 | ![](https://github.com/xinliangnote/PHP/blob/master/images/4_php_2.png)
141 | 
142 | #### 案例
143 | 
144 | 漏洞主要产生在包含这些方法的代码中,比如 curl、file_get_contents、fsockopen。
145 | 
146 | ```
147 | //代码片段
148 | set();
  8 | $redis->get();
  9 | $redis->hset();
 10 | $redis->hget();
 11 | ```
 12 | 
 13 | #### 队列
 14 | 
 15 | ```
 16 | //举例
 17 | $redis->rpush();
 18 | $redis->lpop();
 19 | $redis->lrange();
 20 | ```
 21 | 
 22 | #### 发布订阅
 23 | 
 24 | ```
 25 | //举例
 26 | $redis->publish();
 27 | $redis->subscribe();
 28 | ```
 29 | 
 30 | #### 计数器
 31 | 
 32 | ```
 33 | //举例
 34 | $redis->set();
 35 | $redis->incr();
 36 | ```
 37 | 
 38 | #### 排行榜
 39 | 
 40 | ```
 41 | //举例
 42 | $redis->zadd();
 43 | $redis->zrevrange();
 44 | $redis->zrange();
 45 | ```
 46 | #### 集合间操作
 47 | 
 48 | ```
 49 | //举例
 50 | $redis->sadd();
 51 | $redis->spop();
 52 | $redis->sinter();
 53 | $redis->sunion();
 54 | $redis->sdiff();
 55 | ```
 56 | 
 57 | #### 悲观锁
 58 | 
 59 | 解释:悲观锁(Pessimistic Lock), 顾名思义,就是很悲观。
 60 | 
 61 | 每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁。
 62 | 
 63 | 场景:如果项目中使用了缓存且对缓存设置了超时时间。
 64 | 
 65 | 当并发量比较大的时候,如果没有锁机制,那么缓存过期的瞬间,
 66 | 
 67 | 大量并发请求会穿透缓存直接查询数据库,造成雪崩效应。
 68 | 
 69 | ```
 70 | /**
 71 |  * 获取锁
 72 |  * @param  String  $key    锁标识
 73 |  * @param  Int     $expire 锁过期时间
 74 |  * @return Boolean
 75 |  */
 76 | public function lock($key = '', $expire = 5) {
 77 |     $is_lock = $this->_redis->setnx($key, time()+$expire);
 78 |     //不能获取锁
 79 |     if(!$is_lock){
 80 |         //判断锁是否过期
 81 |         $lock_time = $this->_redis->get($key);
 82 |         //锁已过期,删除锁,重新获取
 83 |         if (time() > $lock_time) {
 84 |             unlock($key);
 85 |             $is_lock = $this->_redis->setnx($key, time() + $expire);
 86 |         }
 87 |     }
 88 | 
 89 |     return $is_lock? true : false;
 90 | }
 91 | 
 92 | /**
 93 |  * 释放锁
 94 |  * @param  String  $key 锁标识
 95 |  * @return Boolean
 96 |  */
 97 | public function unlock($key = ''){
 98 |     return $this->_redis->del($key);
 99 | }
100 | 
101 | // 定义锁标识
102 | $key = 'test_lock';
103 | 
104 | // 获取锁
105 | $is_lock = lock($key, 10);
106 | if ($is_lock) {
107 |     echo 'get lock success
'; 108 | echo 'do sth..
'; 109 | sleep(5); 110 | echo 'success
'; 111 | unlock($key); 112 | } else { //获取锁失败 113 | echo 'request too frequently
'; 114 | } 115 | ``` 116 | 117 | #### 乐观锁 118 | 119 | 解释:乐观锁(Optimistic Lock), 顾名思义,就是很乐观。 120 | 121 | 每次去拿数据的时候都认为别人不会修改,所以不会上锁。 122 | 123 | watch命令会监视给定的key,当exec时候如果监视的key从调用watch后发生过变化,则整个事务会失败。 124 | 125 | 也可以调用watch多次监视多个key。这样就可以对指定的key加乐观锁了。 126 | 127 | 注意watch的key是对整个连接有效的,事务也一样。 128 | 129 | 如果连接断开,监视和事务都会被自动清除。 130 | 131 | 当然了exec,discard,unwatch命令都会清除连接中的所有监视。 132 | 133 | ``` 134 | $strKey = 'test_age'; 135 | 136 | $redis->set($strKey,10); 137 | 138 | $age = $redis->get($strKey); 139 | 140 | echo "---- Current Age:{$age} ----

"; 141 | 142 | $redis->watch($strKey); 143 | 144 | // 开启事务 145 | $redis->multi(); 146 | 147 | //在这个时候新开了一个新会话执行 148 | $redis->set($strKey,30); //新会话 149 | 150 | echo "---- Current Age:{$age} ----

"; //30 151 | 152 | $redis->set($strKey,20); 153 | 154 | $redis->exec(); 155 | 156 | $age = $redis->get($strKey); 157 | 158 | echo "---- Current Age:{$age} ----

"; //30 159 | 160 | //当exec时候如果监视的key从调用watch后发生过变化,则整个事务会失败 161 | ``` 162 | 163 | 上面的一些场景,咱们大部分都使用过,却还没有提及到Rdb文件。 164 | 165 | “对吧,使用过Redis,却不知道Rdb文件,你中枪了吗?” 166 | 167 | ## Rdb文件是什么,它是干什么的 168 | 169 | Redis 作为互联网产品开发中不可缺少的常备武器,它性能高、数据结构丰富、简单易用,但同时也是因为太容易用了,不管什么数据、不管这数据有多大、不管数据有多少,通通塞进去,最后导致的问题就是 Redis 内存使用持续上升,但是又不知道里面的数据是不是有用,是否可以拆分和清理,最可怕的是服务器发生宕机后,Redis 数据库里的所有数据将会全部丢失。 170 | 171 | 比如当内存上升,性能慢时,我们进行性能调优的时候,我们想知道: 172 | 173 | - 哪些Key占用了大量的内存? 174 | - 想知道每个Key的占用空间? 175 | - 想知道占用空间大的Key都存了啥? 176 | - 想知道占用空间大的Key的重要性,当性能慢的时候是否可以马上删除? 177 | - 更想知道这些Key是哪个业务方,哪个开发创建的?这样就可以找他沟通了。 178 | 179 | #### 尝试解决问题的思路 180 | 181 | 一、先通过 keys * 命令,拿到所有的 key,然后根据 key 再获取所有的内容。 182 | 183 | - 优点:可以不使用 Redis 机器的硬盘,直接网络传输。 184 | - 缺点:如果 key 数据特别多,keys 命令可能会直接导致 Redis 卡住,从而影响业务使用 或 对 Redis 请求太多次,资源消耗多,遍历数据太慢。 185 | 186 | 二、开启 aof,通过 aof 文件获取所有的数据。 187 | 188 | - 优点:无需影响 Redis 服务,完全离线操作,足够安全。 189 | - 缺点:有一些 Redis 实例写入频繁,不适合开启 aof,普适性不强;aof 文件有可能特别大,传输、解析起来太慢,效率低。 190 | 191 | 三、使用 bgsave,获取 rdb 文件,解析后获取数据。 192 | 193 | - 优点:机制成熟,可靠性好;文件相对小,传输、解析效率高。 194 | - 缺点:bgsave 虽然会 fork 子进程,但还是有可能导致主进程卡住一段时间,对业务有产生影响的风险。 195 | 196 | 综合评估后,决定采用低峰期在从节点做 bgsave 获取 rdb 文件,相对安全可靠,也可以覆盖所有业务的 Redis 集群。 197 | 198 | 也就是说每个实例每天在低峰期自动生成一个 .rdb 文件,即使报表数据有一天的延迟也是可以接受的。 199 | 200 | “哦,原来.rdb文件是磁盘的缓存文件,那么如何开启持久化呢?” 201 | 202 | **下面简单的介绍下,Redis 的持久化。** 203 | 204 | **Redis 支持两种方式的持久化,一种是RDB方式,一种是AOF方式。** 205 | 206 | RDB 是 Redis 用来进行持久化的一种方式,是把当前内存中的数据集,快照写入磁盘。 207 | 208 | #### RDB - 自动 209 | 210 | RDB(Redis DataBase)方式是通过快照完成的,当符合一定条件时Redis会自动将内存中的所有数据进行快照,并且存储到硬盘上,RDB是Redis的默认持久化方式。 211 | 212 | ``` 213 | vim /usr/local/redis/conf/redis.conf 214 | 215 | save 900 1 #15分钟内有至少1个键被更改 216 | save 300 10 #5分钟内至少有10个键被更改 217 | save 60 1000 #1分钟内至少有10000个键被更改 218 | 219 | #以上条件都是或的关系,当满足其一就会进行快照。 220 | 221 | dbfilename "dump.rdb" #持久化文件名称 222 | dir "/data/dbs/redis/6381" #持久化数据文件存放的路径 223 | 224 | #配置文件修改后,需要重启redis服务。 225 | ``` 226 | 227 | 还可以通过命令行的方式进行配置: 228 | 229 | ``` 230 | CONFIG GET save #查看redis持久化配置 231 | 232 | CONFIG SET save "100 20" #修改redis持久化配置 233 | 234 | #使用命令行的方式配置,即时生效,服务器重启后需要重新配置。 235 | ``` 236 | 237 | #### RDB - 手动 238 | 239 | - save 240 | 241 | 该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。 242 | 243 | 显然该命令对于内存比较大的实例会造成长时间阻塞,这是致命的缺陷。 244 | 245 | - bgsave 246 | 247 | 执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。 248 | 249 | 具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段。 250 | 251 | #### AOF 252 | 253 | AOF(APPEND ONLY MODE)是通过保存对redis服务端的写命令(如set、sadd、rpush)来记录数据库状态的,即保存你对redis数据库的写操作。 254 | 255 | 配置日志文件如下: 256 | 257 | ``` 258 | vim /usr/local/redis/conf/redis.conf 259 | dir "/data/dbs/redis/6381" #AOF文件存放目录 260 | appendonly yes #开启AOF持久化,默认关闭 261 | appendfilename "appendonly.aof" #AOF文件名称(默认) 262 | appendfsync no #AOF持久化策略 263 | auto-aof-rewrite-percentage 100 #触发AOF文件重写的条件(默认) 264 | auto-aof-rewrite-min-size 64mb #触发AOF文件重写的条件(默认) 265 | 266 | #上面的每个参数,可以找资料了解下,不做多解释了。 267 | ``` 268 | 269 | RDB 与 AOF 的优缺点,见上面的即可。 270 | 271 | 至此,我们了解了 Redis 持久化的一些配置,里面的细节建议查询相关资料进行研究。 272 | 273 | 接下来继续,通过上一步我们拿到了 rdb 文件,就相当于拿到了Redis实例的数据。 274 | 275 | 1. 解析 rdb 文件,获取key和value的值。 276 | 2. 根据相应的数据结构及内容,估算内存消耗。 277 | 3. 统计并生成报表。 278 | 279 | ## 分析工具 280 | 281 | - 雪球 rdr:https://github.com/xueqiu/rdr 282 | - redis-rdb-tools:https://github.com/sripathikrishnan/redis-rdb-tools 283 | 284 | ## 小结 285 | 286 | 1. 讲解了工作中常用的 redis 使用场景。 287 | 2. 讲解了 redis 持久化的两个方式(RDB、AOF)。 288 | 3. 推荐了两个分析rdb的工具。 289 | 290 | 通过对 redis 的使用 到 了解到服务器上如何对redis数据做持久化快照,再到如何利用工具进行分析rdb文件,最后通过分析后的数据,可以反过来对 redis 的使用提出一些建议。 291 | 292 | 其他知识点也是这样,我们不能只停留在方法的简单调用,就觉得理解了这门技术! 293 | 294 | ## 联想 295 | 296 | 其实上面分析出来的数据,是不可能定位到这个key是哪个业务方的,哪个开发创建的,是否重要等等,那我们应该怎么做呢? 297 | 298 | 1. 制定开发团队的Redis Key的使用规范,通过key的命名可以得到: 299 | - 属于什么业务(加域名表示) 300 | - 属于什么数据类型(加数据类型标示) 301 | - 是否设置过期时间 302 | - ... 303 | 304 | 2. 统一平台进行Redis Key的申请,只有申请了才能进行使用: 305 | - 填写申请人 306 | - 填写申请时间 307 | - 填写申请业务方 308 | - 填写数据类型 309 | - 填写Key的重要性 310 | - 填写Key是否存在过期时间 311 | - 根据填写项生成规范的key名称 312 | - ...(等等需要标记的) 313 | 314 | 3. 上面我们已经能分析出某个redis实例rdb文件的内容,通过分析出来的内容 与 统一平台申请的数据,进行整合,分析key的合格率、内存使用量、不同数据类型的分布、内存占用量Top 100的值 等等。 315 | 316 | 4. 我们可以通过运维了解到,每个服务器与实例之间的配置关系,就可以了解到某台服务器(N个实例)上的 key的合格率、内存使用量、不同数据类型的分布、内存占用量Top 100的值等等。 317 | 318 | 这样,在后台系统中就可以看到哪台服务器,哪个实例的使用情况,解决了Redis滥用并无法进行监控的问题。 319 | -------------------------------------------------------------------------------- /05-PHP 缓存技术.md: -------------------------------------------------------------------------------- 1 | ## 概述 2 | 3 | ![](https://github.com/xinliangnote/PHP/blob/master/images/5_php_1.png) 4 | 5 | 缓存已经成了项目中是必不可少的一部分,它是提高性能最好的方式,例如减少网络I/O、减少磁盘I/O 等,使项目加载速度变的更快。 6 | 7 | 缓存可以是CPU缓存、内存缓存、硬盘缓存,不同的缓存查询速度也不一样(CPU缓存 > 内存缓存 > 硬盘缓存)。 8 | 9 | 接下来,给大家逐一进行介绍。 10 | 11 | ## 浏览器缓存 12 | 13 | 浏览器将请求过的页面存储在客户端缓存中,当访问者再次访问这个页面时,浏览器就可以直接从客户端缓存中读取数据,减少了对服务器的访问,加快了网页的加载速度。 14 | 15 | #### 强缓存 16 | 17 | 用户发送的请求,直接从客户端缓存中获取,不请求服务器。 18 | 19 | 根据 Expires 和 Cache-Control 判断是否命中强缓存。 20 | 21 | 代码如下: 22 | 23 | ``` 24 | header('Expires: '. gmdate('D, d M Y H:i:s', time() + 3600). ' GMT'); 25 | header("Cache-Control: max-age=3600"); //有效期3600秒 26 | ``` 27 | 28 | Cache-Control 还可以设置以下参数: 29 | 30 | - public:可以被所有的用户缓存(终端用户的浏览器/CDN服务器) 31 | - private:只能被终端用户的浏览器缓存 32 | - no-cache:不使用本地缓存 33 | - no-store:禁止缓存数据 34 | 35 | #### 协商缓存 36 | 37 | 用户发送的请求,发送给服务器,由服务器判定是否使用客户端缓存。 38 | 39 | 代码如下: 40 | 41 | ``` 42 | $last_modify = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']); 43 | if (time() - $last_modify < 3600) { 44 | header('Last-Modified: '. gmdate('D, d M Y H:i:s', $last_modify).' GMT'); 45 | header('HTTP/1.1 304'); //Not Modified 46 | exit; 47 | } 48 | header('Last-Modified: '. gmdate('D, d M Y H:i:s').' GMT'); 49 | ``` 50 | 51 | #### 用户操作行为对缓存的影响 52 | 53 | 操作行为 | Expires | Last-Modified 54 | --- | --- | --- 55 | 地址栏回车 | 有效 | 有效 56 | 页面跳转 | 有效 | 有效 57 | 新开窗口 | 有效 | 有效 58 | 前进/后退 | 有效 | 有效 59 | F5刷新 | 无效 | 有效 60 | Ctrl+F5刷新 | 无效 | 无效 61 | 62 | ## 文件缓存 63 | 64 | #### 数据文件缓存 65 | 66 | 将更新频率低,读取频率高的数据,缓存成文件。 67 | 68 | 比如,项目中多个地方用到城市数据做三级联动,我们就可以将城市数据缓存成一个文件(city_data.json),JS 可以直接读取这个文件,无需请求后端服务器。 69 | 70 | #### 全站静态化 71 | 72 | CMS(内容管理系统),也许大家都比较熟悉,比如早期的 DEDE、PHPCMS,后台都可以设置静态化HTML,用户在访问网站的时候读取的都是静态HTML,不用请求后端的数据库,也不用Ajax请求数据接口,加快了网站的加载速度。 73 | 74 | 静态化HTML有以下优点: 75 | 76 | - 有利于搜索引擎的收录(SEO) 77 | - 页面打开速度快 78 | - 减少服务器负担 79 | 80 | #### CDN缓存 81 | 82 | CDN(Content Delivery Network)内容分发网络。 83 | 84 | 用户访问网站时,自动选择就近的CDN节点内容,不需要请求源服务器,加快了网站的打开速度。 85 | 86 | 缓存主要包括 HTML、图片、CSS、JS、XML 等静态资源。 87 | 88 | 89 | ## NoSQL缓存 90 | 91 | #### Memcached 缓存 92 | 93 | Memcached 是高性能的分布式内存缓存服务器。 94 | 95 | 一般的使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。 96 | 97 | 它也能够用来存储各种格式的数据,包括图像、视频、文件等。 98 | 99 | Memcached 仅支持K/V类型的数据,不支持持久化存储。 100 | 101 | **Memcache 与 Memcached 的区别** 102 | 103 | - Memcached 从0.2.0开始,要求PHP版本>=5.2.0,Memcache 要求PHP版本>=4.3。 104 | - Memcached 最后发布时间为2018-12-24,Memcache 最后发布时间2013-04-07。 105 | - Memcached 基于libmemcached,Memcache 基于PECL扩展。 106 | 107 | 可以将 Memcached 看作是 Memcache 的升级版。 108 | 109 | PHP Memcached 使用手册: 110 | 111 | http://www.php.net/manual/zh/book.memcached.php 112 | 113 | Memcached 经常拿来与 Redis 做对比,接下来介绍下 Redis 缓存。 114 | 115 | #### Redis缓存 116 | 117 | Redis 是一个高性能的 K/V 数据库。 118 | 119 | Redis 很大程度补偿了 Memcached K/V存储的不足,比如 List(链表)、Set(集合)、Zset(有序集合)、Hash(散列),既可以将数据存储在内存中,也可以将数据持久化到磁盘上,支持主从同步。 120 | 121 | 总的来说,可以将 Redis 看作是 Memcached 的扩展版,更加重量级,功能更强大。 122 | 123 | Redis 在日常工作中使用的居多。 124 | 125 | Redis 学习网址:http://www.redis.cn/ 126 | 127 | #### MongoDB缓存 128 | 129 | MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。 130 | 131 | 旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。 132 | 133 | MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。 134 | 135 | MongoDB 学习网址:http://www.mongodb.org.cn 136 | 137 | ## WEB服务器缓存 138 | 139 | #### Apache缓存 140 | 141 | 利用 `mod_expires` ,指定缓存的过期时间,可以缓存HTML、图片、JS、CSS 等。 142 | 143 | 打开 `http.conf`,开启模块: 144 | 145 | ``` 146 | LoadModule expires_module modules/mod_expires.so 147 | ``` 148 | 149 | 指定缓存的过期时间: 150 | 151 | ``` 152 | 153 | #打开缓存 154 | ExpiresActive on 155 | 156 | #css缓存(8640000秒=10天) 157 | ExpiresByType text/css A8640000 158 | 159 | #js缓存 160 | ExpiresByType application/x-javascript A8640000 161 | ExpiresByType application/javascript A8640000 162 | 163 | #html缓存 164 | ExpiresByType text/html A8640000 165 | 166 | #图片缓存 167 | ExpiresByType image/jpeg A8640000 168 | ExpiresByType image/gif A8640000 169 | ExpiresByType image/png A8640000 170 | ExpiresByType image/x-icon A8640000 171 | 172 | 173 | ``` 174 | 175 | #### Nginx缓存 176 | 177 | 利用 `expire` 参数,指定缓存的过期时间,可以缓存HTML、图片、JS、CSS 等。 178 | 179 | 打开 `nginx.conf` : 180 | 181 | ``` 182 | //以图片为例: 183 | location ~\.(gif|jpg|jepg|png|bmp|ico)$ { #加入新的location 184 | root html; 185 | expires 1d; #指定缓存时间 186 | } 187 | ``` 188 | 189 | 大家也可以了解下:proxy_cache_path 和 proxy_cache,进行缓存的设置。 190 | 191 | ## Opcode缓存 192 | 193 | Opcode(Operate Code)操作码。 194 | 195 | PHP程序运行完后,马上释放所有内存,所有程序中的变量都销毁,每次请求都要重新翻译、执行,导致速度可能会偏慢。 196 | 197 | 当解释器完成对脚本代码的分析后,便将它们生成可以直接运行的中间代码,也称为操作码。 198 | 199 | 操作码 的目地是避免重复编译,减少CPU和内存开销。 200 | 201 | #### APC缓存 202 | 203 | APC(Alternative PHP Cache)可选 PHP 缓存。 204 | 205 | APC 的目标是提供一个自由、 开放,和健全的框架,用于缓存、优化 PHP 中间代码。 206 | 207 | APC 可以去掉 php 动态解析以及编译的时间,使php脚本可以执行的更快。 208 | 209 | APC 扩展最后的发布时间为 2012-09-03。 210 | 211 | 感兴趣可以了解下,官方介绍:http://php.net/manual/zh/book.apc.php 212 | 213 | #### eAccelerator 214 | 215 | eAccelerator:A PHP opcode cache。 216 | 217 | 感兴趣可以了解下,官方介绍:http://eaccelerator.net/ 218 | 219 | #### XCache 220 | 221 | XCache 是一个又快又稳定的 PHP opcode 缓存器。 222 | 223 | 感兴趣可以了解下,官方介绍:http://xcache.lighttpd.net/ 224 | 225 | ## 小结 226 | 227 | 文章主要简单的介绍了 浏览器缓存、文件缓存、NoSQL缓存、WEB服务器缓存、Opcode缓存。 228 | 229 | 每一种缓存都可以深入研究,从介绍 -> 安装 -> 使用 -> 总结应用场景。 230 | 231 | 大家可以思考下,通过上面的介绍,工作中我们使用了哪些缓存? 232 | 233 | 还可以再使用哪些缓存,可以对我们的项目有帮助? 234 | 235 | ## 关于缓存的常见问题 236 | 237 | 用过缓存,大家肯定遇到过比较头痛的问题,比如数据一致性,雪崩,热点数据缓存,缓存监控等等。 238 | 239 | 给大家列出几个问题,纯属抛转引玉。 240 | 241 | #### 当项目中使用到缓存,我们是选择 Redis 还是 Memcached ,为什么? 242 | 243 | 举一些场景: 244 | 245 | 一、比如实现一个简单的日志收集功能或发送大量短信、邮件的功能,实现方式是先将数据收集到队列中,然后有一个定时任务去消耗队列,处理该做的事情。 246 | 247 | 直接使用 Redis 的 lpush,rpop 或 rpush,lpop。 248 | 249 | ``` 250 | //进队列 251 | $redis->lpush(key, value); 252 | 253 | //出队列 254 | $redis->rpop(key); 255 | ``` 256 | 257 | Memcached 没有这种数据结构。 258 | 259 | 二、比如我们要存储用户信息,ID、姓名、电话、年龄、身高 ,怎么存储? 260 | 261 | 方案一:key => value 262 | 263 | key = user_data_用户ID 264 | 265 | value = json_encode(用户数据) 266 | 267 | 查询时,先取出key,然后进行json_decode解析。 268 | 269 | 方案二:hash 270 | 271 | key = user_data_用户ID 272 | 273 | hashKey = 姓名,value = xx 274 | 275 | hashKey = 电话,value = xx 276 | 277 | hashKey = 年龄,value = xx 278 | 279 | hashKey = 身高,value = xx 280 | 281 | 查询时,取出key即可。 282 | 283 | ``` 284 | //新增 285 | $redis->hSet(key, hashKey, value); 286 | $redis->hSet(key, hashKey, value); 287 | $redis->hSet(key, hashKey, value); 288 | 289 | //编辑 290 | $redis->hSet(key, hashKey, value); 291 | 292 | //查询 293 | $redis->hGetAll(key); //查询所有属性 294 | $redis->hGet(key, hashKey); //查询某个属性 295 | ``` 296 | 297 | 方案二 优于 方案一。 298 | 299 | 三、比如社交项目类似于新浪微博,个人中心的关注列表和粉丝列表,双向关注列表,还有热门微博,还有消息订阅 等等。 300 | 301 | 以上都用 Redis 提供的相关数据结构即可。 302 | 303 | 四、Memcached 只存储在内存中,而 Redis 既可以存储在内存中,也可以持久化到磁盘上。 304 | 305 | 如果需求中的数据需要持久化,请选择 Redis 。 306 | 307 | 个人在工作中没有用到 Memcached ,通过查询资料得到 Memcached 内存分配时优于 Redis。 308 | 309 | Memcached 默认使用 Slab Allocation 机制管理内存,按照预先规定的大小,将分配的内存分割成特定长度的块以存储相应长度的key-value数据记录,以完全解决内存碎片问题。 310 | 311 | #### 如何保证,缓存与数据库的数据一致性? 312 | 313 | 新增数据:先新增到数据库,再新增到缓存。 314 | 315 | 编辑数据:先删除缓存数据,再修改数据库中数据,再新增到缓存。 316 | 317 | 删除数据:先删除缓存数据,再删除数据库中数据。 318 | 319 | 查询数据:先查询缓存数据,没有,再查询数据库,再新增到缓存。 320 | 321 | 强一致性是很难保证的,比如事务一致性,时间点一致性,最终一致性等。 322 | 323 | 具体问题具体分析吧。 324 | 325 | #### 缓存穿透怎么办? 326 | 327 | 用户请求缓存中不存在的数据,导致请求直接落在数据库上。 328 | 329 | 一、设置有规则的Key值,先验证Key是否符合规范。 330 | 331 | 二、接口限流、降级、熔断,请研究 istio:https://istio.io/ 332 | 333 | 三、布隆过滤器。 334 | 335 | 四、为不存在的key值,设置空缓存和过期时间,如果存储层创建了数据,及时更新缓存。 336 | 337 | #### 雪崩怎么办? 338 | 339 | 一、互斥锁,只允许一个请求去重建索引,其他请求等待缓存重建执行完,重新从缓存获取数据。 340 | 341 | ![](https://github.com/xinliangnote/PHP/blob/master/images/5_php_2.png) 342 | 343 | 二、双缓存策略,原始缓存和拷贝缓存,当原始缓存失效请求拷贝缓存,原始缓存失效时间设置为短期,拷贝缓存设置为长期。 344 | 345 | 已上,纯属抛转引玉,结合自己的情况,具体问题,具体分析吧。 346 | -------------------------------------------------------------------------------- /09-Composer 包开发居然这么简单.md: -------------------------------------------------------------------------------- 1 | ## composer 是用来干嘛的? 2 | 3 | 对于不了解composer的同学来说,肯定会有这个疑问。 4 | 5 | 这个单词经常在网上看到,特别是在 GitHub 中,在使用 `Laravel`、`Yii` 时也经常看到这个词,在安装的时候推荐使用 composer 安装?难道只是安装的时候使用吗?我也使用过其他框架,为什么 `Thinkphp`、`Codeigniter` 不用composer安装呢? 6 | 7 | 带着这些疑问,我们进行学习。 8 | 9 | composer 官方网址:https://www.phpcomposer.com 10 | 11 | composer 是 PHP(5.3+) 用来管理依赖关系的工具。 12 | 13 | 长久以来,PHP的开源方式都是项目级的,就是说一开源就是一个项目,比如一整套的CMS(Dede、WordPress、discuz)、一整套框架(Thinkphp、Codeigniter)。为啥呢?其中一个很重要的原因是你不好拆开,如果拆开的话,没有一个有效的管理工具组合起来,导致拆开的小模块大家无人问津。 14 | 15 | 然后 composer 出现了,它就是一个有效的管理工具,它负责管理大家拆开的小模块,然后进行有效的整合,使之成为一个完整的项目。 16 | 17 | 比如,记录日志使用 monolog/monolog ,HTTP client使用:guzzlehttp/guzzle 等等。 18 | 19 | composer 包的平台:https://packagist.org,这里面包含了大量的优秀的安装包,我们就很轻松一个 composer 命令就可以将优秀的代码用到我们项目中来。 20 | 21 | 作为一名骄傲PHPer,我们总不能永远只使用别人的开发包,我们必须自己动手开发一个包给别人用,给自己用。 22 | 23 | 我们既然知道了它有这么多的好处,就让我们去学习他吧,先从 composer 的安装说起。 24 | 25 | ## composer 是如何安装的? 26 | 27 | 官方入门文档:https://docs.phpcomposer.com/00-intro.html 28 | 29 | ![](https://github.com/xinliangnote/PHP/blob/master/images/9_php_1.jpeg) 30 | 31 | 通过上述的方法,可以进行安装完成。 32 | 33 | 接下来我们以 GitHub 结合 Composer 工具来进行示例讲解如何开发一个 Composer 包。 34 | 35 | ## composer 包是如何开发的? 36 | 37 | 比如,开发一个处理数字的 composer 包。 38 | 39 | #### 在 GitHub上 创建一个项目 40 | 41 | 1. 登录 GitHub(如果没有账号,请进行创建),点击右上角“+”,选择“New repository”。 42 | 2. 在创建界面中,Repository name 填写“numberFormat”,Description是选填的,暂时先不填, 43 | 接着在 Public(GitHub推荐的方式,免费,所有人都能访问)和 Private(收费,指定人才能访问,2019-01-09后对个人开发者免费了)中选择“Public”,接着在勾选“Initialize this repository with a README”,点击“Create Repository”按钮后创建成功。 44 | 45 | 至此,表示在GitHub上已经创建了一个名为“numberFormat”的空项目。 46 | 47 | 接下来,需要将远程的项目 clone 到本地(Git命令行、Git客户端)进行编码。 48 | 49 | #### 学习创建 composer.json 50 | 51 | composer.json 有哪些参数,如何编写,请参考文档:https://docs.phpcomposer.com/04-schema.html#composer.json 52 | 53 | 一个项目要调用开发包,通过composer.json就可以知道该样去加载文件。 54 | 55 | composer.json 可以使用两个方式创建,一种是`composer init`,另一种是手工创建。 56 | 57 | 咱们一起先执行下`composer init` 看看效果。 58 | 59 | 在本地创建numberFormat目录,然后 git clone 刚才创建的项目。 60 | 61 | ``` 62 | //进入到本地numberFormat目录 63 | composer init 64 | 65 | Welcome to the Composer config generator 66 | 67 | This command will guide you through creating your composer.json config. 68 | 69 | Package name (/) [root/number-format]:number-format/number-format 70 | 71 | Description []:一个处理数字的包 72 | 73 | Author [XinLiang <109760455@qq.com>, n to skip]: //回车 74 | 75 | Minimum Stability []: //回车 76 | 77 | Package Type (e.g. library, project, metapackage, composer-plugin) []: //回车 78 | 79 | License []: //回车 80 | 81 | Define your dependencies. 82 | 83 | Would you like to define your dependencies (require) interactively [yes]?no 84 | 85 | Would you like to define your dev dependencies (require-dev) interactively [yes]?no 86 | 87 | { 88 | "name": "number-format/number-format", 89 | "description": "一个处理数字的包", 90 | "authors": [ 91 | { 92 | "name": "XinLiang", 93 | "email": "109760455@qq.com" 94 | } 95 | ], 96 | "require": {} 97 | } 98 | 99 | Do you confirm generation [yes]? //回车 100 | 101 | ``` 102 | 103 | 至此,本地numberFormat目录就看到 composer.json 文件了,当然可以直接在目录下按照这个格式进行手工创建,后期直接编辑该文件即可。 104 | 105 | #### 创建项目编码内容 106 | 107 | 开发包结构如下: 108 | 109 | --src 源码目录(必须) 110 | 111 | --tests 单元测试目录(非必须) 112 | 113 | 我们按照既定的目录结构去创建目录和文件,然后再到composer.json里面修改一下即可。 114 | 115 | 接下来,在src目录中创建一个类(NumberFormat.php): 116 | 117 | ``` 118 | /** 119 | * 数字格式化类 120 | * @author XinLiang 121 | */ 122 | 123 | namespace numberFormat; 124 | 125 | class NumberFormat 126 | { 127 | /** 128 | * 格式化字节 129 | * @param int $num 数字 130 | * @param int $precision 精准度 131 | * @return string 132 | */ 133 | public static function byte_format($num = 0, $precision = 1) 134 | { 135 | if ($num >= 1000000000000) 136 | { 137 | $num = round($num / 1099511627776, $precision); 138 | $unit = 'TB'; 139 | } 140 | elseif ($num >= 1000000000) 141 | { 142 | $num = round($num / 1073741824, $precision); 143 | $unit = 'GB'; 144 | } 145 | elseif ($num >= 1000000) 146 | { 147 | $num = round($num / 1048576, $precision); 148 | $unit = 'MB'; 149 | } 150 | elseif ($num >= 1000) 151 | { 152 | $num = round($num / 1024, $precision); 153 | $unit = 'KB'; 154 | } 155 | else 156 | { 157 | return number_format($num).' Bytes'; 158 | } 159 | 160 | return number_format($num, $precision).' '.$unit; 161 | } 162 | } 163 | ``` 164 | 165 | 修改 composer.json 166 | 167 | ``` 168 | { 169 | "name": "number-format/number-format", 170 | "description": "一个处理数字的包", 171 | "authors": [ 172 | { 173 | "name": "XinLiang", 174 | "email": "109760455@qq.com" 175 | } 176 | ], 177 | "minimum-stability": "dev", 178 | "require": { 179 | "php": ">=5.3.0" 180 | }, 181 | "autoload": { 182 | "psr-4": { 183 | "numberFormat\\": "src/" 184 | } 185 | }, 186 | "license": "MIT" 187 | } 188 | ``` 189 | 190 | 至此,我们的开发包已经完成,接下来我们来测试下这个包是否可用。 191 | 192 | #### 测试开发包 193 | 194 | 在本地numberFormat目录下,通过`composer install` 安装 195 | 196 | ``` 197 | composer install 198 | 199 | Loading composer repositories with package information 200 | Updating dependencies (including require-dev) 201 | Nothing to install or update 202 | Writing lock file 203 | Generating autoload files 204 | 205 | //表示安装成功 206 | ``` 207 | 208 | 惊奇的发现,在本地numberFormat目录多一个`vendor`目录。 209 | 210 | 在tests目录创建 NumberFormatTest.php 211 | 212 | ``` 213 | /** 214 | * 数字格式化测试类 215 | * @author XinLiang 216 | */ 217 | 218 | require '../vendor/autoload.php'; 219 | 220 | use \numberFormat; 221 | 222 | $number = '102400010'; 223 | echo numberFormat\NumberFormat::byte_format($number); 224 | 225 | //输出:97.7 MB 226 | ``` 227 | 228 | 至此,测试成功,接下来就是要发布到packagist平台,给广大开发者见面了。 229 | 230 | #### 发布到 packagist 平台 231 | 232 | packagist.org 为 composer 安装包的平台(可用GitHub账号登录)。 233 | 234 | 1. 现将本地代码提交到GitHub。 235 | 2. 发布到 packagist 平台,登录后在首页的右上角有一个"Submit"按钮,点击即可进入开发包提交的界面。在“Repository URL (Git/Svn/Hg)”输入框中,输入GitHub项目的地址,点击“Check”按钮,稍微等待几秒钟,会显示验证成功,并显示出“Submit”按钮,点击即完成了开发包的提交了。 236 | 237 | 恭喜你,这个开发包可以在任何支持 composer 的PHP框架中使用了。 238 | 239 | 那么问题来了,刚才我们的包写的有的简陋,后期我们维护代码,新增代码还需要按照原来的方式操作一遍吗? 240 | 241 | 不!因为我们可以在GitHub平台设置代码更新,同时能让 packagist.org 自动更新,是不是很酷! 242 | 243 | 在GitHub中找到代码仓库,然后选择"settings" -> “Webhooks” ,默认是绑定自动更新的。 244 | 245 | 如果未绑定,可以这样设置:"settings" -> “Webhooks” -> "Add webhook" -> 246 | 247 | 1. Payload URL填写:“https://packagist.org/api/github” 248 | 2. Content type填写:“application/json” 249 | 3. Secret填写:“packagist提供的token” 250 | 4. 其他的默认即可 251 | 5. 点击“Add webhook” 完成。 252 | 253 | 至此,后期我们更新代码后会自动同步到 packagist.org 上。 254 | 255 | ``` 256 | //其他开发者可以这样获取包 257 | composer require number-format/number-format:dev-master 258 | ``` 259 | 260 | 为什么会有:dev-master,为什么引用其他的包不用这样设置? 261 | 262 | 因为我们引用的其他包都是稳定包,默认为:-stable。 263 | 264 | 是因为我们 composer.json 中设置了 minimum-stability 属性,这个可以了解下“版本约束”,在这就不多说了。 265 | 266 | 当我们在发布包后,如果获取不到报错怎么办,有可能是镜像的问题。 267 | 268 | #### composer 设置镜像地址 269 | 270 | ``` 271 | //查看全局设置 272 | composer config -gl 273 | 274 | //第一种:设置国内镜像 275 | composer config -g repo.packagist composer https://packagist.phpcomposer.com 276 | 277 | //第二种:设置国内镜像 278 | composer config -g repo.packagist composer https://packagist.laravel-china.org 279 | 280 | //第三种:设置国内镜像 281 | composer config -g repos.packagist composer https://php.cnpkg.org 282 | ``` 283 | 284 | ## 小结 285 | 286 | 通过这篇文章,解决了上述提到的三个问题: 287 | 288 | 1. composer 是用来干嘛的? 289 | 2. composer 是如何安装的? 290 | 3. composer 包是如何开发的? 291 | 292 | 看完后,是不是觉得 Composer 包开发原来这么简单,作为骄傲的程序员,去开发属于自己的 Composer 包吧! 293 | 294 | -------------------------------------------------------------------------------- /02-PHP 接口签名验证.md: -------------------------------------------------------------------------------- 1 | ## 概览 2 | 3 | 工作中,我们时刻都会和接口打交道,有的是调取他人的接口,有的是为他人提供接口,在这过程中肯定都离不开签名验证。 4 | 5 | 在设计签名验证的时候,一定要满足以下几点: 6 | 7 | - 可变性:每次的签名必须是不一样的。 8 | - 时效性:每次请求的时效性,过期作废。 9 | - 唯一性:每次的签名是唯一的。 10 | - 完整性:能够对传入数据进行验证,防止篡改。 11 | 12 | 下面主要分享一些工作中常用的加解密的方法。 13 | 14 | ## 常用验证 15 | 16 | 举例:/api/login?username=xxx&password=xxx&sign=xxx 17 | 18 | 发送方和接收方约定一个加密的盐值,进行生成签名。 19 | 20 | 示例代码: 21 | 22 | ``` 23 | //创建签名 24 | private function _createSign() 25 | { 26 | $strSalt = '1scv6zfzSR1wLaWN'; 27 | $strVal = ''; 28 | if ($this->params) { 29 | $params = $this->params; 30 | ksort($params); 31 | $strVal = http_build_query($params, '', '&', PHP_QUERY_RFC3986); 32 | } 33 | return md5(md5($strSalt).md5($strVal)); 34 | } 35 | 36 | //验证签名 37 | if ($_GET['sign'] != $this->_createSign()) { 38 | echo 'Invalid Sign.'; 39 | } 40 | ``` 41 | 42 | 上面使用到了 MD5 方法,MD5 属于单向散列加密。 43 | 44 | ## 单向散列加密 45 | 46 | #### 定义 47 | 48 | 把任意长的输入串变化成固定长的输出串,并且由输出串难以得到输入串,这种方法称为单项散列加密。 49 | 50 | #### 常用算法 51 | 52 | - MD5 53 | - SHA 54 | - MAC 55 | - CRC 56 | 57 | #### 优点 58 | 59 | **以 MD5 为例。** 60 | 61 | - 方便存储:加密后都是固定大小(32位)的字符串,能够分配固定大小的空间存储。 62 | - 损耗低:加密/加密对于性能的损耗微乎其微。 63 | - 文件加密:只需要32位字符串就能对一个巨大的文件验证其完整性。 64 | - 不可逆:大多数的情况下不可逆,具有良好的安全性。 65 | 66 | #### 缺点 67 | 68 | - 存在暴力破解的可能性,最好通过加盐值的方式提高安全性。 69 | 70 | #### 应用场景 71 | 72 | - 用于敏感数据,比如用户密码,请求参数,文件加密等。 73 | 74 | #### 推荐密码的存储方式 75 | 76 | **password_hash()** 使用足够强度的单向散列算法创建密码的哈希(hash)。 77 | 78 | 示例代码: 79 | 80 | ``` 81 | //密码加密 82 | $password = '123456'; 83 | $strPwdHash = password_hash($password, PASSWORD_DEFAULT); 84 | 85 | //密码验证 86 | if (password_verify($password, $strPwdHash)) { 87 | //Success 88 | } else { 89 | //Fail 90 | } 91 | ``` 92 | 93 | PHP 手册地址: 94 | 95 | http://php.net/manual/zh/function.password-hash.php 96 | 97 | ## 对称加密 98 | 99 | #### 定义 100 | 101 | 同一个密钥可以同时用作数据的加密和解密,这种方法称为对称加密。 102 | 103 | #### 常用算法 104 | 105 | - DES 106 | - AES 107 | 108 | AES 是 DES 的升级版,密钥长度更长,选择更多,也更灵活,安全性更高,速度更快。 109 | 110 | #### 优点 111 | 112 | 算法公开、计算量小、加密速度快、加密效率高。 113 | 114 | #### 缺点 115 | 116 | 发送方和接收方必须商定好密钥,然后使双方都能保存好密钥,密钥管理成为双方的负担。 117 | 118 | #### 应用场景 119 | 120 | 相对大一点的数据量或关键数据的加密。 121 | 122 | #### AES 123 | 124 | AES 加密类库在网上很容易找得到,请注意类库中的 `mcrypt_encrypt` 和 `mcrypt_decrypt` 方法! 125 | 126 | ![](https://github.com/xinliangnote/PHP/blob/master/images/2_php_1.png) 127 | 128 | ![](https://github.com/xinliangnote/PHP/blob/master/images/2_php_2.png) 129 | 130 | 在 PHP7.2 版本中已经被弃用了,在新版本中使用 `openssl_encrypt` 和 `openssl_decrypt` 两个方法。 131 | 132 | 示例代码(类库): 133 | 134 | ``` 135 | class Aes 136 | { 137 | /** 138 | * var string $method 加解密方法 139 | */ 140 | protected $method; 141 | 142 | /** 143 | * var string $secret_key 加解密的密钥 144 | */ 145 | protected $secret_key; 146 | 147 | /** 148 | * var string $iv 加解密的向量 149 | */ 150 | protected $iv; 151 | 152 | /** 153 | * var int $options 154 | */ 155 | protected $options; 156 | 157 | /** 158 | * 构造函数 159 | * @param string $key 密钥 160 | * @param string $method 加密方式 161 | * @param string $iv 向量 162 | * @param int $options 163 | */ 164 | public function __construct($key = '', $method = 'AES-128-CBC', $iv = '', $options = OPENSSL_RAW_DATA) 165 | { 166 | $this->secret_key = isset($key) ? $key : 'CWq3g0hgl7Ao2OKI'; 167 | $this->method = in_array($method, openssl_get_cipher_methods()) ? $method : 'AES-128-CBC'; 168 | $this->iv = $iv; 169 | $this->options = in_array($options, [OPENSSL_RAW_DATA, OPENSSL_ZERO_PADDING]) ? $options : OPENSSL_RAW_DATA; 170 | } 171 | 172 | /** 173 | * 加密 174 | * @param string $data 加密的数据 175 | * @return string 176 | */ 177 | public function encrypt($data = '') 178 | { 179 | return base64_encode(openssl_encrypt($data, $this->method, $this->secret_key, $this->options, $this->iv)); 180 | } 181 | 182 | /** 183 | * 解密 184 | * @param string $data 解密的数据 185 | * @return string 186 | */ 187 | public function decrypt($data = '') 188 | { 189 | return openssl_decrypt(base64_decode($data), $this->method, $this->secret_key, $this->options, $this->iv); 190 | } 191 | } 192 | ``` 193 | 194 | 示例代码: 195 | 196 | ``` 197 | $aes = new Aes('HFu8Z5SjAT7CudQc'); 198 | $encrypted = $aes->encrypt('锄禾日当午'); 199 | echo '加密前:锄禾日当午
加密后:', $encrypted, '
'; 200 | 201 | $decrypted = $aes->decrypt($encrypted); 202 | echo '加密后:', $encrypted, '
解密后:', $decrypted; 203 | ``` 204 | 205 | 运行结果: 206 | 207 | ![](https://github.com/xinliangnote/PHP/blob/master/images/2_php_3.png) 208 | 209 | ## 非对称加密 210 | 211 | #### 定义 212 | 213 | 需要两个密钥来进行加密和解密,这两个秘钥分别是公钥(public key)和私钥(private key),这种方法称为非对称加密。 214 | 215 | #### 常用算法 216 | 217 | - RSA 218 | 219 | #### 优点 220 | 221 | 与对称加密相比,安全性更好,加解密需要不同的密钥,公钥和私钥都可进行相互的加解密。 222 | 223 | #### 缺点 224 | 225 | 加密和解密花费时间长、速度慢,只适合对少量数据进行加密。 226 | 227 | #### 应用场景 228 | 229 | 适合于对安全性要求很高的场景,适合加密少量数据,比如支付数据、登录数据等。 230 | 231 | #### RSA 与 RSA2 232 | 233 | 算法名称 | 标准名称 | 备注 234 | --- | --- | --- 235 | RSA2 | SHA256WithRSA | 强制要求RSA密钥的长度至少为2048 236 | RSA | SHA1WithRSA | 对RSA密钥的长度不限制,推荐使用2048位以上 237 | 238 | RSA2 比 RSA 有更强的安全能力。 239 | 240 | 蚂蚁金服,新浪微博 都在使用 RSA2 算法。 241 | 242 | 创建公钥和私钥: 243 | 244 | ``` 245 | openssl genrsa -out private_key.pem 2048 246 | openssl rsa -in private_key.pem -pubout -out public_key.pem 247 | ``` 248 | 249 | 执行上面命令,会生成 `private_key.pem` 和 `public_key.pem` 两个文件。 250 | 251 | 示例代码(类库): 252 | 253 | ``` 254 | class Rsa2 255 | { 256 | private static $PRIVATE_KEY = 'private_key.pem 内容'; 257 | private static $PUBLIC_KEY = 'public_key.pem 内容'; 258 | 259 | /** 260 | * 获取私钥 261 | * @return bool|resource 262 | */ 263 | private static function getPrivateKey() 264 | { 265 | $privateKey = self::$PRIVATE_KEY; 266 | return openssl_pkey_get_private($privateKey); 267 | } 268 | 269 | /** 270 | * 获取公钥 271 | * @return bool|resource 272 | */ 273 | private static function getPublicKey() 274 | { 275 | $publicKey = self::$PUBLIC_KEY; 276 | return openssl_pkey_get_public($publicKey); 277 | } 278 | 279 | /** 280 | * 私钥加密 281 | * @param string $data 282 | * @return null|string 283 | */ 284 | public static function privateEncrypt($data = '') 285 | { 286 | if (!is_string($data)) { 287 | return null; 288 | } 289 | return openssl_private_encrypt($data,$encrypted,self::getPrivateKey()) ? base64_encode($encrypted) : null; 290 | } 291 | 292 | /** 293 | * 公钥加密 294 | * @param string $data 295 | * @return null|string 296 | */ 297 | public static function publicEncrypt($data = '') 298 | { 299 | if (!is_string($data)) { 300 | return null; 301 | } 302 | return openssl_public_encrypt($data,$encrypted,self::getPublicKey()) ? base64_encode($encrypted) : null; 303 | } 304 | 305 | /** 306 | * 私钥解密 307 | * @param string $encrypted 308 | * @return null 309 | */ 310 | public static function privateDecrypt($encrypted = '') 311 | { 312 | if (!is_string($encrypted)) { 313 | return null; 314 | } 315 | return (openssl_private_decrypt(base64_decode($encrypted), $decrypted, self::getPrivateKey())) ? $decrypted : null; 316 | } 317 | 318 | /** 319 | * 公钥解密 320 | * @param string $encrypted 321 | * @return null 322 | */ 323 | public static function publicDecrypt($encrypted = '') 324 | { 325 | if (!is_string($encrypted)) { 326 | return null; 327 | } 328 | return (openssl_public_decrypt(base64_decode($encrypted), $decrypted, self::getPublicKey())) ? $decrypted : null; 329 | } 330 | 331 | /** 332 | * 创建签名 333 | * @param string $data 数据 334 | * @return null|string 335 | */ 336 | public function createSign($data = '') 337 | { 338 | if (!is_string($data)) { 339 | return null; 340 | } 341 | return openssl_sign($data, $sign, self::getPrivateKey(), OPENSSL_ALGO_SHA256) ? base64_encode($sign) : null; 342 | } 343 | 344 | /** 345 | * 验证签名 346 | * @param string $data 数据 347 | * @param string $sign 签名 348 | * @return bool 349 | */ 350 | public function verifySign($data = '', $sign = '') 351 | { 352 | if (!is_string($sign) || !is_string($sign)) { 353 | return false; 354 | } 355 | return (bool)openssl_verify($data, base64_decode($sign), self::getPublicKey(), OPENSSL_ALGO_SHA256); 356 | } 357 | } 358 | ``` 359 | 360 | 示例代码: 361 | 362 | ``` 363 | $rsa2 = new Rsa2(); 364 | 365 | $privateEncrypt = $rsa2->privateEncrypt('锄禾日当午'); 366 | echo '私钥加密后:'.$privateEncrypt.'
'; 367 | 368 | $publicDecrypt = $rsa2->publicDecrypt($privateEncrypt); 369 | echo '公钥解密后:'.$publicDecrypt.'
'; 370 | 371 | $publicEncrypt = $rsa2->publicEncrypt('锄禾日当午'); 372 | echo '公钥加密后:'.$publicEncrypt.'
'; 373 | 374 | $privateDecrypt = $rsa2->privateDecrypt($publicEncrypt); 375 | echo '私钥解密后:'.$privateDecrypt.'
'; 376 | 377 | $sign = $rsa2->createSign('锄禾日当午'); 378 | echo '生成签名:'.$privateEncrypt.'
'; 379 | 380 | $status = $rsa2->verifySign('锄禾日当午', $sign); 381 | echo '验证签名:'.($status ? '成功' : '失败') ; 382 | ``` 383 | 384 | 运行结果: 385 | 386 | 部分数据截图如下: 387 | 388 | ![](https://github.com/xinliangnote/PHP/blob/master/images/2_php_4.png) 389 | 390 | #### JS-RSA 391 | 392 | **JSEncrypt** :用于执行OpenSSL RSA加密、解密和密钥生成的Javascript库。 393 | 394 | Git源:https://github.com/travist/jsencrypt 395 | 396 | **应用场景:** 397 | 398 | 我们在做 WEB 的登录功能时一般是通过 Form 提交或 Ajax 方式提交到服务器进行验证的。 399 | 400 | 为了防止抓包,登录密码肯定要先进行一次加密(RSA),再提交到服务器进行验证。 401 | 402 | 一些大公司都在使用,比如淘宝、京东、新浪 等。 403 | 404 | 示例代码就不提供了,Git上提供的代码是非常完善的。 405 | 406 | ## 密钥安全管理 407 | 408 | 这些加密技术,能够达到安全加密效果的前提是 **密钥的保密性**。 409 | 410 | 实际工作中,不同环境的密钥都应该不同(开发环境、预发布环境、正式环境)。 411 | 412 | 那么,应该如何安全保存密钥呢? 413 | 414 | #### 环境变量 415 | 416 | 将密钥设置到环境变量中,每次从环境变量中加载。 417 | 418 | #### 配置中心 419 | 420 | 将密钥存放到配置中心,统一进行管理。 421 | 422 | #### 密钥过期策略 423 | 424 | 设置密钥有效期,比如一个月进行重置一次。 425 | 426 | **在这里希望大佬提供新的思路 ~** 427 | 428 | ## 接口调试工具 429 | 430 | #### Postman 431 | 432 | 一款功能强大的网页调试与发送网页 HTTP 请求的 Chrome插件。 433 | 434 | 这个不用多介绍,大家肯定都使用过。 435 | 436 | #### SocketLog 437 | 438 | Git源:https://github.com/luofei614/SocketLog 439 | 440 | **解决的痛点:** 441 | 442 | - 正在运行的API有Bug,不能在文件中使用var_dump进行调试,因为会影响到client的调用。将日志写到文件中,查看也不是很方便。 443 | 444 | - 我们在二次开发一个新系统的时候,想查看执行了哪些Sql语句及程序的warning,notice等错误信息。 445 | 446 | SocketLog,可以解决以上问题,它通过WebSocket将调试日志输出到浏览器的console中。 447 | 448 | **使用方法** 449 | 450 | 1. 安装、配置Chrome插件 451 | 2. SocketLog服务端安装 452 | 3. PHP中用SocketLog调试 453 | 4. 配置日志类型和相关参数 454 | 455 | ## 在线接口文档 456 | 457 | 接口开发完毕,需要给请求方提供接口文档,文档的编写现在大部分都使用Markdown格式。 458 | 459 | 也有一些开源的系统,可以下载并安装到自己的服务器上。 460 | 461 | 也有一些在线的系统,可以在线使用同时也支持离线导出。 462 | 463 | 根据自己的情况,选择适合自己的文档平台吧。 464 | 465 | 常用的接口文档平台: 466 | 467 | - eolinker 468 | - Apizza 469 | - Yapi 470 | - RAP2 471 | - DOClever 472 | 473 | ## 扩展 474 | 475 | 一、在 HTTP 和 RPC 的选择上,可能会有一些疑问,RPC框架配置比较复杂,明明用HTTP能实现为什么要选择RPC? 476 | 477 | 下面简单的介绍下 HTTP 与 RPC 的区别。 478 | 479 | **传输协议:** 480 | 481 | - HTTP 基于 HTTP 协议。 482 | - RPC 即可以 HTTP 协议,也可以 TCP 协议。 483 | 484 | HTTP 也是 RPC 实现的一种方式。 485 | 486 | **性能消耗:** 487 | 488 | - HTTP 大部分基于 JSON 实现的,序列化需要时间和性能。 489 | - RPC 可以基于二进制进行传输,消耗性能少一点。 490 | 491 | 推荐一个像 JSON ,但比 JSON 传输更快占用更少的新型序列化类库 **MessagePack**。 492 | 493 | 官网地址:https://msgpack.org/ 494 | 495 | 还有一些服务治理、负载均衡配置的区别。 496 | 497 | **使用场景:** 498 | 499 | 比如浏览器接口、APP接口、第三方接口,推荐使用 HTTP。 500 | 501 | 比如集团内部的服务调用,推荐使用 RPC。 502 | 503 | RPC 比 HTTP 性能消耗低,传输效率高,服务治理也方便。 504 | 505 | 推荐使用的 RPC 框架:**Thrift**。 506 | 507 | 二、动态令牌 508 | 509 | 简单介绍下几种动态令牌,感兴趣的可以深入了解下。 510 | 511 | OTP:One-Time Password 一次性密码。 512 | 513 | HOTP:HMAC-based One-Time Password 基于HMAC算法加密的一次性密码。 514 | 515 | TOTP:Time-based One-Time Password 基于时间戳算法的一次性密码。 516 | 517 | 使用场景: 518 | 519 | - 公司VPN登录双因素验证 520 | - 服务器登录动态密码验证 521 | - 网银、网络游戏的实体动态口令牌 522 | - 银行转账动态密码 523 | - ... 524 | 525 | ## 小结 526 | 527 | 本文讲了设计签名验证需要满足的一些条件:可变性、时效性、唯一性、完整性。 528 | 529 | 还讲了一些加密方法:单向散列加密、对称加密、非对称加密,同时分析了各种加密方法的优缺点,**大家可以根据自己的业务特点进行自由选择**。 530 | 531 | 提供了 Aes、Rsa 相关代码示例。 532 | 533 | 分享了可以编写接口文档的在线系统。 534 | 535 | 分享了开发过程中使用的接口调试工具。 536 | 537 | 扩展中分析了 HTTP 和 RPC 的区别,动态令牌的介绍等。 538 | 539 | 还提出了一个问题,**关于如何安全的进行密钥管理?** , 欢迎各位 前辈/大佬,提供新的思路 ~ 540 | --------------------------------------------------------------------------------