分布式缓存整理

分布式缓存

1 Redis和Memcached有什么区别?

  1. redis支持服务端的数据操作,Memcached需要将数据取回到客户端修改后再set回去

  2. redis拥有更丰富的数据结构与操作api

  3. 使用简单的key-value存储的话,Memcached的内存利用率更高,但是如果使用hash结构的话,Redis的内存利用率更高

  4. Redis是单线程模型,Memcached可以使用多线程模型,所以在存储小数据的时候Redis性能更高

  5. Memcached不支持原生的集群模式,但是Redis支持原生集群模式

 

redis的线程模型是什么?

单线程模型

1)文件事件处理器 File Event Handler

redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler。这个文件事件处理器,是单线程的,redis才叫做单线程的模型,采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器来处理这个事件。

 

如果被监听的socket准备好执行acceptreadwriteclose等操作的时候,跟操作对应的文件事件就会产生,这个时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。

 

文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了redis内部的线程模型的简单性。

 

文件事件处理器的结构包含4个部分多个socketIO多路复用程序文件事件分派器事件处理器(命令请求处理器、命令回复处理器、连接应答处理器,等等)。

事件处理器包含:

  • 连接应答 处理器

    处理socket的连接请求

  • 命令请求 处理器

    处理socket的读写请求

  • 命令回复 处理器

    将socket的读写请求后的结果返回

多个socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,但是会将socket放入一个队列中排队,每次从队列中取出一个socket给事件分派器,事件分派器把socket给对应的事件处理器。

 

然后一个socket的事件处理完之后,IO多路复用程序才会将队列中的下一个socket给事件分派器。文件事件分派器会根据每个socket当前产生的事件,来选择对应的事件处理器来处理。

 

2)文件事件

 

当socket变得可读时(比如客户端对redis写入指令,或者close操作),或者有新的可以应答的socket出现时(客户端对redis执行connect操作),socket就会产生一个AE_READABLE事件。

 

当socket变得可写的时候(客户端对redis执行read操作),socket会产生一个AE_WRITABLE事件。

 

IO多路复用程序可以同时监听AE_READABLEAE_WRITABLE两种事件要是一个socket同时产生了AE_READABLE和AE_WRITABLE两种事件,那么文件事件分派器优先处理AE_READABLE事件,然后才是AE_WRITABLE事件。

 

3)文件事件处理器

 

如果是客户端要连接redis,那么会为socket关联------------------连接应答处理器

如果是客户端要写数据到redis,那么会为socket关联------------------命令请求处理器

如果是客户端要从redis读数据,那么会为socket关联------------------命令回复处理器

 

4)客户端与redis通信的一次流程

 

在redis启动初始化的时候,redis会将连接应答处理器跟AE_READABLE事件关联起来,接着如果一个客户端跟redis发起连接,此时会产生一个AE_READABLE事件,然后由连接应答处理器来处理跟客户端建立连接,创建客户端对应的socket,同时将服务端创建的这个socket的AE_READABLE事件跟命令请求处理器关联起来。

 

当客户端向redis发起请求的时候(不管是读请求还是写请求,都一样),首先就会在socket产生一个AE_READABLE事件,然后由对应的命令请求处理器来处理。这个命令请求处理器就会从socket中读取请求相关数据,然后进行执行和处理。

接着redis这边准备好了给客户端的响应数据之后,就会将socket的AE_WRITABLE事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在socket上产生一个AE_WRITABLE事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入socket,供客户端来读取。

 

命令回复处理器写完之后,就会删除这个socket的AE_WRITABLE事件和命令回复处理器的关联关系。

 

为什么redis是单线程的但是还可以支撑高并发?

1)纯内存操作

2)核心是基于非阻塞的IO多路复用机制

3)单线程反而避免了多线程的频繁上下文切换问题

 

redis都有哪些数据类型?分别在哪些场景下使用比较合适?

由于网上有关于redis数据结构的详细操作,这里只做简单描述。

(1)string

这是最基本的类型了,就是普通的set和get,做简单的kv缓存

 

(2)hash

