Gabriel

Gabriel 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Gabriel 赞了文章 · 3月2日

redis & redis-cluster

redis是一款高速缓存非关系型数据库,为什么说他是”高速“的,应为其读写都在内存中,这也是非关系行数据库的一大特点,Redis是以key-value的形式存储的。下面将从以下几点来介绍redis。(redis的安装及启动请自行解决...)

  1. redis数据结构
  2. redis数据持久化
  3. redis cluster集群的实现方案

8.最后可以安装redis可视化工具--brew install rdm


一:redis支持的数据结构

  1. 字符串(Strings)
    设置:set key value获取:get key
  2. 列表(List)
    设置:lpush key value1 / lpush key value2 获取:lrange 0 1
  3. 集合(Sets)
    设置:sadd key value1 / sadd key value2 获取:sismember key
  4. 集合(Sorted Sets)
    设置:zadd key value1 / zadd key value2 获取:sismember key
  5. 哈希(Hashes)
    hmset key:001 cou1 value1 cou2 value2 获取:hgetall key:001

具体大家可以按照上面的格式去尝试新建各类数据类型。

二:redis数据持久化
Redis虽然是一种内存型数据库,一旦服务器进程退出,数据库的数据就会丢失,为了解决这个问题Redis提供了两种持久化的方案,将内存中的数据保存到磁盘中,避免数据的丢失。

  1. RDB持久化
    RDB是默认持久化方式,其规定以下几种时间段内去触发持久化操作

    # 以下配置表示的条件:
    # 服务器在900秒之内被修改了1次
    save 900 1
    # 服务器在300秒之内被修改了10次
    save 300 10
    # 服务器在60秒之内被修改了10000次
    save 60 10000

    该方式有个致命的缺点——RDB持久化相当于备份数据库状态,那么如果数据文件很大的化就会导致服务崩溃等一系列问题。

  2. AOF持久化
    AOF持久化(Append-Only-File),与RDB持久化不同,AOF持久化是通过保存Redis服务器锁执行的写状态来记录数据库的。具体来说,RDB持久化相当于备份数据库状态,而AOF持久化是备份数据库接收到的命令,所有被写入AOF的命令都是以redis的协议格式来保存的。以下为开启AOF持久化的配置方式:

    #AOF 和 RDB 持久化方式可以同时启动并且无冲突。  
    #如果AOF开启,启动redis时会加载aof文件,这些文件能够提供更好的保证。 
    appendonly yes
    
    # 只增文件的文件名称。(默认是appendonly.aof)  
    # appendfilename appendonly.aof 
    #redis支持三种不同的写入方式:  
    #  
    # no:不调用,之等待操作系统来清空缓冲区当操作系统要输出数据时。很快。  
    # always: 每次更新数据都写入仅增日志文件。慢,但是最安全。
    # everysec: 每秒调用一次。折中。
    appendfsync everysec  
    
    # 设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入.官方文档建议如果你有特殊的情况可以配置为'yes'。但是配置为'no'是最为安全的选择。
    no-appendfsync-on-rewrite no  
    
    # 自动重写只增文件。  
    # redis可以自动盲从的调用‘BGREWRITEAOF’来重写日志文件,如果日志文件增长了指定的百分比。  
    # 当前AOF文件大小是上次日志重写得到AOF文件大小的二倍时,自动启动新的日志重写过程。
    auto-aof-rewrite-percentage 100  
    # 当前AOF文件启动新的日志重写过程的最小值,避免刚刚启动Reids时由于文件尺寸较小导致频繁的重写。
    auto-aof-rewrite-min-size 64mb

    其实我么可以查看dump.rdb文件和appendonly.aof文件就会很明显发现他们各自的存储数据的方式。还有一点就是所有的配置都是在redis.conf文件中。服务每次重启的时候都会首先从持久化文件中去拉取数据。两个持久化文件路径大家可查看redis.conf中的dir配置项获得,我本地的如下:

    clipboard.png

自行尝试:先利用RDB的方式添加一些key-value,在开启aof方式,观察dump.rdb文件和appendonly.aof文件大小的变化。

三:redis cluster集群的实现方案
redis在年初发布了3.0.0,官方支持了redis cluster,也就是集群。redis cluster在设计的时候,就考虑到了去中心化,去中间件,也就是说,集群中的每个节点都是平等的关系,都是对等的,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。

Redis 集群没有并使用传统的一致性哈希来分配数据,而是采用另外一种叫做哈希槽 (hash slot)的方式来分配的。redis cluster 默认分配了 16384 个slot,当我们set一个key 时,会用CRC16算法来取模得到所属的slot,然后将这个key 分到哈希槽区间的节点上,具体算法就是:CRC16(key) % 16384。必须要3个以后的主节点,否则在创建集群时会失败。

1.开启集群配置

[root@web3 redis-3.0.5]# vi redis.conf
#修改以下地方
port 7000
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
appendonly yes

2.我们这边模拟6台服务(3M+3S)

[root@web3 redis-3.0.5]# mkdir -p /usr/local/cluster-test
[root@web3 redis-3.0.5]# cd /usr/local/cluster-test/
[root@web3 cluster-test]# mkdir 7000
[root@web3 cluster-test]# mkdir 7001
[root@web3 cluster-test]# mkdir 7002
[root@web3 cluster-test]# mkdir 7003
[root@web3 cluster-test]# mkdir 7004
[root@web3 cluster-test]# mkdir 7005

3.把redis.conf文件拷贝6份并修改相应的端口号最后CP到7000-7005文件夹下去,再将对行的服务文件redis-serverCP到7000-7005文件夹下去

4.启动这这6个服务

[root@web3 cluster-test]# cd /usr/local/cluster-test/7000/
[root@web3 7000]# redis-server redis.conf
[root@web3 7000]# cd ../7001
[root@web3 7001]# redis-server redis.conf
[root@web3 7001]# cd ../7002
[root@web3 7002]# redis-server redis.conf
[root@web3 7002]# cd ../7003
[root@web3 7003]# redis-server redis.conf
[root@web3 7003]# cd ../7004
[root@web3 7004]# redis-server redis.conf
[root@web3 7004]# cd ../7005
[root@web3 7005]# redis-server redis.conf
[root@web3 7005]#

5.查看6个服务的启动进程情况

[root@web3 7005]# ps -ef|grep redis
root     11380     1  0 07:37 ?        00:00:00 redis-server *:7000 [cluster]
root     11384     1  0 07:37 ?        00:00:00 redis-server *:7001 [cluster]
root     11388     1  0 07:37 ?        00:00:00 redis-server *:7002 [cluster]
root     11392     1  0 07:37 ?        00:00:00 redis-server *:7003 [cluster]
root     11396     1  0 07:37 ?        00:00:00 redis-server *:7004 [cluster]
root     11400     1  0 07:37 ?        00:00:00 redis-server *:7005 [cluster]
root     11404  8259  0 07:38 pts/0    00:00:00 grep redis

6.将6个服务连在一起构招成集群

redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

7.如果输出以下信息恭喜你集群搭建成功了

[root@web3 7000]# redis-trib.rb check 127.0.0.1:7000
Connecting to node 127.0.0.1:7000: OK
Connecting to node 127.0.0.1:7002: OK
Connecting to node 127.0.0.1:7003: OK
Connecting to node 127.0.0.1:7005: OK
Connecting to node 127.0.0.1:7001: OK
Connecting to node 127.0.0.1:7004: OK
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: 3707debcbe7be66d4a1968eaf3a5ffaf4308efa4 127.0.0.1:7000
   slots:0-5460 (5461 slots) master
   1 additional replica(s)
M: dfa0754c7854a874a6ebd2613b86140ad97701fc 127.0.0.1:7002
   slots:10923-16383 (5461 slots) master
   1 additional replica(s)
S: d2237fdcfbba672de766b913d1186cebcb6e1761 127.0.0.1:7003
   slots: (0 slots) slave
   replicates 3707debcbe7be66d4a1968eaf3a5ffaf4308efa4
S: 30858dbf483b61b9838d5c1f853a60beaa4e7afd 127.0.0.1:7005
   slots: (0 slots) slave
   replicates dfa0754c7854a874a6ebd2613b86140ad97701fc
M: cb5c04b6160c3b7e18cad5d49d8e2987b27e0d6c 127.0.0.1:7001
   slots:5461-10922 (5462 slots) master
   1 additional replica(s)
S: 4b4aef8b48c427a3c903518339d53b6447c58b93 127.0.0.1:7004
   slots: (0 slots) slave
   replicates cb5c04b6160c3b7e18cad5d49d8e2987b27e0d6c
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

8.最后可以安装redis可视化工具--brew install rdm

图片描述

查看原文

赞 4 收藏 30 评论 0

Gabriel 赞了文章 · 3月2日

优雅搭建redis-cluster

前言

记得年方二八时候,数数还是数得过来的,如今年逾二十八,数都数不清了,前几天在狗东哪里整了《算法导论》、《概率导论》两本教科书看看学学,也好在研究机器学习的时候有解惑的根本。redis-cluster正好成功的引起了我的兴趣,就当着等书之前的休闲折腾

redis早前没有现成的集群模块,如今流行起来了,功能也越来越强大了,下面我就来部署一下,当然这中间也会遇到很多问题的。不过没关系,有问题咱们解决即可

手动部署

根据官网介绍,手动部署集群需要进行一下操作

cd /usr/local/redis
mkdir -p cluster/7000
cp ./etc/redis.conf  cluster/7000
vi cluster/7000/redis.conf

这里我们针对关键的地方修改即可,具体情况如下

port 7000
cluster-enabled yes
cluster-config-file nodes-7000.conf
cluster-node-timeout 5000
appendonly yes

修改完成后,复制

cp -r cluster/7000 cluster/7001
cp -r cluster/7000 cluster/7002
cp -r cluster/7000 cluster/7003
cp -r cluster/7000 cluster/7004
cp -r cluster/7000 cluster/7005

然后依次修改每个节点配置文件的端口号,修改好配置,最后启动所有的节点

redis-server cluster/7000/redis.conf
redis-server cluster/7001/redis.conf
redis-server cluster/7002/redis.conf
redis-server cluster/7003/redis.conf
redis-server cluster/7004/redis.conf
redis-server cluster/7005/redis.conf

这个时候虽然所有节点启动了,但是还不能称之为集群。下面我们要使用ruby的辅助工具redis-trib来将所有的节点联系起来,这个工具就是我们的redis安装源文件的src目录下的redis-trib.rb文件,但是这个文件运行需要ruby版redis的驱动


yum install ruby
gem install redis

安装失败,redis依赖的ruby版本必须要大于2.2.0,下面我们来手动安装ruby2.4.2
先清理yum安装的ruby

yum remove ruby

下面通过RVM(Ruby Version Manager)来安装ruby

gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E37D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://get.rvm.io | bash -s stable
rvm install 2.4.2
gem install redis

安装成功后

./src/redis-trib.rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005

自动部署

以上是手动创建集群的方法,下面还有自动启动节点,创建集群,停止集群的服务,文件在redis安装目录下,为utils/create-cluster/create-cluster


cp utils/create-cluster/create-cluster /etc/init.d/create-cluster
vi /etc/init.d/create-cluster

修改PROT并添加ROOT=/usr/local/redis

