redis实战经验

如何避免key冲突?

  • select db
    • 默认db16个,编号0~15,可通过配置修改db个数
    • 默认使用db-0
  • key命名格式
    • 以"xxx:yyy:zzz"格式命名

安全考虑

  • 访问限制
    • 只对内网访问,以防外部通过6379端口访问
  • 危险函数
    • 禁用危险函数:flush、flushall
  • 使用秘钥
    • $redis->auth(password)

性能提升

  • 连接
    • 使用官方扩展,弃用predis
    • 使用单例方式连接,使用pconnect
    • 用中间件,实现连接池
  • 持久化策略
    • 根据实际情况,关闭持久化
  • 服务部署
    • 与web服务共存,本地调用
    • 与存储服务隔离(mysql),避免高io
    • 一主多从,提升性能,或使用集群
  • 版本升级
    • 低版本:setnx + expire
    • 高版本:set(k, v, array(ex, nx))
  • 批量操作
    • 使用管道命令,批量导入数据
    • 使用mset, mget
  • 合理操作
    • 使用hset存放json,而不是set
    • 使用scan代替keys
    • Nginx + Lua + Redis

问题:
如何检查redis服务是否健康?

缓存设计中的要点

高性能

  • 简洁的通讯协议,快速连接
  • 操作基于内存,快速读写
  • 成熟的数据结构,快速定位
  • 基于牛X的语言实现,快速运行
  • 主从架构,读写分离

高可用

  • 异常监控,自动化应急
  • 数据灾备,快速恢复
  • 主从架构,从服务上位
  • 分布式、集群化
  • 服务自动降级
  • 多级缓存

存储

  • 成熟的数据结构,减少冗余
  • 数据压缩方案,减小数据大小
  • 置换算法:FIFO、LRU、LFU
  • 索引,以空间换时间
  • 数据分片,快捷扩容、无限扩容
  • 一定要设置过期时间!

常见缓存过期策略

策略 优点 缺点
定时过期
每个设置了过期时间的key,都携带一个定时器做倒计时,到期自动删除
立即清除,无空间浪费 定时器占用大量cpu,影响性能
惰性过期
只有当访问的时候,才会判断是否过期,过期则删除
最大化节省cpu 过期数据未被访问时,会占用存储,造成空间浪费
定期过期
每隔一定时间,随机扫描一定数量带有有效期的key,并清除其中已过期的key
前两种的这种方案 难点
需要合理设置 “时间”、 “数量”

常见存储置换策略:FIFO、LRU、LFU

  • FIFO(first in first out)
    • 淘汰最早的数据
  • LRU(least recently used)
    • 淘汰最长时间未被使用的数据
  • LFU(least frequently used)
    • 淘汰使用次数最少的数据

Redis置换策略配置(maxmemory-policy)

  • noeviction:不置换
  • volatile-[ lru | random | ttl ]
    • 对具备有效期的key,按lru/随机/最短置换
  • allkeys-[ lru | random ]
    • 对所有key,按lru/随机置换

一致性

缓存与数据库保持一致
- 低效做法:使用事务,数据库与缓存的更新
- 符合ACID
- 业务普遍做法:先更新数据库,再删除缓存
- 严谨但复杂的做法:消息队列、订阅binlog

旁路缓存原则:

读操作:
- 先读缓存
- 如果命中,直接返回
- 如果未命中,访问DB,并写入缓存

写操作:
- 删缓存,而不是更新缓存
- 先写DB,再删缓存

问题:
以下方式,存在什么隐患?
- 先更新缓存,再更新数据库
- 先删除缓存,再更新数据库
- 先更新数据库,再更新缓存

防雪崩

高并发时失效
雪崩:在用户高并发瞬间,如果缓存不可用(失效),用户的请求压力,都转到数据库,导致数据库挂掉,并最终导致整个系统挂掉。

如何防止缓存雪崩的发生?

  • 缓存服务高可用
    • 服务挂了,做什么都没用
  • 多个缓存不能同时过期
    • 随机失效,而不是同时或定点
  • 多级缓存
    • 多级缓存,尽可能把数据库挡在后面
  • 使用互斥锁
    • 只有拿到锁的请求,才能查库
  • 排队限流
    • 控制查库的并发峰值
  • 提前预热
    • 预先更新缓存
  • 公用性质缓存更应重视
    • 如:首页、推荐位
    • 个性化数据相对风险小