这个是类似map的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在redis里,然后每次读写缓存的时候,可以就操作hash里的某个字段。

 

(3)list

有序列表,这个数据结构可以适用到很多场景

 

比如微博,某个大v的粉丝,就可以以list的格式放在redis里去缓存

 

key=某大v

 

value=[zhangsan, lisi, wangwu]

 

比如可以通过list存储一些列表型的数据结构,类似粉丝列表了、文章的评论列表了之类的东西

 

比如可以通过lrange命令,就是从某个元素开始读取多少个元素,可以基于list实现分页查询,这个很棒的一个功能,基于redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走

 

比如可以搞个简单的消息队列,从list头怼进去,从list尾巴那里弄出来

 

(4)set

无序集合,自动去重

 

直接基于set将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于jvm内存里的HashSet进行去重,但是如果你的某个系统部署在多台机器上呢?

 

得基于redis进行全局的set去重

 

可以基于set做交集、并集、差集的操作,比如交集吧,可以把A,B两个人的粉丝列表整一个交集,看看俩人的共同好友

 

或者集合A-集合B,A的好友但是却不是B的好友,有可能是B认识的人

 

(5)sorted set

排序的set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序,最大的特点是有个分数可以自定义排序规则

 

比如说想根据时间对数据排序,那么可以写入进去的时候用某个时间作为分数

 

排行榜:将每个用户以及其对应的什么分数写入进去,zadd board score username,接着zrevrange board 0 99,就可以获取排名前100的用户;zrank board username,可以看到用户在排行榜里的排名

 

zadd board 85 zhangsan

zadd board 72 wangwu

zadd board 96 lisi

zadd board 62 zhaoliu

 

96 lisi

85 zhangsan

72 wangwu

62 zhaoliu

 

zrevrange board 0 3

 

获取排名前3的用户

 

96 lisi

85 zhangsan

72 wangwu

redis的过期策略都有哪些?

  • 定期删除

    redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。

    假设redis里放了10万个key,都设置了过期时间,你每隔几百毫秒,就检查10万个key,那redis基本上就死了,cpu负载会很高的,消耗在你的检查过期key上了。

    注意这里可不是每隔100ms就遍历所有的设置过期时间的key,那样就是一场性能上的灾难。实际上redis是每隔100ms随机抽取一些key来检查和删除的。

  • 惰性删除

    惰性删除了就是说在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。

    一般是配合定期删除惰性删除一起使用

内存淘汰机制都有哪些?

  1. no-eviction:当内存不足以容纳新写入数据时,新写入操作会报错

  2. allkeys-lru:当内存不足以容纳新写入数据时,在所有键空间中,移除最近最少使用的key这个是最常用的

  3. allkeys-random:当内存不足以容纳新写入数据时,在所有键空间中,随机移除某个key

  4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)

  5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key

  6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除

 

LRU代码实现

API实现

import java.util.LinkedHashMap;
import java.util.Map;
​
public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {
    //定义缓存的容量
    private int cacheSize;
​
    //带参数的构造器
    LRULinkedHashMap(int cacheSize) {
        //如果accessOrder为true的话,则会把访问过的元素放在链表后面,放置顺序是访问的顺序
        //如果accessOrder为flase的话,则按插入顺序来遍历
        super((int) Math.ceil(cacheSize / 0.75f) + 1, 0.75f, true);
        //传入指定的缓存最大容量
        this.cacheSize = cacheSize;
    }
​
    //实现LRU的关键方法,如果map里面的元素个数大于了缓存最大容量,则删除链表的顶端元素
    @Override
    public boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > cacheSize;
    }
​
    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        for (Map.Entry<K, V> entry : this.entrySet()) {
            stringBuilder.append(String.format("[ %s : %s ]\n", entry.getKey(), entry.getValue()));
        }
        return stringBuilder.toString();
    }