#!/bin/bash
# Settings
PORT=6999
TIMEOUT=2000
NODES=6
REPLICAS=1
ROOT=/usr/local/redis
# You may want to put the above config parameters into config.sh in order to
# override the defaults without modifying this script.

if [ -a config.sh ]
then
    source "config.sh"
fi

# Computed vars
ENDPORT=$((PORT+NODES))

if [ "$1" == "start" ]
then
    while [ $((PORT < ENDPORT)) != "0" ]; do
        PORT=$((PORT+1))
        echo "Starting $PORT"
        $ROOT/bin/redis-server --port $PORT --cluster-enabled yes --cluster-config-file nodes-${PORT}.conf --cluster-node-timeout $TIMEOUT --appendonly yes --appendfilename appendonly-${PORT}.aof --dbfilename dump-${PORT}.rdb --logfile ${PORT}.log --daemonize yes
    done
    exit 0
fi

if [ "$1" == "create" ]
then
    HOSTS=""
    while [ $((PORT < ENDPORT)) != "0" ]; do
        PORT=$((PORT+1))
        HOSTS="$HOSTS 127.0.0.1:$PORT"
    done
    $ROOT/src/redis-trib.rb create --replicas $REPLICAS $HOSTS
    exit 0
fi

if [ "$1" == "stop" ]
then
    while [ $((PORT < ENDPORT)) != "0" ]; do
        PORT=$((PORT+1))
        echo "Stopping $PORT"
        $ROOT/bin/redis-cli -p $PORT shutdown nosave
    done
    exit 0
fi

if [ "$1" == "watch" ]
then
    PORT=$((PORT+1))
    while [ 1 ]; do
        clear
        date
        $ROOT/bin/redis-cli -p $PORT cluster nodes | head -30
        sleep 1
    done
    exit 0
fi

if [ "$1" == "tail" ]
then
    INSTANCE=$2
    PORT=$((PORT+INSTANCE))
    tail -f ${PORT}.log
    exit 0
fi

if [ "$1" == "call" ]
then
    while [ $((PORT < ENDPORT)) != "0" ]; do
        PORT=$((PORT+1))
        $ROOT/bin/redis-cli -p $PORT $2 $3 $4 $5 $6 $7 $8 $9
    done
    exit 0
fi

if [ "$1" == "clean" ]
then
    rm -rf *.log
    rm -rf appendonly*.aof
    rm -rf dump*.rdb
    rm -rf nodes*.conf
    exit 0
fi

if [ "$1" == "clean-logs" ]
then
    rm -rf *.log
    exit 0
fi

echo "Usage: $0 [start|create|stop|watch|tail|clean]"
echo "start       -- Launch Redis Cluster instances."
echo "create      -- Create a cluster using redis-trib create."
echo "stop        -- Stop Redis Cluster instances."
echo "watch       -- Show CLUSTER NODES output (first 30 lines) of first node."
echo "tail <id>   -- Run tail -f of instance at base port + ID."
echo "clean       -- Remove all instances data, logs, configs."
echo "clean-logs  -- Remove just instances logs."

启动节点,创建集群就可以用一下命令来实现了

service create-cluster start
service create-cluster create

停止集群

service create-cluster stop

还有watchtailcleanclean-logs命令,这里就不一一说明了,这个教程就写到这吧

查看原文

赞 4 收藏 2 评论 0

Gabriel 赞了文章 · 3月2日

缓存雪崩、缓存穿透、缓存更新了解多少?

前言

只有光头才能变强。
文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y

回顾前面:

Redis

今天来分享一下Redis几道常见的面试题:

  • 如何解决缓存雪崩?
  • 如何解决缓存穿透?
  • 如何保证缓存与数据库双写时一致的问题?

一、缓存雪崩

1.1什么是缓存雪崩?

回顾一下我们为什么要用缓存(Redis):

为什么要缓存

现在有个问题,如果我们的缓存挂掉了,这意味着我们的全部请求都跑去数据库了

如果缓存挂掉了,全部请求跑去数据库了

在前面学习我们都知道Redis不可能把所有的数据都缓存起来(内存昂贵且有限),所以Redis需要对数据设置过期时间,并采用的是惰性删除+定期删除两种策略对过期键删除。Redis对过期键的策略+持久化

如果缓存数据设置的过期时间是相同的,并且Redis恰好将这部分数据全部删光了。这就会导致在这段时间内,这些缓存同时失效,全部请求到数据库中。

这就是缓存雪崩

  • Redis挂掉了,请求全部走数据库。
  • 对缓存数据设置相同的过期时间,导致某段时间内缓存失效,请求全部走数据库。

缓存雪崩如果发生了,很可能就把我们的数据库搞垮,导致整个服务瘫痪!

1.2如何解决缓存雪崩?

对于“对缓存数据设置相同的过期时间,导致某段时间内缓存失效,请求全部走数据库。”这种情况,非常好解决:

  • 解决方法:在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期

对于“Redis挂掉了,请求全部走数据库”这种情况,我们可以有以下的思路:

  • 事发前:实现Redis的高可用(主从架构+Sentinel 或者Redis Cluster),尽量避免Redis挂掉这种情况发生。
  • 事发中:万一Redis真的挂了,我们可以设置本地缓存(ehcache)+限流(hystrix),尽量避免我们的数据库被干掉(起码能保证我们的服务还是能正常工作的)
  • 事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据

二、缓存穿透

2.1什么是缓存穿透

比如,我们有一张数据库表,ID都是从1开始的(正数):

随便找了一张数据库表

但是可能有黑客想把我的数据库搞垮,每次请求的ID都是负数。这会导致我的缓存就没用了,请求全部都找数据库去了,但数据库也没有这个值啊,所以每次都返回空出去。

缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。

缓存穿透

这就是缓存穿透

  • 请求的数据在缓存大量不命中,导致请求走数据库。

缓存穿透如果发生了,也可能把我们的数据库搞垮,导致整个服务瘫痪!

2.1如何解决缓存穿透?

解决缓存穿透也有两种方案:

  • 由于请求的参数是不合法的(每次都请求不存在的参数),于是我们可以使用布隆过滤器(BloomFilter)或者压缩filter提前拦截,不合法就不让这个请求到数据库层!
  • 当我们从数据库找不到的时候,我们也将这个空对象设置到缓存里边去。下次再请求的时候,就可以从缓存里边获取了。

    • 这种情况我们一般会将空对象设置一个较短的过期时间

参考资料:

三、缓存与数据库双写一致

3.1对于读操作,流程是这样的

上面讲缓存穿透的时候也提到了:如果从数据库查不到数据则不写入缓存。

一般我们对读操作的时候有这么一个固定的套路

  • 如果我们的数据在缓存里边有,那么就直接取缓存的。
  • 如果缓存里没有我们想要的数据,我们会先去查询数据库,然后将数据库查出来的数据写到缓存中
  • 最后将数据返回给请求

3.2什么是缓存与数据库双写一致问题?

如果仅仅查询的话,缓存的数据和数据库的数据是没问题的。但是,当我们要更新时候呢?各种情况很可能就造成数据库和缓存的数据不一致了。

  • 这里不一致指的是:数据库的数据跟缓存的数据不一致

数据库和缓存的数据不一致

从理论上说,只要我们设置了键的过期时间,我们就能保证缓存和数据库的数据最终是一致的。因为只要缓存数据过期了,就会被删除。随后读的时候,因为缓存里没有,就可以查数据库的数据,然后将数据库查出来的数据写入到缓存中。

除了设置过期时间,我们还需要做更多的措施来尽量避免数据库与缓存处于不一致的情况发生。

3.3对于更新操作

一般来说,执行更新操作时,我们会有两种选择:

  • 先操作数据库,再操作缓存
  • 先操作缓存,再操作数据库

首先,要明确的是,无论我们选择哪个,我们都希望这两个操作要么同时成功,要么同时失败。所以,这会演变成一个分布式事务的问题。

所以,如果原子性被破坏了,可能会有以下的情况:

  • 操作数据库成功了,操作缓存失败了
  • 操作缓存成功了,操作数据库失败了
如果第一步已经失败了,我们直接返回Exception出去就好了,第二步根本不会执行。

下面我们具体来分析一下吧。

3.3.1操作缓存

操作缓存也有两种方案:

  • 更新缓存
  • 删除缓存

一般我们都是采取删除缓存缓存策略的,原因如下:

  1. 高并发环境下,无论是先操作数据库还是后操作数据库而言,如果加上更新缓存,那就更加容易导致数据库与缓存数据不一致问题。(删除缓存直接和简单很多)
  2. 如果每次更新了数据库,都要更新缓存【这里指的是频繁更新的场景,这会耗费一定的性能】,倒不如直接删除掉。等再次读取时,缓存里没有,那我到数据库找,在数据库找到再写到缓存里边(体现懒加载)

基于这两点,对于缓存在更新时而言,都是建议执行删除操作!

3.3.2先更新数据库,再删除缓存

正常的情况是这样的:

  • 先操作数据库,成功;
  • 再删除缓存,也成功;

如果原子性被破坏了:

  • 第一步成功(操作数据库),第二步失败(删除缓存),会导致数据库里是新数据,而缓存里是旧数据
  • 如果第一步(操作数据库)就失败了,我们可以直接返回错误(Exception),不会出现数据不一致。

如果在高并发的场景下,出现数据库与缓存数据不一致的概率特别低,也不是没有:

  • 缓存刚好失效
  • 线程A查询数据库,得一个旧值
  • 线程B将新值写入数据库
  • 线程B删除缓存
  • 线程A将查到的旧值写入缓存

要达成上述情况,还是说一句概率特别低

因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

对于这种策略,其实是一种设计模式:Cache Aside Pattern

先修改数据库,再删除缓存

删除缓存失败的解决思路

  • 将需要删除的key发送到消息队列中
  • 自己消费消息,获得需要删除的key
  • 不断重试删除操作,直到成功

3.3.3先删除缓存,再更新数据库

正常情况是这样的:

  • 先删除缓存,成功;
  • 再更新数据库,也成功;

如果原子性被破坏了:

  • 第一步成功(删除缓存),第二步失败(更新数据库),数据库和缓存的数据还是一致的。
  • 如果第一步(删除缓存)就失败了,我们可以直接返回错误(Exception),数据库和缓存的数据还是一致的。

看起来是很美好,但是我们在并发场景下分析一下,就知道还是有问题的了:

  • 线程A删除了缓存
  • 线程B查询,发现缓存已不存在
  • 线程B去数据库查询得到旧值
  • 线程B将旧值写入缓存
  • 线程A将新值写入数据库

所以也会导致数据库和缓存不一致的问题。

并发下解决数据库与缓存不一致的思路

  • 将删除缓存、修改数据库、读取缓存等的操作积压到队列里边,实现串行化

将操作积压到队列中

3.4对比两种策略

我们可以发现,两种策略各自有优缺点:

  • 先删除缓存,再更新数据库

    • 在高并发下表现不如意,在原子性被破坏时表现优异
  • 先更新数据库,再删除缓存(Cache Aside Pattern设计模式)

    • 在高并发下表现优异,在原子性被破坏时表现不如意

3.5其他保障数据一致的方案与资料

可以用databus或者阿里的canal监听binlog进行更新。

参考资料:

最后

这是几道Redis常见的面试题,希望大家看完有所帮助,顺利拿到offer!

乐于输出干货的Java技术公众号:Java3y。200多篇原创技术文章、海量视频资源、精美脑图!