// 业务中改成这样是否有问题
function getDataBuyKey($strKey) {
    // 从缓存中取数据
    $arrData = getCacheData($strKey);
    // 如果未命中缓存,或缓存即将失效,抢占锁
    if (!$arrData || $arrData['ttl'] - time() < 10) {
        // 如果抢锁成功(带有效期的锁),可以查库
        if (redis::set('lock_' . $strKey, 1, array('nx', 'ex' => 10))) {
            $arrData = getDbData($strKey); // 查库
            setCacheData($arrData); // 更新缓存
        }
    }
    return $arrData;
}

防穿透

被故意不命中
用户伪造大量请求,故意不命中缓存,当这些请求集中转到数据库时,导致数据库挂掉,并最终导致整个系统挂掉。

如何防止缓存穿透的发生?

  • 缓存空值
    • 额外缓存一份空值,以防反复查库
  • 缓存数据范围
    • 如,应用ID范围、页码范围、时间段
  • 预先使用布隆过滤(bloom filter)
    • 一定不存在的数据,请求将被拦截
function getDataBuyKey($strKey) {
    $arrData = [];
    // 先执行key的有效验证
    if (checkKey($strKey)) {
        // 从缓存中取数据,未取到,返回null
        $arrData = getCacheData($strKey);
    }
    // 如果未命中缓存,或者缓存即将失效,抢占锁
    if (is_null($arrData) || $arrData['ttl'] - time() < 10) {
        // 如果抢锁成功(带有效期的锁),可以查库
        if (redis::set('lock_' . $strKey, 1, array('nx', 'ex' => 10))) {
            $arrData = (array)getDbData($strKey); // 查库
            setCacheData($arrData); // 更新缓存,若为空值,写入"[]"
        }
        is_null($arrData) && $arrData = []
    }
    return $arrData; // 返回空数据,或即将失效的数据,或最新的数据
}

cookie中的domain设置

项目中遇到这样一个问题:用户登录后,跳转到该域名下的其他页面时,cookie的值改变了。由于是在app中,只能抓包,看到请求header头中有两个同名cookie。而在服务端,超全局变量cookie数组,只取了第一个cookie。由于项目变动,重新设置cookie时只设置了当前域名下的cookie,所以旧的cookie永远无法清除。

这种问题,其实如果基础好一点,对cookie足够了解,是很快能够解决的。由于很久不写这些基础,所以还是花了很长时间。因为当时无法确定cookie的domain和path,所以只能靠猜去清除cookie。

虽然通过git log找到原先设置cookie的域名成功清除了旧cookie,但是如果早点看资料,应该并不难解决。(看文档胜过一切)

对于cookie只需要牢记两点:设置范围作用域
- 设置范围:只能设置当前域名以及父域名(无法设置子域或者同级域名)
- 作用域:当前域名以及所有子域名

举个简单的例子:
对于域名 test.com,在访问a.test.com时,只能设置domain为a.test.com或者test.com;如果设置domain为a.test.com,则child.a.test.comtool.child.a.test.com等都是能够访问到该cookie的。而b.test.com是无法访问到的。

如果设置domain为test.com,则a.test.com以及test.com下的所有子域名都能访问下,这意味着它的同级域名b.test.com也能够访问到。有什么用呢?

这样可以用来解决sso单点登录或者跨域cookie等问题。当然,现在的单点登录一般是通过返回的ticket凭证到服务端做校验去获取信息。

比如,对于a.test.comb.test.com两个项目,公用user.test.com用户系统做单点登录,可以通过设置domain为test.com,在user退出的时候清除根域名下的cookie以达到协同退出的功能。

回到上面的问题:所以在当前域名无法清除cookie的时候,只可能是它的上级域名设置了cookie。而对于path,一般默认情况下是 / 根目录。

参考资料:cookie设置域名问题,cookie跨域

php的几种运行模式CLI、CGI、FastCGI、mod_php

常用运行模式

CLI

CLI:命令行,可以在控制台或者shell中输入命令获取输出,没有header头信息

CGI

公共网关接口(Common Gateway Interface/CGI):是一种重要的互联网技术,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI描述了服务器和请求处理程序之间传输数据的一种标准。即web server将用户请求以消息的方式转交给PHP独立进程,PHP与web服务之间无从属关系。

FastCGI

快速通用网关接口(Fast Common Gateway Interface/FastCGI):是一种让交互程序与Web服务器通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本。