​
    //test
    public static void main(String[] args) {
        LRULinkedHashMap<String, Integer> testCache = new LRULinkedHashMap<>(3);
        testCache.put("A", 1);
        testCache.put("B", 2);
        testCache.put("C", 3);
//        System.out.println(testCache.get("B"));
//        System.out.println(testCache.get("A"));
        testCache.put("D", 4);
        testCache.put("E", 5);
        System.out.println(testCache);
    }
}

 

原生实现

import java.util.HashMap;
​
/**
 * 使用cache和链表实现缓存
 */
public class LRU2<K, V> {
    /**
     * 最大缓存大小
     */
    private final int MAX_CACHE_SIZE;
    /**
     * 头结点
     */
    private Entry<K, V> head;
    /**
     * 尾节点
     */
    private Entry<K, V> tail;
​
    /**
     * 缓存map
     */
    private HashMap<K, Entry<K, V>> cacheMap;
​
    public LRU2(int cacheSize) {
        MAX_CACHE_SIZE = cacheSize;
        cacheMap = new HashMap<>();
    }
​
    public void put(K key, V value) {
        Entry<K, V> entry = getEntry(key);
        //如果是新添加的元素
        if (entry == null) {
            //如果缓存大小>=最大指定大小
            if (cacheMap.size() >= MAX_CACHE_SIZE) {
                //从缓存中删除尾节点的元素
                cacheMap.remove(tail.key);
                //去掉尾节点
                removeTail();
            }
            //创建新节点
            entry = new Entry<>();
            entry.key = key;
            entry.value = value;
            //将每次新增的节点都放到头结点
            moveToHead(entry);
            //并且添加到缓存中
            cacheMap.put(key, entry);
        } else {
            //如果是缓存中已经存在的元素 则直接赋值
            entry.value = value;
            //将每次新增的节点都放到头结点
            moveToHead(entry);
        }
    }
​
    /**
     * 根据键获取值
     *
     * @param key
     * @return
     */
    public V get(K key) {
        Entry<K, V> entry = getEntry(key);
        if (entry == null) {
            return null;
        }
        //将每次操作过的节点放到头结点位置
        moveToHead(entry);
        return entry.value;
    }
​
    /**
     * 移除当前Key
     *
     * @param key
     */
    public void remove(K key) {
        //获取当前Key的Value
        Entry<K, V> entry = getEntry(key);
        //如果value存在
        if (entry != null) {
            //如果value为头结点
            if (entry == head) {
                Entry<K, V> next = head.next;
                //让当前头结点的下一个节点为空
                head.next = null;
                //让之前头结点的下一个节点为新的头结点
                head = next;
                //让当前头结点的前一个节点为空
                head.pre = null;
            } else if (entry == tail) {//如果当前需要删除的节点为尾节点
                Entry<K, V> prev = tail.pre;
                //让当前尾节点的前一个节点为空
                tail.pre = null;
                //让旧的尾节点的前一个节点为新的尾节点
                tail = prev;
                //让当前尾节点的下一个节点为空
                tail.next = null;
            } else {//如果当前需要删除的节点为中间节点
                //直接让当前删除的节点的上一个节点的next指向当前删除的节点的下一个节点
                entry.pre.next = entry.next;
                //直接让当前删除的节点的下一个节点的pre指向当前删除的节点的上一个节点
                entry.next.pre = entry.pre;
            }
            //从缓存map中删除该键
            cacheMap.remove(key);
        }
    }
​
    /**
     * 移除尾节点
     */
    private void removeTail() {
        //如果尾节点不为空
        if (tail != null) {
            //让尾节点的前一个节点=prev
            Entry<K, V> prev = tail.pre;
            //如果尾节点的前一个节点为空 说明整个链表就一个节点
            if (prev == null) {
                //让头尾节点都为空
                head = null;
                tail = null;
            } else {
                //如果尾节点的前一个节点不为空
                //让尾节点与前一个节点断开
                tail.pre = null;
                //让prev为当前链表的尾节点
                tail = prev;
                //让prev的下一个节点为空(之前引用着已经删除了的尾节点)
                tail.next = null;
            }
        }
    }
​
    /**
     * 将参数节点放到头结点
     * 此时置于尾端的节点就是访问次数最少的节点 应该被淘汰掉
     * 符合LRU算法思想
     *
     * @param entry 参数节点
     */
    private void moveToHead(Entry<K, V> entry) {
        //如果当前节点已经为头结点 直接结束
        if (entry == head) {
            return;
        }
        //如果当前头尾节点都为空 说明整个链表为空
        if (head == null || tail == null) {
            //即让头尾节点都为当前节点entry
            head = tail = entry;
            return;
        }
​
        //如果当前节点的前一个节点不为空
        if (entry.pre != null) {
            //让当前节点的前一个节点的next指向当前节点的下一个节点
            // 比如 A->-B->C 现在将B移到头结点 那么B与A C断开 即为 A->C
            entry.pre.next = entry.next;
        }
        //如果当前节点的下一个节点不为空
        if (entry.next != null) {
            //让当前节点的下一个节点的pre指向当前节点的前一个节点
            entry.next.pre = entry.pre;
        }
        //如果当前节点即为尾节点
        if (entry == tail) {
            //让prev为尾节点的前一个节点
            Entry<K, V> prev = entry.pre;
            //如果尾节点的前一个节点不为空
            if (prev != null) {
                //让尾节点的前一个节点为空
                tail.pre = null;
                //让prev为当前尾节点
                tail = prev;
                //让prev的下一个节点为空
                tail.next = null;
            }
        }
​
        entry.next = head;
        head.pre = entry;
        entry.pre = null;
        head = entry;
    }
​
    private Entry<K, V> getEntry(K key) {
        return cacheMap.get(key);
    }
​
    private static class Entry<K, V> {
        Entry<K, V> pre;
        Entry<K, V> next;
        K key;
        V value;
    }
​
    @Override
    public String toString() {
        StringBuilder stringBuilder = new StringBuilder();
        Entry<K, V> entry = head;
        stringBuilder.append("head >> ");
        while (entry != null) {
            stringBuilder.append(String.format("%s:%s ", entry.key, entry.value));
            entry = entry.next;
        }
        stringBuilder.append(" >> tail");
        return stringBuilder.toString();
    }
​
    public static void main(String[] args) {
        LRU2<Integer, Integer> lru2 = new LRU2<>(5);
        lru2.put(1, 1);
        System.out.println(lru2);
        lru2.put(2, 2);
        System.out.println(lru2);
        lru2.put(3, 3);
        System.out.println(lru2);
        lru2.get(1);
        System.out.println(lru2);
        lru2.put(4, 4);
        lru2.put(5, 5);
        lru2.put(6, 6);
        System.out.println(lru2);
    }
}

 

