美文网首页
Redis面试必看之持久化、主从同步、布隆过滤器、集群等详解

Redis面试必看之持久化、主从同步、布隆过滤器、集群等详解

作者: 江月照我眠 | 来源:发表于2022-01-25 17:26 被阅读0次

    一、数据类型

    redis支持以下五种数据类型:

    1. String类型

    二进制安全,可存储字符串、图片和视频等,支持incr自增操作(set/get/mset/mget/incr...)

    2. List类型

    双向链表,可以实现消息队列功能(rpush/lpop/llen...)

    3. Set类型

    无序集合,通过HashTable实现,可实现快速查找(去重)(sadd/srem/sismember...)

    4. Sorted Set类型

    有序集合,排好序的Set类型,可以用来实现优先级队列(zadd/zrem/zincrby...)

    5. Hash类型

    每个key都是一个HashTable,适合存储对象,如用户信息对象,id作key(hset/hget/hmset/hmget/hlen...)

    二、事务处理

    redis支持简单的事务操作,通过multi命令进入事务操作的上下文,接下来的redis命令将依次存入队列,识别到exec命令时,redis将按队列顺序执行队列中的所有命令,并将执行结果一并打包返回,然后终结事务上下文。

    三、持久化

    redis是基于内存的数据库,内存数据存在一个严重的弊端:突然宕机后者断电时,内存的数据不会保存。为了解决这个问题,redis提供了2种持久化的方式,分别为内存快照(Snapshotting)和日志追加(Append-only file),下面介绍一下。

    1. 内存快照(RDB)

    内存快照是将内存中的数据写入二进制文件中,默认文件名为dump.rdb。redis重启时读取rdb中的数据。
    redis内存快照分为自动和手动2种触发模式:

    1.1 手动触发

    客户端使用save或bgsave命令告诉redis需要做一次快照操作

    • save:阻塞redis服务器,拒绝所有redis命令,直到save完成。 PS: 尽量不要使用
    • bgsave:fork一个子进程(非线程),通过子进程去进行save操作,而主进程可以执行命令。

    需要注意以下问题:

    • 在执行bgsave时,为避免父子进程同时执行rdbSave而产生竞争,客户端发送的save命令将被拒绝。
    • 在执行bgsave时,如果发送了bgrewriteaof(aof重写)命令,该命令将延迟到bgsave完成之后执行,如果正在执行bgrewriteaof命令,bgsave将被拒绝。
    • 虽然bgsave是由子进程进行rdb文件的生成,但是在fork子进程的时候依然会造成父进程的阻塞,因此使用时要格外注意。
    1.2 自动触发

    因为basave命令可以不阻塞父进程保存数据,所以redis可以设置服务器配置的save选项,让服务器每隔一段时间自动执行一次bgsave命令。
    save 秒数 修改次数
    可以设置多个条件实现不同的快照方案,满足任何一个条件,redis都将进行一次内存快照:

    save 900 1
    save 300 10
    save 60  1000
    
    2. 日志追加(AOF)

    aof是把增加、修改的命令通过write函数追加到日志文件的尾部(默认名appendonly.aof)。redis重启时读取aof文件中的所有命令并且执行,从而把数据写入内存中。

    由于操作系统内核的I/O接口可能存在缓存,所以日志追加的方式可能不会立即写入文件中,这就有可能丢失部分数据。redis可以提供了以下三种方式配置来告诉redis何时去执行fsync函数强制系统将缓存写入磁盘:

    #appendfsync always  # 每次收到增加或修改命令就立刻强制写入磁盘
    appendfsync everysec # 每秒强制写入磁盘一次
    #appendfsync no      # 是否写入磁盘完全依赖系统
    

    日志追加的方式有效降低了数据丢失的风险,也带了另一个问题,那就是持久化文件不断膨胀。例如,执行incr nums命令100次,文件就会保存100条incr命令,其实99条都是多余的命令,因为恢复数据只需要执行set nums 100就可以了。

    redis提供了bgwriteaof,当redis收到此命令时,就使用类似于内存快照的方式将内存的数据以命令的方式保存到临时文件中,最后替换原来的日志文件。

    四、主从同步

    redis支持主从同步。主从同步可以防止主机(Master)挂掉导致网站不能正常运作,只需要把从机(Slave)设置为主机即可。主从同步有诸多优点:

    • Master可以有多个Slave。
    • 多个Slave可以连接到相同的Master,还可以连接到其他Slave形成图形结构。
    • 不会阻塞Master。当一个或多个Slave与Master初次进行数据同步时,Master可以继续处理客户端请求。相反,Slave在初次进行数据同步时不能处理请求(2.2版本以后不再阻塞)。
    • 在Master服务器禁止持久化,只在Slave服务器进行数据持久化。
    1. 主从同步原理

    主从同步设置好以后,Slave自动与Master建立连接,发送SYNC命令。无论是初次建立连接还是重连,Master都将启动一个后台进程,将内存数据以快照的方式写入文件中,同时Master主进程开始收集新的命令。Master后台进程完成快照操作以后,将数据文件发送给Slave,Slave将文件保存到磁盘上,然后把数据加载到内存中。接着Master把缓存的命令发给Slave,后续Master收到的写命令都通过开始建立的连接发送到Slave。当Master与Slave断开连接,Slave自动重新建立连接。当Master同时收到多个Slave发来的同步请求,只会启动一个进程写数据库镜像,然后发给所有的Slave。

    主从同步第一阶段(建立连接):

      1. Slave服务器主动连接到Master服务器。
      1. Slave服务器发送SYNC命令到Master服务器请求同步。
      1. Master服务器启动新的进程备份数据库到rdb文件。
      1. Master服务器把rdb文件传输给Slave服务器。
      1. Slave服务器清空数据库文件,把rdb文件导入数据库中。

    主从同步第二阶段(持续同步):

      1. Master服务器把所有用户的更改数据的操作,通过命令的形式转发给所有的Slave服务器。
      1. Slave服务器执行Master服务器发送的命令。

    Redis主从复制的配置,只需要在Slave服务器的配置文件中加入以下配置项:

    slaveof 192.168.1.1 6379 # 指定Master的ip和端口
    

    五、布隆过滤器

    布隆过滤器是一种比较巧妙的概率型数据结构,它可以告诉你某种东西可能存在或者一定不存在。当它告诉你某种东西存在时,这种东西可能存在;当它告诉你某种东西不存在时,那它一定不存在。

    1.1 实现原理

    布隆过滤器的数据结构就是一个很大的位数组和几个不同的的无偏哈希函数。向布隆过滤器添加元素时,会根据多个无偏哈希函数,算出一个整体的索引值,然后对位数组长度进行一个取模运算,每个无偏哈希函数都会得到一个不同的位置。再把这几个位数组对应的位置值置1,就完成了一个bf.add命令操作。

    向布隆过滤器查询元素是否存在时,和添加元素一样,也会哈希出几个位置来,看对应的位置是否都为1。只要有一个位0,那么就说明这个布隆过滤器不存在这个元素;如果都为1,并不能完全说明这个元素就一定存在,有可能这些位置为1是因为其他元素的存在,这就是布隆过滤器会存在误判的原因。

    1.2 基本用法

    布隆过滤器的基本用法:

    • bf.add:添加单个元素, 类似于集合的sadd。
    • bf.madd:添加多个元素。
    • bf.exists:判断某个元素是否存在,类似于集合的sismember。
    • bf.mexusts:判断多个元素是否存在。
    1.3 进阶用法

    创建一个自定义的布隆过滤器:bf.reserve key error_rate capacity

    1. key:键名。
    2. error_rate:期望错误率,错误率越低,需要的空间越大。
    3. capacity:初识容量,当初实际元素个数超过这个实际容量时,错误率将会上升。
    bf.reserve one-more-filter 0.0001 1000000
    

    六、Redis集群

    redis集群主要是redis的一个分布式实现,主要实现目标是:

    • 可扩展性:能够方便地扩展集群,可扩展到1000个节点。
    • 写入安全:集群会尽可能保存客户端写入的数据,但在故障转移时可能出现短时的数据丢失。
    • 可用性:集群的大多数节点都是可达的,并且对于不可达的主节点都至少有一个从节点数可达的情况下,集群仍可以继续提供服务。

    redis集群采用的是p2p的运行模式,完全去中心化。把所有的key分成了16384个solt(插槽),每个redis实例负责其中一部分solt。集群中的所有信息都通过节点之间定期的数据交换而更新。

    1. 集群搭建
    redis集群至少需要6个节点(3主3从)才能完成,下面演示的是单台机器上用多个端口进行模拟,生产环境中必须在多台机器上搭建集群才能保证其可用性,以免因机器宕机而造成集群不可用。

    bind 192.168.128.1                                        #绑定IP
    port 9000                                                 #指定端口
    dir /usr/local/etc/redis/9000                             #指定保存数据文件的路径
    cluster-enabled yes                                       #开启集群
    cluster-config-file /usr/local/etc/redis/9000/nodes.conf  #自动创建,保存集群运行情况
    cluster-node-timeout 15000                                #指定集群中节点的超时时间
    cluster-require-full-coverage no                          #指定集群如果没有完全汗覆盖16384个槽时集群停止服务,默认yes,一定要设置为no,否则集群可能因为某个节点宕机而停止服务。
    daemonize yes                                             #指定是否以守护进程模式运行
    logfile /usr/local/etc/redis/9000/redis.log               #指定保存日志的文件路径
    

    2. 集群管理
    redis提供了一个管理集群的脚本工具:redis-trib.rb,需要提前安装ruby和rubygems。

    3. 集群建立
    执行命令启动节点:

    /usr/local/redis/bin/redis-server /usr/local/etc/redis/9000/redis.conf
    /usr/local/redis/bin/redis-server /usr/local/etc/redis/9001/redis.conf
    /usr/local/redis/bin/redis-server /usr/local/etc/redis/9002/redis.conf
    /usr/local/redis/bin/redis-server /usr/local/etc/redis/9003/redis.conf
    /usr/local/redis/bin/redis-server /usr/local/etc/redis/9004/redis.conf
    /usr/local/redis/bin/redis-server /usr/local/etc/redis/9005/redis.conf
    

    执行完毕后可以用ps -aux | grep redis查看节点是否启动成功,启动成功后使用redis-trib.rb的create命令进行集群的创建:

    ./redis-trib.rb create --replicas 1 192.168.128.1:9000 192.168.128.1:9001 192.168.128.1:9002 192.168.128.1:9003 192.168.128.1:9004 192.168.128.1:9005
    

    其中“--replicas”选项的作用是指定每个主节点需要一个从节点。看到“[OK] All 16384 solts covered”就表示集群搭建成功了。

    4. 添加节点
    复制两份配置文件,然后根据对应的节点进行修改,修改完成后执行:

    /usr/local/redis/bin/redis-server /usr/local/etc/redis/9007/redis.conf
    /usr/local/redis/bin/redis-server /usr/local/etc/redis/9008/redis.conf
    

    启动完毕后,使用redis-trib.rb的add-node命令可以把节点加入到集群中,一个作主节点,一个作从节点。

    ./redis-trib.rb add-node 192.168.128.1:9007 192.168.128.1:9008
    

    上述命令以9007为主节点,9008为从节点添加到集群中,但要让9008节点从属于9007节点,我们必须添加9007节点的ID,在redis-cli中用cluster nodes命令可以查询集群状态,得到类似"dd62920df907f18a290e4f6ff8d7f94832ccf993"这样的ID,然后我们把上面的命令修改一下:

    ./redis-trib.rb add-node --slave --master -id dd62920df907f18a290e4f6ff8d7f94832ccf993 192.168.128.1:9007 192.168.128.1:9008
    

    5. 数据迁移
    上面的配置没有为添加的节点分配任何的solt,所以新增的数据不会储存在这个节点上。为了让新增的节点分担集群的储存压力,我们可以使用redis-trib.rb的reshard命令把集群的一部分solt迁移到新增的节点上:

    ./redis-trin.rb reshard 192.168.128.1:9000
    

    回车之后需要我们输入要迁移的solt个数和迁移的目标节点ID

    6. 故障转移
    在redis集群中,某个主节点宕机不会导致整个集群停止服务,这是靠故障转移来保证集群的高可用性。当某个主节点宕机,集群会从此节点的所有从节点选举出一个节点你作为新的主节点。

    六、代码演示(PHP)

    <?php
    /**
     * Redis 操作,支持 Master/Slave 的负载集群
     */
    class RedisCluster
    {
    
        // 是否使用 M/S 的读写集群方案
        private $_isUseCluster = false;
    
        // Slave 句柄标记
        private $_sn = 0;
    
        // 服务器连接句柄
        private $_linkHandle = array(
            'master' => null, // 只支持一台 Master
    
            'slave' => array(), // 可以有多台 Slave
        );
    
        /**
         * 构造函数
         *
         * @param boolean $isUseCluster 是否采用 M/S 方案
         */
        public function __construct($isUseCluster = false)
        {
            $this->_isUseCluster = $isUseCluster;
        }
    
        /**
         * 连接服务器,注意:这里使用长连接,提高效率,但不会自动关闭
         *
         * @param array $config Redis服务器配置
         * @param boolean $isMaster 当前添加的服务器是否为 Master 服务器
         * @return boolean
         */
        public function connect($config = array('host' => '127.0.0.1', 'port' => 6379), $isMaster = true)
        {
            // default port
            if (!isset($config['port'])) {
                $config['port'] = 6379;
            }
            // 设置 Master 连接
            if ($isMaster) {
                $this->_linkHandle['master'] = new \Redis();
                $ret = $this->_linkHandle['master']->pconnect($config['host'], $config['port']);
            } else {
                // 多个 Slave 连接
                $this->_linkHandle['slave'][$this->_sn] = new Redis();
                $ret = $this->_linkHandle['slave'][$this->_sn]->pconnect($config['host'], $config['port']);
                ++$this->_sn;
            }
            return $ret;
        }
    
        /**
         * 关闭连接
         *
         * @param int $flag 关闭选择 0:关闭 Master 1:关闭 Slave 2:关闭所有
         * @return boolean
         */
        public function close($flag = 2)
        { 
            switch ($flag) {
                // 关闭 Master
                case 0:
                    $this->getRedis()->close();
                    break;
                // 关闭 Slave
                case 1:
                    for ($i = 0; $i < $this->_sn; ++$i) {
                        $this->_linkHandle['slave'][$i]->close();
                    }
                    break;
                // 关闭所有
                case 1:
                    $this->getRedis()->close();
                    for ($i = 0; $i < $this->_sn; ++$i) {
                        $this->_linkHandle['slave'][$i]->close();
                    }
                    break;
            }
            return true;
        }
    
        /**
         * 得到 Redis 原始对象可以有更多的操作
         *
         * @param boolean $isMaster 返回服务器的类型 true:返回Master false:返回Slave
         * @param boolean $slaveOne 返回的Slave选择 true:负载均衡随机返回一个Slave选择 false:返回所有的Slave选择
         * @return redis object
         */
        public function getRedis($isMaster = true, $slaveOne = true)
        {
            // 只返回 Master
            if ($isMaster) {
                return $this->_linkHandle['master'];
            } else {
                return $slaveOne ? $this->_getSlaveRedis() : $this->_linkHandle['slave'];
            }
        }
    
        /**
         * 写缓存
         *
         * @param string $key 组存KEY
         * @param string $value 缓存值
         * @param int $expire 过期时间, 0:表示无过期时间
         */
        public function set($key, $value, $expire = 0)
        {
            // 永不超时
            if ($expire == 0) {
                $ret = $this->getRedis()->set($key, $value);
            } else {
                $ret = $this->getRedis()->setex($key, $expire, $value);
            }
            return $ret;
        }
    
        /**
         * 读缓存
         *
         * @param string $key 缓存KEY,支持一次取多个 $key = array('key1','key2')
         * @return string || boolean  失败返回 false, 成功返回字符串
         */
        public function get($key)
        {
            // 是否一次取多个值
            $func = is_array($key) ? 'mGet' : 'get';
            // 没有使用M/S
            if (!$this->_isUseCluster) {
                return $this->getRedis()->{$func}($key);
            }
            // 使用了 M/S
            return $this->_getSlaveRedis()->{$func}($key);
        }
    
        /*
        // magic function
        public function __call($name,$arguments){
        return call_user_func($name,$arguments);
        }
         */
        /**
         * 条件形式设置缓存,如果 key 不存时就设置,存在时设置失败
         *
         * @param string $key 缓存KEY
         * @param string $value 缓存值
         * @return boolean
         */
        public function setnx($key, $value)
        {
            return $this->getRedis()->setnx($key, $value);
        }
    
        /**
         * 删除缓存
         *
         * @param string || array $key 缓存KEY,支持单个健:"key1" 或多个健:array('key1','key2')
         * @return int 删除的健的数量
         */
        public function remove($key)
        {
            // $key => "key1" || array('key1','key2')
            return $this->getRedis()->delete($key);
        }
    
        /**
         * 值加加操作,类似 ++$i ,如果 key 不存在时自动设置为 0 后进行加加操作
         *
         * @param string $key 缓存KEY
         * @param int $default 操作时的默认值
         * @return int 操作后的值
         */
        public function incr($key, $default = 1)
        {
            if ($default == 1) {
                return $this->getRedis()->incr($key);
            } else {
                return $this->getRedis()->incrBy($key, $default);
            }
        }
    
        /**
         * 值减减操作,类似 --$i ,如果 key 不存在时自动设置为 0 后进行减减操作
         *
         * @param string $key 缓存KEY
         * @param int $default 操作时的默认值
         * @return int 操作后的值
         */
        public function decr($key, $default = 1)
        {
            if ($default == 1) {
                return $this->getRedis()->decr($key);
            } else {
                return $this->getRedis()->decrBy($key, $default);
            }
        }
    
        /**
         * 添空当前数据库
         *
         * @return boolean
         */
        public function clear()
        {
            return $this->getRedis()->flushDB();
        }
    
        /* =================== 以下私有方法 =================== */
    
        /**
         * 随机 HASH 得到 Redis Slave 服务器句柄
         *
         * @return redis object
         */
        private function _getSlaveRedis()
        {
            // 就一台 Slave 机直接返回
            if ($this->_sn <= 1) {
                return $this->_linkHandle['slave'][0];
            }
            // 随机 Hash 得到 Slave 的句柄
            $hash = $this->_hashId(mt_rand(), $this->_sn);
            return $this->_linkHandle['slave'][$hash];
        }
    
        /**
         * 根据ID得到 hash 后 0~m-1 之间的值
         *
         * @param string $id
         * @param int $m
         * @return int
         */
        private function _hashId($id, $m = 10)
        {
            //把字符串K转换为 0~m-1 之间的一个值作为对应记录的散列地址
            $k = md5($id);
            $l = strlen($k);
            $b = bin2hex($k);
            $h = 0;
            for ($i = 0; $i < $l; $i++) {
                //相加模式HASH
                $h += substr($b, $i * 2, 2);
            }
            $hash = ($h * 1) % $m;
            return $hash;
        }
    
        /**
         *    lpush
         */
        public function lpush($key, $value)
        {
            return $this->getRedis()->lpush($key, $value);
        }
    
        /**
         *    rpush
         */
        public function rpush($key, $value)
        {
            return $this->getRedis()->rpush($key, $value);
        }
    
        /**
         *    add lpop
         */
        public function lpop($key)
        {
            return $this->getRedis()->lpop($key);
        }
    
        /**
         *    add rpop
         */
        public function rpop($key)
        {
            return $this->getRedis()->rpop($key);
        }
    
        /**
         * lrange
         */
        public function lrange($key, $start, $end)
        {
            return $this->getRedis()->lrange($key, $start, $end);
        }
    
        /**
         * rrange
         */
        public function rrange($key, $start, $end)
        {
            return $this->getRedis()->rrange($key, $start, $end);
        }
    
        /**
         *    set hash opeation
         */
        public function hset($name, $key, $value)
        {
            if (is_array($value)) {
                return $this->getRedis()->hset($name, $key, serialize($value));
            }
            return $this->getRedis()->hset($name, $key, $value);
        }
        /**
         *    get hash opeation
         */
        public function hget($name, $key = null, $serialize = true)
        {
            if ($key) {
                $row = $this->getRedis()->hget($name, $key);
                if ($row && $serialize) {
                    unserialize($row);
                }
                return $row;
            }
            return $this->getRedis()->hgetAll($name);
        }
    
        /**
         * delete hash opeation
         */
        public function hdel($name, $key = null)
        {
            if ($key) {
                return $this->getRedis()->hdel($name, $key);
            }
            return $this->getRedis()->hdel($name);
        }
    
        /**
         * Transaction start
         */
        public function multi()
        {
            return $this->getRedis()->multi();
        }
    
        /**
         * Transaction send
         */
        public function exec()
        {
            return $this->getRedis()->exec();
        }
    
        /** 集合操作 **/
        
        /*
        * 将一个元素加入集合,已经存在集合中的元素则忽略。
        * 若集合不存在则先创建,若key不是集合类型则返回false,若元素已存在返回0,插入成功返回1。
        */
        public function sAdd($key, $value)
        {
            return $this->getRedis()->sAdd($key, $value);
        }
    
        /*
        * 返回集合中所有成员。
        */
        public function sMembers($key)
        {
            return $this->getRedis()->sMembers($key);
        }
    
        /*
        * 判断集合里是否存在指定元素,是返回true,否则返回false。
        */
        public function sismember($key, $value)
        {
            return $this->getRedis()->sismember($key, $value);
        }
    
        /*
        * 返回集合中元素的数量
        */
        public function scard($key)
        {
            return $this->getRedis()->scard($key);
        }
    
        /*
        * 随机删除并返回集合里的一个元素。
        */
        public function sPop($key)
        {
            return $this->getRedis()->sPop($key);
        }
    
        /*
        * 随机返回(n)个集合内的元素,由第二个参数决定返回多少个
        * 如果 n 大于集合内元素的个数则返回整个集合
        * 如果 n 是负数时随机返回 n 的绝对值,数组内的元素会重复出现
        */
        public function sRandMember($key, $n)
        {
            return $this->getRedis()->sRandMember($key, $n);
        }
    
        /*
        * 删除集合中指定的一个元素,元素不存在返回0。删除成功返回1,否则返回0。
        */
        public function srem($key, $value)
        {
            return $this->getRedis()->srem($key, $value);
        }
    
        /*
        * 模糊搜索相对的元素,
        * 参数:key,迭代器变量,匹配值,每次返回元素数量(默认为10个)
        */
        public function sscan($key, $it, $n = 10)
        {
            // return $this->getRedis()->sscan($key, $it, 's*', $n);
        }
    }
    
    

    相关文章

      网友评论

          本文标题:Redis面试必看之持久化、主从同步、布隆过滤器、集群等详解

          本文链接:https://www.haomeiwen.com/subject/tcmwhrtx.html