CGI 和 FastCGI 是一种通信协议规范,不是一个实体

CGI 程序和FastCGI程序,是指实现这两个协议的程序,可以是任何语言实现这个协议的。(PHP-CGI 和 PHP-FPM就是实现FastCGI的程序)

FastCGI和CGI的区别

  • CGI每接收一个请求就要fork一个进程处理,只能接收一个请求作出一个响应。请求结束后该进程就会结束。
  • FastCGI会事先启动起来,作为一个cgi的管理服务器存在,预先启动一系列的子进程来等待处理,然后等待web服务器发过来的请求,一旦接受到请求就交由子进程处理,这样由于不需要在接受到请求后启动cgi,会快很多。
  • FastCGI使用进程/线程池来处理一连串的请求。这些进程/线程由FastCGI服务器管理,而不是Web服务器。 当进来一个请求时,Web服务器把环境变量和这个页面请求通过一个Socket长连接传递给FastCGI进程。FastCGI像是一个常驻型的CGI,它可以一直执行,在请求到达时不会花费时间去fork一个进程来处理(这是CGI对位人诟病的fork-and-execute模式)。正是因为它只是一个通信协议,它还支持分布式的运算,即FastCGI程序可以在网站服务器以外的主机上执行并且接受来自其他网站服务器的请求。

FastCGI整个流程

  1. Web server启动时载入FastCGI进程管理器
  2. FastCGI自身初始化,启动多个CGI解释器进程(可见多个php-cgi)并等待来自Web server的请求
  3. 当请求Web server时,Web server通过socket请求FastCGI进程管理器,FastCGI进程管理器选择并连接到一个CGI解释器,Web server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi
  4. FastCGI子进程处理请求完成后将标准输出和错误从同一连接返回给Web server,当FastCGI子进程结束后请求便结束。FastCGI子进程接着等待处理来自FastCGI进程管理器的下一个连接,在CGI模式中,php-cgi在此便退出了。

PHP-FPM:PHP的FastCGI进程管理器

PHP-CGI 和 PHP-FPM的区别

php-cgi与php-fpm一样,也是一个fastcgi进程管理器

php-cgi的问题在于:
- php-cgi变更php.ini配置后需重启php-cgi才能让新的php-ini生效,不可以平滑重启
- 直接杀死php-cgi进程,php就不能运行了。

PHP-FPM和Spawn-FCGI就没有这个问题,守护进程会平滑从新生成新的子进程。针对php-cgi的不足,php-fpm应运而生。

PHP-FPM 的管理对象是php-cgi。使用PHP-FPM来控制PHP-CGI的FastCGI进程

mod_php(传统模式)

apache的php模块,将PHP做为web-server的子进程控制,两者之间有从属关系。

最明显的例子就是在CGI模式下,如果修改了PHP.INI的配置文件,不用重启web服务便可生效,而模块模式下则需要重启web服务。

以mod_php模式运行PHP,意味着php是作为apache的一个模块来启动的,因此只有在apache启动的时候会读取php.ini配置文件并加载扩展模块,在apache运行期间是不会再去读取和加载扩展模块的。如果修改php的配置,需要重启apache服务

Apache的工作模式 prefork的工作原理

一个单独的控制进程(父进程)负责产生子进程,这些子进程用于监听请求并作出应答。

Apache总是试图保持一些备用的 (spare)或是空闲的子进程用于迎接即将到来的请求。这样客户端就无需在得到服务前等候子进程的产生。

在Unix系统中,父进程通常以root身份运行以便邦定80端口,而 Apache产生的子进程通常以一个低特权的用户运行。User和Group指令用于配置子进程的低特权用户。运行子进程的用户必须要对他所服务的内容有读取的权限,但是对服务内容之外的其他资源必须拥有尽可能少的权限。

Apache的工作模式 worker的工作原理

每个进程能够拥有的线程数量是固定的。服务器会根据负载情况增加或减少进程数量。

一个单独的控制进程(父进程)负责子进程的建立。每个子进程能够建立ThreadsPerChild数量的服务线程和一个监听线程,该监听线程监听接入请求并将其传递给服务线程处理和应答。

nginx默认是使用的fastcgi模式,可以配合fpm使用

js和php闭包的使用和区别

匿名函数

如果只是省去函数名,单纯的当作一个函数式方法返回,只能称为匿名函数(闭包需要将匿名函数当作结果返回),比如:

// js
var foo = function(x, y) {
    return x + y ;
};
console.log(foo(1, 2));  // 3
<?php
// php
$foo = function($a, $b) {
    return $a + $b;
}; // 一定要加分号
echo $foo(1, 2); // 3

闭包

闭包通常是用来创建内部变量,使得这些变量不得被外部随意修改,而只能通过指定的函数接口去修改

js闭包

这里举一个阮老师博客里的例子,阮老师博客:学习Javascript闭包(Closure)

js基础

参考链接:深入理解JS中声明提升、作用域(链)和this关键字

  • js比较特殊的一点是:函数内部可以直接读取到全局变量(对于阮老师的这句话不是很能理解,大概是想表达的意思:父作用域的变量可以在子作用域直接访问,而不需要去声明访问真正的全局变量?)
    • 大部分语言,变量都是先声明在使用,而对于js,具有声明提升的特性(不管在哪里声明,都会在代码执行前处理)
    • 函数和变量的声明总是会隐式地被移动到当前作用域的顶部,函数的声明优先级高于变量的声明
    • var 会在当前作用域声明一个变量,而未声明的变量,会隐式地创建一个全局变量
// 声明提升
console.log(a);  // 1, 未报错
var a = 1;
// 上文链接中的例子
function testOrder(arg) {
    console.log(arg); // arg是形参,不会被重新定义
    console.log(a); // 因为函数声明比变量声明优先级高,所以这里a是函数
    var arg = 'hello'; // var arg;变量声明被忽略, arg = 'hello'被执行
    var a = 10; // var a;被忽视; a = 10被执行,a变成number
    function a() {
        console.log('fun');
    } // 被提升到作用域顶部
    console.log(a); // 输出10
    console.log(arg); // 输出hello
}; 
testOrder('hi');
/* 输出:
hi 
function a() {
        console.log('fun');
    }
10 
hello 
*/
// 全局作用域
var foo = 42;
function test() {
    // 局部作用域
    foo = 21;
}
test();
foo; // 21
// 全局作用域
foo = 42;
function test() {
    // 局部作用域
    var foo = 21;
}
test();
foo; // 42
  • js变量的查找是从里往外的,直到最顶层(全局作用域),并且一旦找到,即停止向上查找。所有内部函数可以访问函数外部的变量,反之无效
function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log(a, b, c);
    }
    bar(b * 3);
}
foo(2);
function foo() {
    var a = 1;
}
console.log(a);  //a is not defined
function foo1() {
    var num = 0;
    addNum = function() {  // 这里未通过var去声明,默认是全局变量
        num += 1;
    };
    function foo2() {
        console.log(num);
    }
    return foo2;
}
var tmp = foo1();
tmp();  // 0

addNum();
tmp(); // 1

这里第二次调用foo2函数,foo1函数的局部变量num并没有被初始化为0,说明打印的是内存中的num。正常函数在每次调用结束后都会销毁局部变量,在重新调用的时候会再次声明变量;而这边没有重新声明的原因是:把foo2函数赋值给了一个全局变量tmp,导致foo2函数一直存在内存中,而foo2函数依赖于foo1函数存在,所以foo1函数也存在内存中,并没有被销毁,所以foo1的局部变量也是存在内存中。

  • this的上下文基于函数调用的情况。和函数在哪定义无关,而和函数怎么调用有关。
    • 在全局上下文(任何函数以外),this指向全局对象(windows)
    • 在函数内部时,this由函数怎么调用来确定
      • 当函数作为对象方法调用时,this指向该对象

下面是阮老师博客里的两个思考题:

var name = "The Window";
var object = {
  name : "My Object",
  getNameFunc : function(){
    return function(){
      return this.name;
    };
  }
};
alert(object.getNameFunc()()); // The Window
var name = "The Window";
var object = {
    name : "My Object",
  getNameFunc : function(){
      var that = this;
    return function(){
        return that.name;
    };
  }
};
alert(object.getNameFunc()());// My Object

this的作用域好像一直是个比较奇怪的东西,对于上面两个例子,我的理解是:第一个例子,是在方法里调用的this,而这个this并没有声明,会隐式地创建一个全局变量,所以调用的全局的name;第二个,调用的that的时候,会向顶级链式查找是否声明that,而这个that有this赋值,这里的this又是通过对象方法调用,则该this指向这个object对象,所有最终调用的是object作用域内的name。不知道这么理解是不是有问题,还望大神指正。