如何保证Redis的高并发(分散流量)和高可用(一主多从+哨兵)?

redis高并发:主从架构,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万QPS,多从用来查询数据,多个从实例可以提供每秒10万的QPS

 

redis高并发的同时,还需要容纳大量的数据:一主多从,每个实例都容纳了完整的数据,比如redis主就10G的内存量,其实你就最对只能容纳10g的数据量。如果你的缓存要容纳的数据量很大,达到了几十g,甚至几百g,或者是几t,那你就需要redis集群,而且用redis集群之后,可以提供可能每秒几十万的读写并发。

 

redis高可用:如果你做主从架构部署,其实就是加上哨兵就可以了,就可以实现,任何一个实例宕机,自动会进行主备切换。

redis的主从复制原理

  1. redis replication的核心机制

    (1)redis采用异步方式复制数据到slave节点,不过redis 2.8开始,slave node会周期性地确认自己每次复制的数据量 (2)一个master node是可以配置多个slave node (3)slave node也可以连接其他的slave node (4)slave node在做复制的时候,是不会block master node的正常工作的 (5)slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了 (6)slave node主要用来进行横向扩容,做读写分离扩容的slave node可以提高读的吞吐量(将大量的请求分散到不同的slave节点上 提高了redis主从架构的吞吐量 单机redis的吞吐量没变)

     

  2. master持久化对于主从架构的安全保障的意义

    如果采用了主从架构,那么建议必须开启master node的持久化!(AOF/RDB)

    不建议用slave node作为master node的数据热备,因为那样的话,如果你关掉master的持久化,可能在master宕机重启的时候数据是空的,然后可能一经过复制,salve node数据也丢了

    master -> RDB和AOF都关闭了 -> 全部在内存中

    master宕机,重启,是没有本地数据可以恢复的,然后就会直接认为自己IDE数据是空的

    master就会将空的数据集同步到slave上去,所有slave的数据全部清空

    100%的数据丢失

    master节点,必须要使用持久化机制

    第二个,master的各种备份方案,要不要做,万一说本地的所有文件丢失了; 从备份中挑选一份rdb去恢复master; 这样才能确保master启动的时候,是有数据的

    即使采用了后续讲解的高可用机制,slave node可以自动接管master node,但是也可能sentinal还没有检测到master failure,master node就自动重启了,还是可能导致上面的所有slave node数据清空故障

  3. 主从架构的核心原理

    启动一个slave node的时候,它会发送一个PSYNC命令给master node

    如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据; 否则如果是slave node第一次连接master node,那么会触发一次full resynchronization

    开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。

    slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。

  4. 主从复制的断点续传

    redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份

    master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制

    但是如果没有找到对应的offset,那么就会执行一次resynchronization

  5. 无磁盘化复制

    master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了

    repl-diskless-sync repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来

  6. 过期key处理

    slave不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。

  7. 什么是99.99%高可用?

    架构上,高可用性,99.99%的高可用性

    讲的学术,99.99%,公式,系统可用的时间 / 系统故障的时间,365天,在365天 * 99.99%的时间内,你的系统都是可以对外提供服务的,那就是高可用性

     