帅的人都关注了

精彩回顾:

觉得我的文章写得不错,不妨点一下

查看原文

赞 120 收藏 89 评论 3

Gabriel 赞了文章 · 3月1日

Redis 缓存雪崩、击穿、穿透

你知道的越多,你不知道的越多

点赞再看,养成习惯

正文

提到Redis我相信各位在面试,或者实际开发过程中对缓存雪崩穿透击穿也不陌生吧,就算没遇到过但是你肯定听过,那三者到底有什么区别,我们又应该怎么去防止这样的情况发生呢,我们有请下一位受害者。

面试开始

一个大腹便便,穿着格子衬衣的中年男子,拿着一个满是划痕的mac向你走来,看着快秃顶的头发,心想着肯定是尼玛顶级架构师吧!但是我们腹有诗书气自华,虚都不虚。

小伙子我看你的简历上写到了Redis,那么我们直接开门见山,直接怼常见的几个大问题,Redis雪崩了解么?

帅气迷人的面试官您好,我了解的,目前电商首页以及热点数据都会去做缓存 ,一般缓存都是定时任务去刷新,或者是查不到之后去更新的,定时任务刷新就有一个问题。

举个简单的例子:如果所有首页的Key失效时间都是12小时,中午12点刷新的,我零点有个秒杀活动大量用户涌入,假设当时每秒 6000 个请求,本来缓存在可以扛住每秒 5000 个请求,但是缓存当时所有的Key都失效了。此时 1 秒 6000 个请求全部落数据库,数据库必然扛不住,它会报一下警,真实情况可能DBA都没反应过来就直接挂了。此时,如果没用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。这就是我理解的缓存雪崩。

我刻意看了下我做过的项目感觉再吊的都不允许这么大的QPS直接打DB去,不过没慢SQL加上分库,大表分表可能还还算能顶,但是跟用了Redis的差距还是很大

同一时间大面积失效,那一瞬间Redis跟没有一样,那这个数量级别的请求直接打到数据库几乎是灾难性的,你想想如果打挂的是一个用户服务的库,那其他依赖他的库所有的接口几乎都会报错,如果没做熔断等策略基本上就是瞬间挂一片的节奏,你怎么重启用户都会把你打挂,等你能重启的时候,用户早就睡觉去了,并且对你的产品失去了信心,什么垃圾产品。

面试官摸了摸自己的头发,嗯还不错,那这种情况咋整?你都是怎么去应对的?

处理缓存雪崩简单,在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会在同一时间大面积失效,我相信,Redis这点流量还是顶得住的。

setRedis(Key,value,time + Math.random() * 10000);

如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题,不过本渣我在生产环境中操作集群的时候,单个服务都是对应的单个Redis分片,是为了方便数据的管理,但是也同样有了可能会失效这样的弊端,失效时间随机是个好策略。

或者设置热点数据永远不过期,有更新操作就更新缓存就好了(比如运维更新了首页商品,那你刷下缓存就完事了,不要设置过期时间),电商首页的数据也可以用这个操作,保险。

那你了解缓存穿透和击穿么,可以说说他们跟雪崩的区别么?

嗯,了解,我先说一下缓存穿透吧,缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,我们数据库的 id 都是1开始自增上去的,如发起为id值为 -1 的数据或 id 为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大,严重会击垮数据库。

小点的单机系统,基本上用postman就能搞死,比如我自己买的阿里云服务

像这种你如果不对参数做校验,数据库id都是大于0的,我一直用小于0的参数去请求你,每次都能绕开Redis直接打到数据库,数据库也查不到,每次都这样,并发高点就容易崩掉了。

至于缓存击穿嘛,这个跟缓存雪崩有点像,但是又有一点不一样,缓存雪崩是因为大面积的缓存失效,打崩了DB,而缓存击穿不同的是缓存击穿是指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。

面试官露出欣慰的眼光,那他们分别怎么解决

缓存穿透我会在接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。

这里我想提的一点就是,我们在开发程序的时候都要有一颗“不信任”的心,就是不要相信任何调用方,比如你提供了API接口出去,你有这几个参数,那我觉得作为被调用方,任何可能的参数情况都应该被考虑到,做校验,因为你不相信调用你的人,你不知道他会传什么参数给你。

举个简单的例子,你这个接口是分页查询的,但是你没对分页参数的大小做限制,调用的人万一一口气查 Integer.MAX_VALUE 一次请求就要你几秒,多几个并发你不就挂了么?是公司同事调用还好大不了发现了改掉,但是如果是黑客或者竞争对手呢?在你双十一当天就调你这个接口会发生什么,就不用我说了吧。这是之前的Leader跟我说的,我觉得大家也都应该了解下。

从缓存取不到的数据,在数据库中也没有取到,这时也可以将对应Key的Value对写为null、位置错误、稍后重试这样的值具体取啥问产品,或者看具体的场景,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。

这样可以防止攻击用户反复用同一个id暴力攻击,但是我们要知道正常用户是不会在单秒内发起这么多次请求的,那网关层Nginx本渣我也记得有配置项,可以让运维大大对单个IP每秒访问次数超出阈值的IP都拉黑。

那你还有别的办法么?

还有我记得Redis还有一个高级用法布隆过滤器(Bloom Filter)这个也能很好的防止缓存穿透的发生,他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return。

那又有小伙伴说了如果黑客有很多个IP同时发起攻击呢?这点我一直也不是很想得通,但是一般级别的黑客没这么多肉鸡,再者正常级别的Redis集群都能抗住这种级别的访问的,小公司我想他们不会感兴趣的。把系统的高可用做好了,集群还是很能顶的。

缓存击穿的话,设置热点数据永远不过期。或者加上互斥锁就能搞定了

作为暖男,代码我肯定帮你们准备好了

面试结束

嗯嗯还不错,三个点都回答得很好,今天也不早了,面试就先到这里,明天你再过来二面我继续问一下你关于Redis集群高可用,主从同步,哨兵等知识点的问题。

晕居然还有下一轮面试!(强行下一期的伏笔哈哈)但是为了offer还是得舔,嗯嗯,好的帅气面试官。

能回答得这么全面这么细节还是忍不住点赞

暗示点赞,每次都看了不点赞,你们想白嫖我么?你们好坏喲,不过我喜欢

总结

我们玩归玩,闹归闹,别拿面试开玩笑。

本文简单的介绍了,Redis雪崩击穿穿透,三者其实都差不多,但是又有一些区别,在面试中其实这是问到缓存必问的,大家不要把三者搞混了,因为缓存雪崩、穿透和击穿,是缓存最大的问题,要么不出现,一旦出现就是致命性的问题,所以面试官一定会问你。

大家一定要理解是怎么发生的,以及是怎么去避免的,发生之后又怎么去抢救,你可以不是知道很深入,但是你不能一点都不去想,面试有时候不一定是对知识面的拷问,或许是对你的态度的拷问,如果你思路清晰,然后知其然还知其所以然那就很赞,还知道怎么预防那来上班吧。

最后暖男我继续给你们做个小的技术总结:

一般避免以上情况发生我们从三个时间段去分析下:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + Hystrix 限流+降级,避免 MySQL 被打死。
  • 事后:Redis 持久化 RDB+AOF,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

上面的几点我会在吊打系列Redis篇全部讲一下这个月应该可以吧Redis更完,限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空白的值。

好处:

数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。 只要数据库不死,就是说,对用户来说,3/5 的请求都是可以被处理的。 只要有 3/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。

这个在目前主流的互联网大厂里面是最常见的,你是不是好奇,某明星爆出什么事情,你发现你去微博怎么刷都空白界面,但是有的人又直接进了,你多刷几次也出来了,现在知道了吧,那是做了降级,牺牲部分用户的体验换来服务器的安全,可还行?

敖丙 | 文 【原创】

查看原文

赞 231 收藏 127 评论 9

Gabriel 赞了文章 · 2月25日

php如何openssl_encrypt加密解密

最近在对接客户的CRM系统,获取令牌时,要用DES方式加密解密,由于之前没有搞错这种加密方式,经过请教了“百度”和“谷歌”两个老师后,结合了多篇文档内容后,终于实现了。

一、DES介绍

DES 是对称性加密里面常见一种,全称为 Data Encryption Standard,即数据加密标准,是一种使用密钥加密的块算法。密钥长度是64位(bit),超过位数密钥被忽略。所谓对称性加密即加密和解密密钥相同,对称性加密一般会按照固定长度,把待加密字符串分成块,不足一整块或者刚好最后有特殊填充字符。

  • 跨语言做 DES 加密解密经常会出现问题,往往是填充方式不对、编码不一致或者加密解密模式没有对应上造成。
  • 常见的填充模式有: pkcs5、pkcs7、iso10126、ansix923、zero。
  • 加密模式有:DES-ECB、DES-CBC、DES-CTR、DES-OFB、DES-CFB。

加密用到的方法:

 openssl_encrypt($data, $method, $password, $options, $iv)

参数说明:

  1. $data 加密明文
  2. $method 加密方法

    • DES-ECB
    • DES-CBC
    • DES-CTR
    • DES-OFB
    • DES-CFB
  3. $passwd 加密密钥[密码]
  4. $options 数据格式选项(可选)【选项有:】

    • 0
    • OPENSSL_RAW_DATA=1
    • OPENSSL_ZERO_PADDING=2
    • OPENSSL_NO_PADDING=3
  5. $iv 密初始化向量(可选)
  • 需要注意:如果$method为DES-ECB,则$iv无需填写

二、解密用到的方法:

openssl_decrypt($data, $method, $password, $options, $iv)

参数说明:

  1. $data 要解密的数据
  2. 其他参数同加密方法

三、用法案例:

参数:

   $data = '1234567887654321';//加密明文
   $method = 'DES-ECB';//加密方法
   $passwd = '12344321';//加密密钥
   $options = 0;//数据格式选项(可选)
   $iv = '';//加密初始化向量(可选)