那其实js闭包的主要目的:访问函数内部的局部变量,即延长作用域链
参考链接:js闭包MDN文档

php闭包

php回调函数

mixed call_user_func ( callable $callback [, mixed $parameter [, mixed $... ]] )
mixed call_user_func_array ( callable $callback , array $param_arr )

这两个函数都是把第一个参数作为回调函数d调用,后面接收参数,区别就是第二个函数第二参数接收数组;在使用上唯一的区别就是,call_user_func函数无法接收引用传递; 个人觉得同样是使用,call_user_func 相比call_user_func_array完全可以被替代,不知道是不是有一些性能上的优势。具体使用样例,请参考官方文档。

<?php
// 引用传递
function foo(&$a, &$b) {
    $a ++;
    $b --;
};
$a = $b = 10;
call_user_func_array('foo', [&$a, &$b]);
echo $a."\n", $b; // 11, 9
基本用法

基本用法,跟js的闭包类似
- 普通调用

<?php
global $tmp = 'hello world';
function foo() {
    var_dump(global $tmp);
}
foo(); // null, 函数内部无法直接调用上级作用域的变量,除非声明为全局变量
<?php
$foo1 = function() {
    $a = 10;
    $foo2 = function() {
        var_dump($a);
    };
    return $foo2;
};
$tmp = $foo1();
$tmp();  // null,原因同上 
  • php想要能够获取上级作用域的变量,需要通过use传递
<?php
$foo1 = function () {
    $a = 10;
    $foo2 = function () use ($a) {
        var_dump($a);
        $a ++;
    };
    $foo2();
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 10, 10,  use并不能实际改变变量的值,只是值传递
<?php
$foo1 = function () {
    $a = 10;
    $foo2 = function () use (&$a) {
        var_dump($a);
        $a ++;
    };
    $foo2();
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 10, 11,  通过值传递改变变量的值
  • 下面两段代码的区别,不是很明白,望大佬指点,为什么后一个值传递就可以获取到已经改变后变量的值。好像都是在调用方法之前,已经执行过变量的递增了吧?
<?php
// 值传递
$foo1 = function () {
    $a = 10;
    $foo2 = function () use ($a) {
        var_dump($a);
    };
    $a ++;
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 10
<?php
// 引用传递
$foo1 = function () {
    $a = 10;
    $foo2 = function () use (&$a) {
        var_dump($a);
    };
    $a ++;
    return $foo2;
};
$tmp = $foo1();
$tmp();  // 11
  • 正确使用
<?php
// 值传递
$foo = function () {
    $a = 10;
    $foo2 = function ($num) use ($a) {
        var_dump($num + $a);
    };
    return $foo2;
};
$tmp = $foo();
$tmp(100); // 110
<?php
// 引用传递
$foo = function () {
    $a = 10;
    $foo2 = function ($num) use (&$a) {
        var_dump($num + $a);
        $a ++;
    };
    return $foo2;
};
$tmp = $foo();
$tmp(100); // 110
$tmp(100); // 111
$tmp(100); // 112  跟js类似,保证变量常驻内存
php Closure 类

共同点

都是为了扩展作用域,获取内部变量

区别

js能够在方法内部直接获取到父级作用域的变量,而php需要通过use声明,并且默认是值传递

应用场景

  • 不是很能理解应用场景,搜索了一下,很多只是写了一个闭包实现的购物车,感觉并不是那么的实用。
  • 如果只是单纯的使用匿名函数,感觉还不如封装成一个私有方法
    >这些只是个人粗鄙的理解,望指正.

phpstorm或vscode使用psr2规范

  • 安装composer
  • 全局安装phpcs
composer global require squizlabs/php_codesniffer

vscode直接插件搜索phpcs安装

phpstorm

全局安装phpcs后,会在C:\Users{user name}\AppData\Roaming\Composer\vendor\bin下生成一个phpcs.bat,后面会用到
- phpstorm -> setting
- languages & Frameworks->PHP->Code Sniffer点击Configuration右侧的按钮
- 找到刚才的phpcs.bat,点击Validate,确认
- Editor->Inspection->PHP
- 双击PHP Code Sniffer validation,点击Coding standard右侧的刷新按钮,然后选择psr2,确定

参考链接: 如何优雅地使用phpstorm?