redis的哨兵原理

  1. 哨兵的介绍

    sentinal,中文名是哨兵

    哨兵是redis集群架构中非常重要的一个组件,主要功能如下

    (1)集群监控,负责监控redis master和slave进程是否正常工作 (2)消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员 (3)故障转移,如果master node挂掉了,会自动转移到slave node上 (4)配置中心,如果故障转移发生了,通知client客户端新的master地址

    哨兵本身也是分布式的,作为一个哨兵集群去运行,互相协同工作

    (1)故障转移时,判断一个master node是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题 (2)即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了

    目前采用的是sentinal 2版本,sentinal 2相对于sentinal 1来说,重写了很多代码,主要是让故障转移的机制和算法变得更加健壮和简单

  2. 哨兵的核心知识

    (1)哨兵至少需要3个实例,来保证自己的健壮性 (2)哨兵 + redis主从的部署架构,是不会保证数据零丢失的,只能保证redis集群的高可用性 (3)对于哨兵 + redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练

  3. 为什么redis哨兵集群只有2个节点无法正常工作?

    哨兵集群必须部署2个以上节点

    如果哨兵集群仅仅部署了个2个哨兵实例

    Configuration: quorum = 1

    master1宕机,slave1和slave2中只要有1个哨兵认为master1宕机就可以还行切换,同时slave1和slave2中会选举出一个哨兵来执行故障转移

    同时这个时候,需要majority,也就是大多数哨兵都是运行的,2个哨兵的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),2个哨兵都运行着,就可以允许执行故障转移

    但是如果整个master1和slave1运行的机器宕机了,那么哨兵只有1个了,此时就没有majority来允许执行故障转移,虽然另外一台机器还有一个R1,但是故障转移不会执行

  4. 经典的3节点哨兵集群

    Configuration: quorum = 2

    如果master1所在机器宕机了,那么三个哨兵还剩下2个,slave2和slave3可以一致认为maste1r宕机,然后选举出一个来执行故障转移

    同时3个哨兵的majority是2,所以还剩下的2个哨兵运行着,就可以允许执行故障转移

Redis持久化机制

我们已经知道对于一个企业级的redis架构来说,持久化是不可减少的

企业级redis集群架构:海量数据、高并发、高可用

持久化主要是做灾难恢复,数据恢复,也可以归类到高可用的一个环节里面去

比如你redis整个挂了,然后redis就不可用了,你要做的事情是让redis变得可用,尽快变得可用