(1) 默认填充方式:

    • 加密:

      $result = openssl_encrypt($data, $method, $passwd, $options);
      var_dump($result);

      结果:

      string(32) "kQYOdswcm9I5elv2wdJucplqAgqDNqXg"
    • 解密

      $result = 'kQYOdswcm9I5elv2wdJucplqAgqDNqXg';
      var_dump(openssl_decrypt($result, $method, $passwd, 0));

      结果:

      string(16) "1234567887654321"

    (2) OPENSSL_RAW_DATA方式【会用PKCS#7进行补位】

    • 加密

      $result = openssl_encrypt($data, $method, $passwd, OPENSSL_RAW_DATA);
      var_dump($result);

      结果:

      string(24) "�v���9z[���nr�j �6��"

      我们可以看到结果是乱码的,这时我们需要base64一下

      $result = openssl_encrypt($data, $method, $passwd, OPENSSL_RAW_DATA);
      var_dump(base64_encode($result));

      这时结果是

      string(32) "kQYOdswcm9I5elv2wdJucplqAgqDNqXg"
    • 解密

      result = openssl_encrypt($data, $method, $passwd, OPENSSL_RAW_DATA);
      
      var_dump(openssl_decrypt($result, $method, $passwd,OPENSSL_RAW_DATA));

      结果:

      string(16) "1234567887654321"

      我们可以看到:默认填充方式与OPENSSL_RAW_DATA,这两种方式加密结果是一样的

    (3) OPENSSL_ZERO_PADDING方式

    看字面意思,是用0填充,但是测试并不起作用

    • 加密

      $result = openssl_encrypt($data, $method, $passwd, OPENSSL_ZERO_PADDING);
      var_dump($result);

      结果:

      string(24) "kQYOdswcm9I5elv2wdJucg==" 
    • 解密:

      $result = openssl_encrypt($data, $method, $passwd, OPENSSL_ZERO_PADDING);
      var_dump(openssl_decrypt($result, $method, $passwd,OPENSSL_ZERO_PADDING));

      结果:

      string(16) "1234567887654321"

    (4) OPENSSL_NO_PADDING【不填充,需要手动填充】

    • 在openssl_encrypt前加上填充过程
    • 加密

        $str_padded = $data;
        if (strlen($str_padded) % 16) {
            $str_padded = str_pad($str_padded,strlen($str_padded) + 16 - strlen($str_padded) % 16, "\0");
        }
        $result = openssl_encrypt($str_padded, $method, $passwd, OPENSSL_NO_PADDING);
        var_dump($result);
        echo '<br>';
        var_dump( base64_encode($result));

      结果:

      string(16) "�v���9z[���nr" 
      string(24) "kQYOdswcm9I5elv2wdJucg=="

      我们可以看到结果是加密的乱码,需要用base64一下,就可以看到结果了

    • 解密:

       //加密begin
        $str_padded = $data;
        if (strlen($str_padded) % 16) {
            $str_padded = str_pad($str_padded,strlen($str_padded) + 16 - strlen($str_padded) % 16, "\0");
        }
        $result = openssl_encrypt($str_padded, $method, $passwd, OPENSSL_NO_PADDING);
        //加密end
       //解密begin
       $str = base64_encode($result);
       $m = openssl_decrypt( base64_decode($str) , $method, $passwd, OPENSSL_NO_PADDING);
       var_dump( rtrim( rtrim( $m,chr(0) ), chr(7) ) );
       //解密 end

      结果:

      string(16) "1234567887654321"

    ** 结尾要去除填充字符’0’和’a’。
    ‘a’是为了兼容用OPENSSL_RAW_DATA加密的结果。 **

    参照的文档有:

    相关知识文章

    查看原文

    赞 10 收藏 8 评论 0

    Gabriel 赞了文章 · 2月23日

    PHP完整实战23种设计模式

    前言

    设计模式是面向对象的最佳实践

    实战

    PHP实战创建型模式

    PHP实战结构型模式

    PHP实战行为型模式

    测试用例

    23种设计模式都提供测试用例,使用方法:

    • 克隆项目: git clone git@github.com:TIGERB/easy-tips.git
    • 运行脚本: php patterns/[文件夹名称]/test.php,

    例如测试责任链模式: 运行 php patterns/chainOfResponsibility/test.php

    运行结果:
    
    请求5850c8354b298: 令牌校验通过~ 
    请求5850c8354b298: 请求频率校验通过~ 
    请求5850c8354b298: 参数校验通过~ 
    请求5850c8354b298: 签名校验通过~ 
    请求5850c8354b298: 权限校验通过~

    源码

    源码地址 https://github.com/TIGERB/eas...

    这是我的一个关于《一个php技术栈后端猿的知识储备大纲》的知识总结,目前只完成了“设计模式”。

    纠错

    如果大家发现有什么理解有误的地方,可以发起一个issue点击纠错,我会及时纠正,THX~

    Easy PHP:一个极速轻量级的PHP全栈框架

    扫面下方二维码关注我的技术公众号,及时为大家推送我的原创技术分享

    图片描述

    查看原文

    赞 165 收藏 567 评论 23

    Gabriel 赞了文章 · 2月20日

    Redis面试复习

    1.认识Redis

    • 工作模型:单线程架构和IO多路复用来实现高性能的内存数据库服务
    • 原因:a)单线程简化数据结构和算法的实现;b)避免线程切换和线程竞争的开销
    • 应用场景:缓存/排行系统/统计器应用/社交网络/消息队列/热数据

    2.数据类型

    2-1.字符串类型

    • 命令相关:使用mget可以减少网络次数,提高开发效率(字符串不能超过512MB)
    • 内部编码:根据当前值的类型和长度决定使用哪种编码

      • int:8bytes长整型
      • embstr:小于等于39bytes的字符串
      • raw:大于39bytes的字符串
    • 底层数据结构:数组
    • 应用场景:缓存功能/计数/共享Seesion/限速

    2-2.哈希类型

    • 命令相关:键值本身又是一个键值对结构; set key field
    • 内部编码:

      • ziplist压缩列表;满足哈希类型元素个数小于hash-max-ziplist-entries和所有值小于hash-max-ziplist-value
      • hashtable哈希表:当无法满足ziplist的条件时,会使用hashtable作为哈希的内部实现
    • 底层数据结构:数组(hashkey) + 链表(field)
    • 应用场景:关系数据表记录

    2-3.列表类型

    • 命令相关:可以根据索引查询修改,阻塞删除,头尾添加;根据不同的命令可以实现队列/栈等操作
    • 内部编码:

      • ziplist压缩列表;满足哈希类型元素个数小于hash-max-ziplist-entries和所有值小于hash-max-ziplist-value
      • linkedlist链表:当无法满足ziplist的条件时,会使用linkedlist作为列表的内部实现
    • 底层数据结构:双向链表
    • 应用场景:消息队列/文章列表

    2-4.集合

    • 命令相关:一个集合中可以存放多个元素
    • 内部编码:

      • intset整数集合:当集合中的元素个数小于set-max-intset-entries配置
      • hashtable哈希表;当集合类型无法满足intset条件
    • 底层数据结构:hash
    • 应用场景:标签功能

    2-5.有序集合

    • 命令相关:一个集合中可以存放多个拥有分数的元素,用于排序
    • 内部编码:

      • ziplist压缩列表:当集合中的元素个数小于set-max-ziplist-entries配置
      • skiplist跳跃表;当集合类型无法满足ziplist条件时
    • 底层数据结构:hash + 跳跃表
    • 应用场景:排行系统

    3.小功能

    3-1.慢查询

    • 配置方式:a)配置文件;b)动态配置(rewrite持久化到本地配置文件中)
    • 获取慢查询日志:slowlog show get [n];采用队列存储,先进先出形式
    • 日志属性:日志标识,发生时间,命令耗时,执行命令和参数
    • 最佳实践:max-len > 1000 & slower-than 1ms

    3-2.Pipeline

    • 场景:将一组命令封装,通过一次RTT传输;能够减少网络延迟和性能瓶颈
    • Redis的批量命令与Pipeline区别:

      • 原子性:批量命令是原子性的
      • 命令格式:批量命令一次对应多个Key
      • 实现机制:Pipeline需要客户端支持
    • 最佳实践:可以将大Pipeline拆分成多个小Pipeline来完成

    3-3.事务和Lua

    • 事务:原子性,保证数据一致性
    • 相关命令:multi开始事务,exec事务提交,discard停止事务;watch确保事务中的key没有被其他客户端修改
    • 事务错误: a)语法错误:Redis事务忽略; b)运行错误:事务提交后执行报错
    • 不支持回滚:保持Redis的简单快捷;关于语法错误而失败应该在开发过程中被发现
    • Lua脚本:Redis脚本语言

      • Lua脚本在Redis中是原子执行的,执行过程中不插入其他命令
      • Lua脚本定制命令并可以常驻内存中,实现复用的效果
      • Lua脚本可以将多条命令一次性打包,减少网络开销

    3-4.Bitmaps和Hyperloglog

    • Bitmaps:一个以位为单位的数组,数组中的每个单元只能存储0和1,数组的下标称之为偏移量
    • 设置:setbit key offset value
    • 运算:bitop [and(交集) | or(并集) | not(非) | xor(异或)] destkey [keys...]
    • 应用场景:大用户量时统计活跃数,能有效减少内存
    • Hyperloglog:一种基数算法,实际数据类型为字符串类型,利用极小的内存空间完成独立总数的统计(不需要单条记录)

    3-5.发布和订阅

    发布者客户端向指定的频道(channel)发布消 息,订阅该频道的每个客户端都可以收到该消息

    • 相关命令:publish发布,subscrible订阅
    • 注意点:a)客户端在执行订阅命令之后进入订阅状态;b)新开启的订阅客户端,无法接受到该频道之前的消息,因为Redis不会对发布的消息进行持久化
    • 使用场景:聊天室/公告牌/消息解耦/视频管理系统

    3-6.GEO

    地理信息定位功能:支持存储地理位置信息来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能;底层实现是:zset

    4.客户端

    客户端通信协议:基于TCP协议定制RESP实现交互的
    RESP协议优点:a)实现容易;b)解析快;c)人类可读

    4-1.客户端管理

    4-2.客户端常见异常

    1.无法从连接池中获取到连接

    对象个数默认是8个:blockWhenExhausted=false代表连接池没有资源可能的原因:

    1. 连接池设置过小
    2. 没有释放连接
    3. 存在慢查询操作
    4. 服务端命令执行过程被堵塞

    2.客户端读写超时

    1.读写超时时间设置过短
    2.命令本身比较慢
    3.客户端与服务端网络不正常
    4.Redis自身发生堵塞

    3.客户端连接超时

    1.连接超时设置过短
    2.Redis发生堵塞,造成tcp-backlog已满
    3.客户端缓冲区异常
    4.输出缓冲区满
    5.长时间闲置连接被服务端主动断开
    6.不正常并发读写

    4.Lua脚本正在执行,并超过lua-time-limit
    5.Redis正在加载持久化文件
    6.Redis使用的内存超过maxmemory配置
    7.客户端连接数过大

    5.持久化

    5-1.RDB方式

    RDB持久化:把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化分为手动触发和自动触发

    • 手动触发:通过bgsave命令Redis进程执行fork操作创建子进程,由子进程负责完成
    • 自动触发:

      • save相关配置: sava m n (表示m秒内数据集存在n次修改即自动触发bgsave)
      • 从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点
      • 执行debug reload命令重新加载Redis时,也会自动触发save操作
      • 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave
    • 执行流程:

    image.png

    • RDB文件处理:保存和压缩并校验
    • RDB优点:

      • 代表某个时间点的数据快照,适用备份和全量复制等场景
      • 压缩的二进制文件,恢复“大数据集”效率较高
    • RDB缺点:

      • 数据的实时持久化较差并且fork()操作会带来堵塞
      • 特定的二进制文件会带来兼容性问题

    5-2.AOF方式

    AOF持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的,解决数据持久化的实时性

    • AOF工作流程:
    1.命令写入以追加方式到AOF_buf
    2.AOF缓冲区根据策略同步到AOF文件
    3.随着AOF文件变大,需要定期对AOF文件进行重写,达到压缩目的
    4.当Redis重启时,可以加载AOF文件进行数据恢复
    • 命令写入追加到缓冲区的目的:

      • 写入的内容是文件协议格式(1.兼容性;2.可读性;3.避免二次处理开销)
      • 在性能和安全性方面做出平衡
    • 同步策略:Redis提供多种AOF缓冲区同步文件策略,由appendfsync控制

      • always:调用write操作后并调用fsync保证AOF文件写入到磁盘中
      • no:调用write操作后,AOF文件同步到磁盘交由操作系统去实现
      • everysec:调用write操作后,由专门线程去每秒调用一次fsync
    1. write操作:会触发延迟写机制,因为Linux在内核提供页缓冲区来提供磁盘IO性能;write操作在写入系统缓冲区后直接返回,同步硬盘依赖于系统调度机制
    2. fsync操作:针对单个文件操作做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回,保证了数据持久化

    重写机制:把Redis进程内的数据转化为命令同步到新AOF文件的过程,这个过程会让AOF文件体积变小,从而提高恢复时的效率

    1.进程已经超时的数据不再写入新文件
    2.通过进程内数据直接生成,避免旧文件中的无效命令
    3.多条命令可以合并为一个

    AOF重写触发方式:
    手动触发:执行bgrewriteaof命令
    自动触发:同时满足以下2个条件

    1.auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB
    2.auto-aof-rewrite-percentage:表示当前AOF文件空间和上一次重写后AOF文件空间的比值
    • AOF重写工作流程

    image.png

    • Redis持久化加载流程

      1. AOF持久化开启时且存在AOF文件时,优先加载AOF文件
      2. AOF关闭或者AOF文件不存在时,加载RDB文件
      3. 加载AOF/RDB文件成功后,Redis启动成功
      4. AOF/RDB文件存在错误时,Redis启动失败
    • 关于AOF文件异常

      • 损坏的文件Redis服务会拒绝启动(可以通过redis-check-aof-fix命令修复后,进行对比并进行手工修改补全)
      • 结尾不挖完整的文件Redis服务忽略并继续启动,同时打印报警信息

    AOF追加阻塞:当开启AOF持久化时,常用的同步磁盘策略是everysec,对于这种方式,Redis会使用同步条线程每秒执行fsync同步硬盘,当系统硬盘资源繁忙时,会造成Redis主线程阻塞
    everysec刷盘策略过程

    1.同步线程负责每秒调用fsync操作进行同步磁盘
    2.主进程会去对比上次fsync同步时间,如果在2s内则通过,否则会堵塞(磁盘资源紧张)
    3.everysec策略最多可能丢失2s数据;如果系统fsync缓慢,会导致主进程堵塞
    • AOF优点:

      • 实时备份:能够到秒级
      • 以appen-only的模式写入:没有磁盘寻址开销,写入性能较高
      • 可读性强:具有更灵活的处理方式
    • AOF缺点:

      • 相同的数据集:AOF文件会比RDB文件大
      • Redis高负载情况下:RDB会拥有更好的性能保证
      • 数据恢复:比较慢并不适合全量备份

    6.复制

    6-1.配置

    • 建立复制的3种方式:

      • 配置文件:slaveof [masterHost] [masterPort]
      • 启动命令:redis-server --slaveof [masterHost] [masterPort]
      • 直接命令:slaveof [masterHost] [masterPort]
    • 断开复制:命令1操作后从节点晋升为主节点;命令2操作后可以完成切主操作

      • 命令1:slaveof no none
      • 命令2:slaveof [newmasterHost] [newmasterPort]
    • 安全性:主节点通过requirepass参数进行密码验证来保证数据安全性

      • 客户端校验:通过auth命令
      • 从节点校验:通过masterauth参数
    • 只读:默认情况下,从节点使用slave-read-only=yes配置为只读模式;因为从节点的数据无法同步给主节点
    • 传输延迟:主从节点一般部署在不同机器上,复制时的网络延迟成为需要考虑的问题

      • repl-disable-tcp-nodelay=yes时:代表关闭,主节点产生的命令无论大小都会及时发送给从节点,这样做延迟会变小,但是增加了网络带宽消耗
      • repl-disable-tcp-nodelay=no时:代表开启,主节点会合并比较小的TCP数据包从而节省网络带宽消耗,但是这样增加了主从之间的延迟

    6-2.复制原理

    复制过程

    1.保存主节点信息:IP+Port
    2.建立socket连接
    3.发送ping命令:a)检测socket是否可用;b)判断主节点是否能处理命令
    4.权限验证
    5.同步数据集:首次建立复制,主节点会把数据集全部发往从节点
    6.命令持续复制:主节点把持续写命令复制给从节点,保持数据一致性

    全量复制过程
    image.png

    部门复制过程
    image.png

    • 心跳判断:主从节点建立连接后保持长连接

      • 主节点默认每隔10s对从节点发送ping命令,判断从节点的存活性和连接状态(repl-ping-slave-period控制发送频率)
      • 从节点在主线程每隔1s发送replconf ack {offset}命令,给主节点上报自身当前的复制偏移量
      • 如果超过repl-timeout配置的值(默认60秒),则判定从节点下线并断开复制客户端连接
    • 补充知识点:

      • Redis为了保证高性能复制过程是异步的,写命令处理完后直接返回给客户端,不等待从节点复制完成。因此从节点数据集会有延迟情况
      • 当使用从节点用于读写分离时会存在数据延迟、过期数据、从节点可用性等问题,需要根据自身业务提前作出规避
      • 在运维过程中,主节点存在多个从节点或者一台机器上部署大量主节点的情况下,会有复制风暴的风险

    7.阻塞

    Redis是单线程架构:所有读写操作都是串行的而会导致阻塞问题

    内在原因:不合理使用API或数据结构、CPU饱和、持久化阻塞
    外在原因:CPU竞争,内存交换,网络问题等

    7-1.发现堵塞

    • 登录Redis:执行info,查看blocked_clients
    • 执行redis-cli --latency -h -p 查看延时情况

    7-2.内在原因

    • 不合理使用API或数据结构:比如执行hgetall获取的数据量会非常大

      • 获取慢查询:a) 修改为低复杂度命令;b) 调整大对象
      • 获取Bigkey:redis-cli <ip+port> bigkeys(大于10K)
    • CPU饱和:把单核CPU占用率达到100%
    • 持久化阻塞:a) fork阻塞;b)AOF刷盘阻塞;c) HugePage写操作阻塞

    7-3.外在原因

    • CPU竞争:其他进程过度消耗CPU时,会影响Redis性能
    • 内存交换:使用的部分内存换出到硬盘
    • 网络问题:a)网络闪断;b)连接拒绝;c)连接溢出

    8.内存

    8-1.内存消耗

    重点指标:mem_fragmentation_ratio

    mem_fragmentation_ratio = used_memory_rss / used_memory
    used_memory_rss: 系统认为Redis使用的物理内存
    used_memory: 内部存储的所有数据占用量
    mem_fragmentation_ratio > 1表示存在内存碎片
    mem_fragmentation_ratio < 1表示存在交换内存
    • Redis内存消耗划分

      • 自身内存
      • 对象内存:用户所有数据
      • 缓冲内存:客户端缓冲/复制积压缓冲/AOF缓冲
      • 内存碎片:频繁更新操作或大量过期键删除导致释放的空间无法利用
    • 子进程内存消耗:指AOF/RDB重写时Redis创建的子进程复制内存的消耗
    • THP机制:虽然可以降低fork子进程的速度,但复制内存页的单位会从4KB变为2MB,如果父进程有大量写命令,会加重内存拷贝量

    8-2.内存管理

    Redis使用maxmemroy参数限制最大可用内存,限制内存的主要目的:

    1.缓存场景:当超过内存上限时根据淘汰策略删除键释放内存
    2.防止所用内存超过物理内存(限制的是used_memory;所以考虑内存溢出)

    内存回收策略

    • 删除过期键带有

      • 惰性删除:如果存在过期键,当客户单请求到的时候进行删除并返回空
      • 定时任务删除:默认每秒运行10次(通过配置hz控制);删除过期键逻辑采用自适应算法,根据键的过期比例,使用快慢两种速率模式回收键
    • 内存溢出控制策略:当达到maxmemory自动触发

      • noeviction:默认策略,不删除任何数据,拒绝所有写入操作并返回客户端错误信息
      • volatile-lru:根据LRU算法删除设置超时属性的键
      • volatile-random:随机删除过期键
      • allkeys-lru:根据LRU算法随机删除键
      • allkeys-random:随机删除所有键
      • volatile-ttl:根据键对象的ttl属性删除最近将要过期数据
    1.关于volatile-lru和volatile-ttl控制策略:如果没有,会回退到noeviction控制策略
    2.在Redis的LRU算法中:可以通过设置样本的数量来调优算法精度(参数:maxmemory-samples 5->10)

    8-3.内存优化

    内存优化总结

    1.精简键值对大小,键值字面量精简,使用高效二进制序列化工具。
    2.使用对象共享池优化小整数对象。
    3.数据优先使用整数,比字符串类型更节省空间。
    4.优化字符串使用,避免预分配造成的内存浪费。
    5.使用ziplist压缩编码优化hash、list等结构,注重效率和空间的平衡。
    6.使用intset编码优化整数集合。
    7.使用ziplist编码的hash结构降低小对象链规

    9.高可用架构

    主从架构问题

    1. 当主节点出现故障,需要手动晋升新的主节点
    2. 主节点的写能力受单机限制
    3. 主节点的存储能力受单机限制

    Sentinel架构问题

    1. 系统架构变复杂后,较难支持在线扩容
    2. 主节点的存储能力受单机限制

    Cluster架构问题

    1. 部分功能受限(key批量操作,key事务操作等等)

    9-1.Sentinel

    Sentinel节点集合会定期对所有节点进行监控,从而实现主从的故障自动转移

    • 监控任务

      • 每隔10s:每个Sentinel节点会向master节点执行info命令获取最新的拓扑信息
      • 每隔2s:每个Sentinel节点会向Redis数据节点的__sentinel__频道发送消息而发现新Sentinel节点及交换主节点状态
      • 每隔1s:每个Sentinel节点会向其他数据节点和Sentinel节点发送ping命令来进行心跳检测
    • 主观下线和客观下线

      • 主观下线:任意Sentinel节点进行心跳检测时在超时时间内没有收到响应消息即判断该数据节点下线
      • 客观下线:所有Sentinel节点的判断票超过quorum个数后,即认为该数据节点下线
    • 领导Sentinel节点选举:故障转移工作的执行者

      • 采用Raft算法进行选举,一般来说哪个Sentinel节点发现客观下线情况就会成为执行者
    • 故障转移过程:

      1. 执行者在从列表中选举出一个节点作为新节点:优先级 > 复制偏移量 > Runid小的
      2. 执行者对新节点执行slave no one命令让其成为主节点
      3. 执行者向其他节点发送命令,让其成为新主节点的从节点
      4. Sentinel集合将原来的主节点更新为从节点(恢复后不抢占)
    • 读写分离:将Slave节点集合作为“读”资源连接池,依赖Sentinel节点的消息通知,获取Redis节点的状态变化

    9-2.Cluster

    • 数据分布:Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据
    • Gossip协议:节点间不断通信交换信息,一段时间后所有节点都会知道整个集群的元数据信息
    • Gossip消息分类:ping/pong/meet/fail

      • meet消息:用于新节点加入
      • ping消息:用于检测节点是否在线和交换彼此状态信息
      • pong消息:当接收到ping和meet消息时,作为响应消息回复
      • fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息
    • 通信节点选择规则:

    image.png

    • 集群伸缩:槽和数据在节点之间的移动;当有新节点加入的时候,每个节点负责的槽和数据会迁移一部分给新节点;反之亦然
    • 请求路由:使用客户端去操作集群

      • 请求重定向:Redis首先计算键对应的槽,再根据槽找出对应的节点,如果是自身节点,则执行命令,否则恢复MOVED重定向发送键命令到目标节点(返回key所对应的槽并不负责转发)
    • Smart客户端:内部维护slot-node映射关系,本地实现键到节点的查找,而MOVE重定向只负责维护客户端的映射关系,减少每次都需要redis节点的命令执行重定向带来的IO开销

    故障转移

    • 故障发现:通过ping/pong消息实现节点通信

      • 主观下线:某个节点认为另一个节点不可用(标记为下线状态)
      • 客观下线:多个节点认为另一个节点不可用(标记为不可用)
    • 恢复流程:

      1. 资格检查:检查最后与主节点断线时间
      2. 准备选举时间:延迟触发机制,通过offset判断从节点优先级
      3. 发起选举:更配置纪元标识本次从节点选举的版本
      4. 选举投票:从节点拥有N/2+1个主节点票时,可以执行替换操作
      5. 替换主节点:负责故障主节点的槽并向集群广播次消息

    开发和运维常见问题::超大规模集群带宽消耗, pub/sub广播问题,集群节点倾斜问题,手动故障转移,在线迁移数据等

    10.缓存设计

    缓存收益:a)加速速度;b)减少后端负载
    缓存成本:a)数据不一致性;b)代码维护;c)运维
    缓存场景:a)开销大的复杂计算;b)加速请求响应

    10-1.缓存更新策略

    LRU/LFU/FIFO算法剔除:缓存使用量超过设定的最大值(maxmemory-policy配置剔除策略)
    超时剔除:缓存数据设置过期时间
    主动更新:数据一致性要求高,需要真实数据更新后立马更新缓存数据
    • 最佳实践

      • 低一致性业务建议设置最大内存和淘汰策略的方式使用
      • 高一致性业务建议结合超时剔除和主动更新

    10-2.缓存穿透

    缓存穿透:查询一个根本不存在的数据,导致不存在的数据每次请求都要到存储层去查询,会使后端存储负载加大
    基本原因

    1.自身业务代码或者数据出现问题
    2.恶意攻击、爬虫等造成大量空命中

    解决办法

    1.缓存空对象
    2.布隆过滤器

    10-3.缓存雪崩

    缓存雪崩:缓存层宕掉后,流量会突然全部打到后端存储
    预防和解决缓存雪崩问题

    1.保证缓存层服务高可用性
    2.依赖隔离组件为后端限流并降级

    10-4.无底洞问题

    10-5.热点key重建

    问题原因:当前key是一个热点key,并发量非常大而重建缓存又不能在短时间内完成
    解决办法:互斥锁、“永远不过期”能够在一定程度上解决热点key问题

    11.其他

    11-1.Linux配置优化

    内存分配控制优化

    1.Redis设置合理的maxmemory,保证机器有20%~30%的限制内存
    2.设置vm.overcommit_memory=1,防止极端情况下造成fork失败

    Swap交换内存优化:当物理内存不足时,系统会使用swap导致磁盘IO会成为性能瓶颈

    权值越大,使用swap概率越高:0-100 默认60:
    echo "vm.swappiness={bestvalue}" >> /etc/sysctl.conf

    OMM killer: 当内存不足时选择性杀死进程

    降低redis优先级:
    echo {value} > /proc/{pid}/oom_adj

    Transparent Huge Pages:虽然可以加快fork操作,但是写时内存copy消耗从4KB-2MB

    关闭大页:
    echo never > /sys/kernel/mm/transparent_hugepage/enabled

    打开文件描述符

    因为openfile优先级大于redis maxclients
    /etc/rc.local配置文件中:ulimit -Sn {max-open-files}

    Tcp backlog: Tcp连接队列长度

    调高全连接队列值:默认是511
    echo 10000 > /proc/sys/net/core/somaxconn

    11-2.误操作恢复

    • AOF机制恢复:文件中有追加记录;恢复时,将AOF文件中的fulshall相关操作去掉,然后使用redis-check-aof工具校验和修复一下AOF文件;如果发生AOF重写,意味着之前的数据就丢掉了
    • RDB机制恢复:文件中依然有追加记录;要注意:防止bgsave命令执行,这样旧的RDB文件会被覆盖;如果开启RDB自动策略,flush涉及键值数量较多,RDB文件会被清除,这样恢复就无望

    快速恢复数据:

    1.防止AOF重写
    2.去掉AOF文件中的flush相关内容
    3.重启Redis服务器,恢复数据

    11-3.Bigkey问题

    危害

    1.网络拥塞
    2.超时堵塞
    3.内存空间不均匀

    定位:找到键的serializedlength信息,然后判断指定键值的大小

    1.debug object key
    2.strlen key
    3.主动检测:scan+debug object;然后检测每个键值的长度
    补充:使用redis-cli -h[ip] -p[port] bigkeys命令(内部进行scan操作,把历史扫描过的最大对象统计出来)

    优雅删除:直接删除所有bigkey可能会导致堵塞

    可以结合Python的RedisAPI编写脚本去实现:
    1.hash key:通过hscan命令,每次获取500个字段,再用hdel命令
    2.set key:使用sscan命令,每次扫描集合中500个元素,再用srem命令每次删除一个元素;
    3.list key:删除大的List键,通过ltrim命令每次删除少量元素。
    4.sorted set key:删除大的有序集合键,和List类似,使用sortedset自带的zremrangebyrank命令,每次删除top 100个元素。
    后台删除:lazyfree机制

    11-4.分布式锁

    11-5.安全建议

    1. 根据具体网络环境决定是否设置Redis密码
    2. rename-command可以伪装命令,但是要注意成本
    3. 合理的防火墙是防止攻击的利器
    4. bind可以将Redis的访问绑定到指定网卡上
    5. 定期备份数据应该作为习惯性操作
    6. 可以适当错开Redis默认端口启动
    7. 使用非root用户启动Redis

    参考

    • 《Redis开发与运维》
    查看原文

    赞 4 收藏 3 评论 0

    Gabriel 赞了文章 · 2月20日

    MySQL 性能优化神器 Explain 使用分析

    简介

    MySQL 提供了一个 EXPLAIN 命令, 它可以对 SELECT 语句进行分析, 并输出 SELECT 执行的详细信息, 以供开发人员针对性优化.
    EXPLAIN 命令用法十分简单, 在 SELECT 语句前加上 Explain 就可以了, 例如:

    EXPLAIN SELECT * from user_info WHERE  id < 300;

    准备

    为了接下来方便演示 EXPLAIN 的使用, 首先我们需要建立两个测试用的表, 并添加相应的数据:

    CREATE TABLE `user_info` (
      `id`   BIGINT(20)  NOT NULL AUTO_INCREMENT,
      `name` VARCHAR(50) NOT NULL DEFAULT '',
      `age`  INT(11)              DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `name_index` (`name`)
    )
      ENGINE = InnoDB
      DEFAULT CHARSET = utf8
    
    INSERT INTO user_info (name, age) VALUES ('xys', 20);
    INSERT INTO user_info (name, age) VALUES ('a', 21);
    INSERT INTO user_info (name, age) VALUES ('b', 23);
    INSERT INTO user_info (name, age) VALUES ('c', 50);
    INSERT INTO user_info (name, age) VALUES ('d', 15);
    INSERT INTO user_info (name, age) VALUES ('e', 20);
    INSERT INTO user_info (name, age) VALUES ('f', 21);
    INSERT INTO user_info (name, age) VALUES ('g', 23);
    INSERT INTO user_info (name, age) VALUES ('h', 50);
    INSERT INTO user_info (name, age) VALUES ('i', 15);
    CREATE TABLE `order_info` (
      `id`           BIGINT(20)  NOT NULL AUTO_INCREMENT,
      `user_id`      BIGINT(20)           DEFAULT NULL,
      `product_name` VARCHAR(50) NOT NULL DEFAULT '',
      `productor`    VARCHAR(30)          DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `user_product_detail_index` (`user_id`, `product_name`, `productor`)
    )
      ENGINE = InnoDB
      DEFAULT CHARSET = utf8
    
    INSERT INTO order_info (user_id, product_name, productor) VALUES (1, 'p1', 'WHH');
    INSERT INTO order_info (user_id, product_name, productor) VALUES (1, 'p2', 'WL');
    INSERT INTO order_info (user_id, product_name, productor) VALUES (1, 'p1', 'DX');
    INSERT INTO order_info (user_id, product_name, productor) VALUES (2, 'p1', 'WHH');
    INSERT INTO order_info (user_id, product_name, productor) VALUES (2, 'p5', 'WL');
    INSERT INTO order_info (user_id, product_name, productor) VALUES (3, 'p3', 'MA');
    INSERT INTO order_info (user_id, product_name, productor) VALUES (4, 'p1', 'WHH');
    INSERT INTO order_info (user_id, product_name, productor) VALUES (6, 'p1', 'WHH');
    INSERT INTO order_info (user_id, product_name, productor) VALUES (9, 'p8', 'TE');

    EXPLAIN 输出格式

    EXPLAIN 命令的输出内容大致如下:

    mysql> explain select * from user_info where id = 2\G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: user_info
       partitions: NULL
             type: const
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 8
              ref: const
             rows: 1
         filtered: 100.00
            Extra: NULL
    1 row in set, 1 warning (0.00 sec)

    各列的含义如下:

    • id: SELECT 查询的标识符. 每个 SELECT 都会自动分配一个唯一的标识符.

    • select_type: SELECT 查询的类型.

    • table: 查询的是哪个表

    • partitions: 匹配的分区

    • type: join 类型

    • possible_keys: 此次查询中可能选用的索引

    • key: 此次查询中确切使用到的索引.

    • ref: 哪个字段或常数与 key 一起被使用

    • rows: 显示此查询一共扫描了多少行. 这个是一个估计值.

    • filtered: 表示此查询条件所过滤的数据的百分比

    • extra: 额外的信息

    接下来我们来重点看一下比较重要的几个字段.

    select_type

    select_type 表示了查询的类型, 它的常用取值有:

    • SIMPLE, 表示此查询不包含 UNION 查询或子查询

    • PRIMARY, 表示此查询是最外层的查询

    • UNION, 表示此查询是 UNION 的第二或随后的查询

    • DEPENDENT UNION, UNION 中的第二个或后面的查询语句, 取决于外面的查询

    • UNION RESULT, UNION 的结果

    • SUBQUERY, 子查询中的第一个 SELECT

    • DEPENDENT SUBQUERY: 子查询中的第一个 SELECT, 取决于外面的查询. 即子查询依赖于外层查询的结果.

    最常见的查询类别应该是 SIMPLE 了, 比如当我们的查询没有子查询, 也没有 UNION 查询时, 那么通常就是 SIMPLE 类型, 例如:

    mysql> explain select * from user_info where id = 2\G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: user_info
       partitions: NULL
             type: const
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 8
              ref: const
             rows: 1
         filtered: 100.00
            Extra: NULL
    1 row in set, 1 warning (0.00 sec)

    如果我们使用了 UNION 查询, 那么 EXPLAIN 输出 的结果类似如下:

    mysql> EXPLAIN (SELECT * FROM user_info  WHERE id IN (1, 2, 3))
        -> UNION
        -> (SELECT * FROM user_info WHERE id IN (3, 4, 5));
    +----+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+
    | id | select_type  | table      | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra           |
    +----+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+
    |  1 | PRIMARY      | user_info  | NULL       | range | PRIMARY       | PRIMARY | 8       | NULL |    3 |   100.00 | Using where     |
    |  2 | UNION        | user_info  | NULL       | range | PRIMARY       | PRIMARY | 8       | NULL |    3 |   100.00 | Using where     |
    | NULL | UNION RESULT | <union1,2> | NULL       | ALL   | NULL          | NULL    | NULL    | NULL | NULL |     NULL | Using temporary |
    +----+--------------+------------+------------+-------+---------------+---------+---------+------+------+----------+-----------------+
    3 rows in set, 1 warning (0.00 sec)

    table

    表示查询涉及的表或衍生表

    type

    type 字段比较重要, 它提供了判断查询是否高效的重要依据依据. 通过 type 字段, 我们判断此次查询是 全表扫描 还是 索引扫描 等.

    type 常用类型

    type 常用的取值有:

    • system: 表中只有一条数据. 这个类型是特殊的 const 类型.

    • const: 针对主键或唯一索引的等值查询扫描, 最多只返回一行数据. const 查询速度非常快, 因为它仅仅读取一次即可.
      例如下面的这个查询, 它使用了主键索引, 因此 type 就是 const 类型的.

    mysql> explain select * from user_info where id = 2\G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: user_info
       partitions: NULL
             type: const
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 8
              ref: const
             rows: 1
         filtered: 100.00
            Extra: NULL
    1 row in set, 1 warning (0.00 sec)
    • eq_ref: 此类型通常出现在多表的 join 查询, 表示对于前表的每一个结果, 都只能匹配到后表的一行结果. 并且查询的比较操作通常是 =, 查询效率较高. 例如:

    mysql> EXPLAIN SELECT * FROM user_info, order_info WHERE user_info.id = order_info.user_id\G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: order_info
       partitions: NULL
             type: index
    possible_keys: user_product_detail_index
              key: user_product_detail_index
          key_len: 314
              ref: NULL
             rows: 9
         filtered: 100.00
            Extra: Using where; Using index
    *************************** 2. row ***************************
               id: 1
      select_type: SIMPLE
            table: user_info
       partitions: NULL
             type: eq_ref
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 8
              ref: test.order_info.user_id
             rows: 1
         filtered: 100.00
            Extra: NULL
    2 rows in set, 1 warning (0.00 sec)
    • ref: 此类型通常出现在多表的 join 查询, 针对于非唯一或非主键索引, 或者是使用了 最左前缀 规则索引的查询.
      例如下面这个例子中, 就使用到了 ref 类型的查询:

    mysql> EXPLAIN SELECT * FROM user_info, order_info WHERE user_info.id = order_info.user_id AND order_info.user_id = 5\G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: user_info
       partitions: NULL
             type: const
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 8
              ref: const
             rows: 1
         filtered: 100.00
            Extra: NULL
    *************************** 2. row ***************************
               id: 1
      select_type: SIMPLE
            table: order_info
       partitions: NULL
             type: ref
    possible_keys: user_product_detail_index
              key: user_product_detail_index
          key_len: 9
              ref: const
             rows: 1
         filtered: 100.00
            Extra: Using index
    2 rows in set, 1 warning (0.01 sec)
    • range: 表示使用索引范围查询, 通过索引字段范围获取表中部分数据记录. 这个类型通常出现在 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, IN() 操作中.
      typerange 时, 那么 EXPLAIN 输出的 ref 字段为 NULL, 并且 key_len 字段是此次查询中使用到的索引的最长的那个.

    例如下面的例子就是一个范围查询:

    mysql> EXPLAIN SELECT *
        ->         FROM user_info
        ->         WHERE id BETWEEN 2 AND 8 \G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: user_info
       partitions: NULL
             type: range
    possible_keys: PRIMARY
              key: PRIMARY
          key_len: 8
              ref: NULL
             rows: 7
         filtered: 100.00
            Extra: Using where
    1 row in set, 1 warning (0.00 sec)
    • index: 表示全索引扫描(full index scan), 和 ALL 类型类似, 只不过 ALL 类型是全表扫描, 而 index 类型则仅仅扫描所有的索引, 而不扫描数据.
      index 类型通常出现在: 所要查询的数据直接在索引树中就可以获取到, 而不需要扫描数据. 当是这种情况时, Extra 字段 会显示 Using index.

    例如:

    mysql> EXPLAIN SELECT name FROM  user_info \G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: user_info
       partitions: NULL
             type: index
    possible_keys: NULL
              key: name_index
          key_len: 152
              ref: NULL
             rows: 10
         filtered: 100.00
            Extra: Using index
    1 row in set, 1 warning (0.00 sec)

    上面的例子中, 我们查询的 name 字段恰好是一个索引, 因此我们直接从索引中获取数据就可以满足查询的需求了, 而不需要查询表中的数据. 因此这样的情况下, type 的值是 index, 并且 Extra 的值是 Using index.

    • ALL: 表示全表扫描, 这个类型的查询是性能最差的查询之一. 通常来说, 我们的查询不应该出现 ALL 类型的查询, 因为这样的查询在数据量大的情况下, 对数据库的性能是巨大的灾难. 如一个查询是 ALL 类型查询, 那么一般来说可以对相应的字段添加索引来避免.
      下面是一个全表扫描的例子, 可以看到, 在全表扫描时, possible_keys 和 key 字段都是 NULL, 表示没有使用到索引, 并且 rows 十分巨大, 因此整个查询效率是十分低下的.

    mysql> EXPLAIN SELECT age FROM  user_info WHERE age = 20 \G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: user_info
       partitions: NULL
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 10
         filtered: 10.00
            Extra: Using where
    1 row in set, 1 warning (0.00 sec)

    type 类型的性能比较

    通常来说, 不同的 type 类型的性能关系如下:
    ALL < index < range ~ index_merge < ref < eq_ref < const < system
    ALL 类型因为是全表扫描, 因此在相同的查询条件下, 它是速度最慢的.
    index 类型的查询虽然不是全表扫描, 但是它扫描了所有的索引, 因此比 ALL 类型的稍快.
    后面的几种类型都是利用了索引来查询数据, 因此可以过滤部分或大部分数据, 因此查询效率就比较高了.

    possible_keys

    possible_keys 表示 MySQL 在查询时, 能够使用到的索引. 注意, 即使有些索引在 possible_keys 中出现, 但是并不表示此索引会真正地被 MySQL 使用到. MySQL 在查询时具体使用了哪些索引, 由 key 字段决定.

    key

    此字段是 MySQL 在当前查询时所真正使用到的索引.

    key_len

    表示查询优化器使用了索引的字节数. 这个字段可以评估组合索引是否完全被使用, 或只有最左部分字段被使用到.
    key_len 的计算规则如下:

    • 字符串

      • char(n): n 字节长度

      • varchar(n): 如果是 utf8 编码, 则是 3 n + 2字节; 如果是 utf8mb4 编码, 则是 4 n + 2 字节.

    • 数值类型:

      • TINYINT: 1字节

      • SMALLINT: 2字节

      • MEDIUMINT: 3字节

      • INT: 4字节

      • BIGINT: 8字节

    • 时间类型

      • DATE: 3字节

      • TIMESTAMP: 4字节

      • DATETIME: 8字节

    • 字段属性: NULL 属性 占用一个字节. 如果一个字段是 NOT NULL 的, 则没有此属性.

    我们来举两个简单的栗子:

    mysql> EXPLAIN SELECT * FROM order_info WHERE user_id < 3 AND product_name = 'p1' AND productor = 'WHH' \G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: order_info
       partitions: NULL
             type: range
    possible_keys: user_product_detail_index
              key: user_product_detail_index
          key_len: 9
              ref: NULL
             rows: 5
         filtered: 11.11
            Extra: Using where; Using index
    1 row in set, 1 warning (0.00 sec)

    上面的例子是从表 order_info 中查询指定的内容, 而我们从此表的建表语句中可以知道, 表 order_info 有一个联合索引:

    KEY `user_product_detail_index` (`user_id`, `product_name`, `productor`)

    不过此查询语句 WHERE user_id < 3 AND product_name = 'p1' AND productor = 'WHH' 中, 因为先进行 user_id 的范围查询, 而根据 最左前缀匹配 原则, 当遇到范围查询时, 就停止索引的匹配, 因此实际上我们使用到的索引的字段只有 user_id, 因此在 EXPLAIN 中, 显示的 key_len 为 9. 因为 user_id 字段是 BIGINT, 占用 8 字节, 而 NULL 属性占用一个字节, 因此总共是 9 个字节. 若我们将user_id 字段改为 BIGINT(20) NOT NULL DEFAULT '0', 则 key_length 应该是8.

    上面因为 最左前缀匹配 原则, 我们的查询仅仅使用到了联合索引的 user_id 字段, 因此效率不算高.

    接下来我们来看一下下一个例子:

    mysql> EXPLAIN SELECT * FROM order_info WHERE user_id = 1 AND product_name = 'p1' \G;
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: order_info
       partitions: NULL
             type: ref
    possible_keys: user_product_detail_index
              key: user_product_detail_index
          key_len: 161
              ref: const,const
             rows: 2
         filtered: 100.00
            Extra: Using index
    1 row in set, 1 warning (0.00 sec)

    这次的查询中, 我们没有使用到范围查询, key_len 的值为 161. 为什么呢? 因为我们的查询条件 WHERE user_id = 1 AND product_name = 'p1' 中, 仅仅使用到了联合索引中的前两个字段, 因此 keyLen(user_id) + keyLen(product_name) = 9 + 50 * 3 + 2 = 161

    rows

    rows 也是一个重要的字段. MySQL 查询优化器根据统计信息, 估算 SQL 要查找到结果集需要扫描读取的数据行数.
    这个值非常直观显示 SQL 的效率好坏, 原则上 rows 越少越好.

    Extra

    EXplain 中的很多额外的信息会在 Extra 字段显示, 常见的有以下几种内容:

    • Using filesort
      当 Extra 中有 Using filesort 时, 表示 MySQL 需额外的排序操作, 不能通过索引顺序达到排序效果. 一般有 Using filesort, 都建议优化去掉, 因为这样的查询 CPU 资源消耗大.

    例如下面的例子:

    mysql> EXPLAIN SELECT * FROM order_info ORDER BY product_name \G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: order_info
       partitions: NULL
             type: index
    possible_keys: NULL
              key: user_product_detail_index
          key_len: 253
              ref: NULL
             rows: 9
         filtered: 100.00
            Extra: Using index; Using filesort
    1 row in set, 1 warning (0.00 sec)

    我们的索引是

    KEY `user_product_detail_index` (`user_id`, `product_name`, `productor`)

    但是上面的查询中根据 product_name 来排序, 因此不能使用索引进行优化, 进而会产生 Using filesort.
    如果我们将排序依据改为 ORDER BY user_id, product_name, 那么就不会出现 Using filesort 了. 例如:

    mysql> EXPLAIN SELECT * FROM order_info ORDER BY user_id, product_name \G
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: order_info
       partitions: NULL
             type: index
    possible_keys: NULL
              key: user_product_detail_index
          key_len: 253
              ref: NULL
             rows: 9
         filtered: 100.00
            Extra: Using index
    1 row in set, 1 warning (0.00 sec)
    • Using index
      "覆盖索引扫描", 表示查询在索引树中就可查找所需数据, 不用扫描表数据文件, 往往说明性能不错

    • Using temporary
      查询有使用临时表, 一般出现于排序, 分组和多表 join 的情况, 查询效率不高, 建议优化.

    查看原文

    赞 293 收藏 303 评论 20

    Gabriel 赞了文章 · 2月20日

    必须了解的mysql三大日志-binlog、redo log和undo log

    来源:https://juejin.im/post/686025...
    作者:六点半起床

    日志是 mysql 数据库的重要组成部分,记录着数据库运行期间各种状态信息。 mysql
    日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。作为开发,我们重点需要关注的是二进制日志( binlog )和事务日志(包括
    redo log undo log ),本文接下来会详细介绍这三种日志。

    binlog

    binlog 用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。 binlog mysql
    的逻辑日志,并且由 Server 层进行记录,使用任何存储引擎的 mysql 数据库都会记录 binlog 日志。

    • 逻辑日志: 可以简单理解为记录的就是sql语句 。
    • 物理日志 mysql 数据最终是保存在数据页中的,物理日志记录的就是数据页变更 。

    binlog 是通过追加的方式进行写入的,可以通过 max_binlog_size 参数设置每个 binlog
    文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。

    binlog使用场景

    在实际应用中, binlog 的主要使用场景有两个,分别是 主从复制数据恢复

    1. 主从复制 :在 Master 端开启 binlog ,然后将 binlog 发送到各个 Slave 端, Slave 端重放 binlog 从而达到主从数据一致。
    2. 数据恢复 :通过使用 mysqlbinlog 工具来恢复数据。

    binlog刷盘时机

    对于 InnoDB 存储引擎而言,只有在事务提交时才会记录 biglog ,此时记录还在内存中,那么 biglog
    是什么时候刷到磁盘中的呢? mysql 通过 sync_binlog 参数控制 biglog 的刷盘时机,取值范围是 0-N

    • 0:不去强制要求,由系统自行判断何时写入磁盘;
    • 1:每次 commit 的时候都要将 binlog 写入磁盘;
    • N:每N个事务,才会将 binlog 写入磁盘。

    从上面可以看出, sync_binlog 最安全的是设置是 1 ,这也是 MySQL 5.7.7
    之后版本的默认值。但是设置一个大一些的值可以提升数据库性能,因此实际情况下也可以将值适当调大,牺牲一定的一致性来获取更好的性能。

    binlog日志格式

    binlog 日志有三种格式,分别为 STATMENT ROW MIXED

    MySQL 5.7.7 之前,默认的格式是 STATEMENT MySQL 5.7.7 之后,默认值是 ROW 。日志格式通过 binlog-format 指定。
    • STATMENT : 基于 SQL 语句的复制( statement-based replication, SBR ),每一条会修改数据的sql语句会记录到 binlog 中 。

      * 优点: 不需要记录每一行的变化,减少了` binlog ` 日志量,节约了 ` IO ` , 从而提高了性能; 
      * 缺点: 在某些情况下会导致主从数据不一致,比如执行` sysdate() ` 、 ` slepp() ` 等 。 
    • ROW : 基于行的复制( row-based replication, RBR ),不记录每条sql语句的上下文信息,仅需记录哪条数据被修改了 。

      • 优点: 不会出现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的问题 ;
      • 缺点: 会产生大量的日志,尤其是 alter table 的时候会让日志暴涨
    • MIXED : 基于 STATMENT ROW 两种模式的混合复制( mixed-based replication, MBR ),一般的复制使用 STATEMENT 模式保存 binlog ,对于 STATEMENT 模式无法复制的操作使用 ROW 模式保存 binlog

    redo log

    为什么需要redo log

    我们都知道,事务的四大特性里面有一个是 持久性 ,具体来说就是
    只要事务提交成功,那么对数据库做的修改就被永久保存下来了,不可能因为任何原因再回到原来的状态 。那么 mysql
    是如何保证一致性的呢?最简单的做法是在每次事务提交的时候,将该事务涉及修改的数据页全部刷新到磁盘中。但是这么做会有严重的性能问题,主要体现在两个方面:

    1. 因为 Innodb 是以 为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这个时候将完整的数据页刷到磁盘的话,太浪费资源了!
    2. 一个事务可能涉及修改多个数据页,并且这些数据页在物理上并不连续,使用随机IO写入性能太差!

    因此 mysql 设计了 redo log 具体来说就是只记录事务对数据页做了哪些修改
    ,这样就能完美地解决性能问题了(相对而言文件更小并且是顺序IO)。

    redo log基本概念

    redo log 包括两部分:一个是内存中的日志缓冲( redo log buffer ),另一个是磁盘上的日志文件( ` redo log
    file )。 mysql 每执行一条 DML 语句,先将记录写入 redo log buffer `
    ,后续某个时间点再一次性将多个操作记录写到 redo log file 。这种 先写日志,再写磁盘 的技术就是 MySQL
    里经常说到的 WAL(Write-Ahead Logging) 技术。

    在计算机操作系统中,用户空间( user space )下的缓冲区数据一般情况下是无法直接写入磁盘的,中间必须经过操作系统内核空间( `
    kernel space )缓冲区( OS Buffer )。因此, redo log buffer 写入 redo log
    file 实际上是先写入 OS Buffer ,然后再通过系统调用 fsync() 将其刷到 redo log file `
    中,过程如下:

    mysql 支持三种将 redo log buffer 写入 redo log file 的时机,可以通过 `
    innodb_flush_log_at_trx_commit ` 参数配置,各参数值含义如下:

    参数值含义
    0(延迟写)事务提交时不会将 redo log buffer 中日志写入到 os buffer ,而是每秒写入 os buffer 并调用 fsync() 写入到 redo log file 中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
    1(实时写,实时刷)事务每次提交都会将 redo log buffer 中的日志写入 os buffer 并调用 fsync() 刷到 redo log file 中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
    2(实时写,延迟刷)每次提交都仅写入到 os buffer ,然后是每秒调用 fsync() os buffer 中的日志写入到 redo log file

    redo log记录形式

    前面说过, redo log 实际上记录数据页的变更,而这种变更记录是没必要全部保存,因此 redo log
    实现上采用了大小固定,循环写入的方式,当写到结尾时,会回到开头循环写日志。如下图:

    同时我们很容易得知, 在innodb中,既有 redo log 需要刷盘,还有 数据页 也需要刷盘, redo log 存在的意义主要就是降低对 数据页 刷盘的要求 。在上图中, write pos 表示 redo log 当前记录的 LSN (逻辑序列号)位置, check point 表示 数据页更改记录** 刷盘后对应 redo log 所处的 LSN (逻辑序列号)位置。 write pos check point 之间的部分是 redo log 空着的部分,用于记录新的记录; check point write pos 之间是 redo log 待落盘的数据页更改记录。当 write pos 追上 check point 时,会先推动 check point 向前移动,空出位置再记录新的日志。

    启动 innodb 的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。因为 redo log 记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志(如 binlog )要快很多。 重启 innodb 时,首先会检查磁盘中数据页的 LSN ,如果数据页的 LSN 小于日志中的 LSN ,则会从 checkpoint 开始恢复。 还有一种情况,在宕机前正处于
    checkpoint 的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度,此时会出现数据页中记录的 LSN 大于日志中的 LSN
    ,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。

    redo log与binlog区别

    redo logbinlog
    文件大小 redo log 的大小是固定的。 binlog 可通过配置参数 max_binlog_size 设置每个 binlog 文件的大小。
    实现方式 redo log InnoDB 引擎层实现的,并不是所有引擎都有。 binlog Server 层实现的,所有引擎都可以使用 binlog 日志
    记录方式redo log 采用循环写的方式记录,当写到结尾时,会回到开头循环写日志。binlog通过追加的方式记录,当文件大小大于给定值后,后续的日志会记录到新的文件上
    适用场景 redo log 适用于崩溃恢复(crash-safe) binlog 适用于主从复制和数据恢复

    binlog redo log 的区别可知: binlog 日志只用于归档,只依靠 binlog 是没有 `
    crash-safe 能力的。但只有 redo log 也不行,因为 redo log InnoDB `
    特有的,且日志上的记录落盘后会被覆盖掉。因此需要 binlog redo log
    二者同时记录,才能保证当数据库发生宕机重启时,数据不会丢失。

    undo log

    数据库事务四大特性中有一个是 原子性 ,具体来说就是 原子性是指对数据库的一系列操作,要么全部成功,要么全部失败,不可能出现部分成功的情况
    。实际上, 原子性 底层就是通过 undo log 实现的。 undo log 主要记录了数据的逻辑变化,比如一条 ` INSERT
    语句,对应一条 DELETE undo log ,对于每个 UPDATE 语句,对应一条相反的 UPDATE
    undo log ,这样在发生错误时,就能回滚到事务之前的数据状态。同时, undo log 也是 MVCC `
    (多版本并发控制)实现的关键,这部分内容在 [ 面试中的老大难-mysql事务和锁,一次性讲清楚!
    ](https://juejin.im/post/685512... 中有介绍,不再赘述。

    参考

    1. juejin.im/post/684490…
    2. www.cnblogs.com/f-ck-need-u…
    3. www.cnblogs.com/ivy-zheng/p…
    4. yq.aliyun.com/articles/59…
    5. www.jianshu.com/p/5af73b203…
    6. www.jianshu.com/p/20e10ed72…

    学习资料分享

    12 套 微服务、Spring Boot、Spring Cloud 核心技术资料,这是部分资料目录:

    • Spring Security 认证与授权
    • Spring Boot 项目实战(中小型互联网公司后台服务架构与运维架构)
    • Spring Boot 项目实战(企业权限管理项目))
    • Spring Cloud 微服务架构项目实战(分布式事务解决方案)
    • ...

      公众号后台回复arch028获取资料::

    查看原文

    赞 7 收藏 5 评论 2

    Gabriel 发布了文章 · 2月4日

    (2)快速排序-PHP版

    <?php
    function quickSort(&$array,$left,$right) {
        if ($left > $right) return;
        $middle = partition($array,$left,$right);
        quickSort($array,$left,$middle-1);
        quickSort($array,$middle+1,$right);
    }
    function partition(&$array,$left,$right) {
        $v = $array[$left];
        $j = $left;
        for ($i=$left;$i<=$right;$i++) {
            if($array[$i] < $v) {
            $temp = $array[$j+1];
            $array[$j+1] = $array[$i];
            $array[$i] = $temp;
            $j++;
        }
     } 
        $temp = $array[$j];
        $array[$j] = $array[$left];
        $array[$left] = $temp;
        return $j;
    }
    
    function makeArray($n) {
        $array = [];
        for ($i = 0;$i<$n;$i++) {
            $array[$i] = mt_rand(0,$n);
        }
        return $array;
    }
    
    $array = makeArray(100);
    quickSort($array,0,count($array)-1);
    echo implode(',',$array);
    查看原文

    赞 0 收藏 0 评论 0

    认证与成就

    • 获得 8 次点赞
    • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

    擅长技能
    编辑

    (゚∀゚ )
    暂时没有

    开源项目 & 著作
    编辑

    (゚∀゚ )
    暂时没有

    注册于 2017-08-21
    个人主页被 1.2k 人浏览