重启redis,尽快让它对外提供服务,但是就像上一讲说,如果你没做数据备份,这个时候redis启动了,也不可用啊,数据都没了

很可能说,大量的请求过来,缓存全部无法命中,在redis里根本找不到数据,这个时候就死定了,缓存雪崩问题,所有请求,没有在redis命中,就会去mysql数据库这种数据源头中去找,一下子mysql承接高并发,然后就挂了

mysql挂掉,你都没法去找数据恢复到redis里面去,redis的数据从哪儿来?从mysql来。。。

具体的完整的缓存雪崩的场景,还有企业级的解决方案,到后面讲

如果你把redis的持久化做好,备份和恢复方案做到企业级的程度,那么即使你的redis故障了,也可以通过备份数据,快速恢复,一旦恢复立即对外提供服务

redis的持久化,跟高可用,是有关系的,企业级redis架构中去讲解

redis持久化:RDBAOF


1、RDB和AOF两种持久化机制的介绍(RDB周期性持久数据/AOF持续记录写命令)

RDB持久化机制,对redis中的数据执行周期性的持久化

AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集

如果我们想要redis仅仅作为纯内存的缓存来用,那么可以禁止RDB和AOF所有的持久化机制

通过RDB或AOF,都可以将redis内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去,比如说阿里云,云服务

如果redis挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动redis,redis就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务

如果同时使用RDB和AOF两种持久化机制,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整


2、RDB持久化机制的优点(适合冷备/恢复数据更快)

(1)RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说Amazon的S3云服务上去,在国内可以是阿里云的ODPS分布式存储上,以预定好的备份策略来定期备份redis中的数据

(2)RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可

(3)相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速


3、RDB持久化机制的缺点(宕机可能丢失间隔时间内的产生的数据/生成RDB文件过大的时候会影响服务正常提供)

(1)如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据

(2)RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒


4、AOF持久化机制的优点(数据不易丢失/写入AOF文件快速/AOF文件智能压缩重写/灵活控制AOF文件)

(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据

(2)AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复

(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指导进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。

(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据


5、AOF持久化机制的缺点(AOF文件较大/写命令性能降低)

(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大

(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的

(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。


6、RDB和AOF到底该如何选择(配合使用)

(1)不要仅仅使用RDB,因为那样会导致你丢失很多数据

(2)也不要仅仅使用AOF,因为那样有两个问题,第一,你通过AOF做冷备,没有RDB做冷备,来的恢复速度更快; 第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug

(3)综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复

redis集群模式的工作原理

redis cluster

支撑N个redis master node,每个master node都可以挂载多个slave node

读写分离的架构,对于每个master来说,写就写到master,然后读就从mater对应的slave去读

高可用,因为每个master都有salve节点,那么如果mater挂掉,redis cluster这套机制,就会自动将某个slave切换成master

redis cluster(多master + 读写分离 + 高可用)

仅高可用=单master+多slave+哨兵, master负责写 ,slave负责读 ,master同步数据到slave

高可用+高并发=(n乘master + nk乘slave)redis集群+哨兵

多个master之间使用一致性hash或者redis slots来分散key的存储节点 , 然后各自同步到其对应的slave节点上,达到横向扩容

n>1 k>1

n为master的个数

k为一个master对应的k个slave

我们只要基于redis cluster去搭建redis集群即可,不需要手工去搭建replication复制+主从架构+读写分离+哨兵集群+高可用

 

如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个G,单机足够了

replication,一个mater,多个slave,要几个slave跟你的要求的读吞吐量有关系,然后自己搭建一个sentinal集群,去保证redis主从架构的高可用性,就可以了

redis cluster,主要是针对海量数据+高并发+高可用的场景,海量数据,如果你的数据量很大,那么建议就用redis cluster

在集群模式下,redis的key是如何寻址的?

  1. hash取模寻址

  2. 一致性hash算法(自动缓存迁移)+虚拟节点(自动负载均衡)

  3. redis cluster的hash slot算法

    redis cluster有固定的16384个hash slot,对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot

    redis cluster中每个master都会持有部分slot,比如有3个master,那么可能每个master持有5000多个hash slot

    hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去

    移动hash slot的成本是非常低的

    客户端的api,可以对指定的数据,让他们走同一个hash slot,通过hash tag来实现

了解什么是redis的雪崩和穿透?

  1. redis雪崩

    某一瞬间大量的键集体过期或由于机器宕机后重启导致大量内存中的数据丢失 导致某一时刻的大量请求直接打到数据库层

    解决方法:可以使用本地ehcache缓存作为二级缓存+hystrix做限流降级处理 当Redis缓存中不存在就去本地缓存中查询一下,这一步骤可以分担一部分压力 如果此时压力还是很大 就启用hystrix限流 让众多请求中只有一部分的请求继续请求,其余的请求走降级之后的方法,比如返回友好的提示信息告诉用户服务端繁忙请稍后再试.

  2. redis穿透(恶意攻击)

    非法用户大量请求缓存DB不存在的数据 比如根据根据订单号查询订单的时候 查询条件全是负数 这样一来 大量的请求就会直接穿过Redis直接打到数据库上 造成数据库压力激增

    解决方法:将不存在的键存入redis 值设为null或者某个标记符号 并设置过期时间 这样一来 在过期时间内再次请求就会直接返回

  3. redis击穿

    某一时刻大量请求某一个键(比如电商网站中的爆款),假如该键即将过期,那么在过期后的一瞬间,大量请求就会直接打到数据库

    解决方法:过期后通过对该键进行加锁 在加锁->从数据库中查询->将结果放入缓存中->返回结果->解锁这一过程中,其余的请求等待并且间隔重试 等待时间不会很久 一旦加锁解锁这一过程结束 程序又恢复到该键未过期时的状态 还有一种解决方法就是将该键设为永不过期

redis崩溃之后会怎么样?

redis崩溃之后大量请求直接打到数据库

数据库承载不了那么大的压力就会崩溃

导致整个系统全盘崩溃

系统该如何应对这种情况?

事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃

事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL被打死

事后:redis持久化,快速恢复缓存数据

 

缓存与数据库的双写一致性

Cache Aside Pattern

  • 查询先从缓存中查 如果查不到再去数据库里查 将结果加到缓存中 然后返回结果

  • 更新先删除缓存 然后更新数据库

 

redis的并发竞争问题

多个系统对同一个键进行操作,如果没有操作顺序的话会导致最终该键的值不一样

如何解决这个问题?

使用分布式锁

Redis事务的CAS方案

MULTI:     开启一个事务,类比于mysql的openSession
EXEC:      提交一个事务,类比于mysql的commit
DISCARD:    取消一个事务,类比于mysql的rollback
WATCH:    监控一个key,与redis事务机制结合使用,形成原子锁

watch指令在redis事物中提供了CAS的行为。为了检测被watch的keys在是否有多个clients同时改变引起冲突,这些keys将会被监控。如果至少有一个被监控的key在执行exec命令前被修改,整个事物将会回滚,不执行任何动作,从而保证原子性操作,并且执行exec会得到null的回复。

生产环境中的redis是怎么部署的?

redis cluster,10台机器,5台机器部署了redis主实例,另外5台机器部署了redis的从实例,每个主实例挂了一个从实例,5个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒5万,5台机器最多是25万读写请求/s。

 

机器是什么配置?32G内存+8核CPU+1T磁盘,但是分配给redis进程的是10g内存,一般线上生产环境,redis的内存尽量不要超过10g,超过10g可能会有问题。

 

5台机器对外提供读写,一共有50g内存。

 

因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis从实例会自动变成主实例继续提供读写服务

 

你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是10kb。100条数据是1mb,10万条数据是1g。常驻内存的是200万条商品数据,占用内存是20g,仅仅不到总内存的50%。

 

目前高峰期每秒就是3500左右的请求量

 

posted @ 2019-07-12 12:25  Tom爱Jerry  阅读(2352)  评论(0编辑  收藏  举报