【Redis】——原理篇——网络模型(带你深入理解Redis的网络模型,手撕LRU,LFU算法!)

目录

一.用户空间和内核空间

二.阻塞IO

三.非阻塞IO

四.***IO多路复用***

1.介绍

2.Select

3.poll

4.***epoll***

1.介绍与对比

2.事件通知机制

3.Web服务流程

5.异步IO

6.总结

五.Redis网络模型

1.redis是单线程还是多线程

2.网络模型

1.单线程下的网络模型(6.0之前)

​编辑

2.多线程(6.0后哪些地方变成了多线程)

3.通信协议

1.RESP协议

2.模拟Redis客户端

六.内存回收

1.过期key回收

1.Redis怎么知道一个key过期了没有

2.是不是TTL到期了就立即删除呢

1.惰性删除

2.周期删除

3.内存淘汰策略

1.LRU(最久未使用的)

2.LFU(最不经常使用)

一.用户空间和内核空间

为什么划分出了用户态和内核态:

操作系统是应用程序和硬件打交道的桥梁,应用程序通过操作系统提供的用户接口程序间接的和硬件打交道。硬件就包括了像CPU,内存,IO设备等,硬件之上的内容称为软件,但是不可能让用户或者应用程序随意操作我们的硬件。在CPU的所有指令中,有些指令是非常危险的,如果错用会导致系统崩溃,从程序运行的安全性方便考虑,又将程序的运行状态划分成用户态和内核态,将两者隔离开。操作系统属于内核态,而操作系统提供的用户接口程序和应用程序都属于用户态。

用户态和内核态的区别:

内核态:在内核中运行的所有程序是可以访问所有的硬件资源的,可以在硬件上执行各种指令。权限较高。

用户态:只能执行一部分指令,对于影响系统稳定运行的程序是不允许执行的,当然IO指令也是不允许执行的,权限较低。

 用户态和内核态的切换:

当一个进程在执行的过程中,执行的业务较多的情况下,会执行一些普通的命令也可能会执行一些特权命令,这时也就涉及到了用户态到内核态的切换。以下面一个IO读写来举例子:IO读写涉及到磁盘。

读时:读的时候,如果还没有数据就得等待磁盘的数据过来,等数据来了读到内核空间的缓冲区中,再拷贝回我们的用户空间。

写出到磁盘:就要经过内核,但内核中没有我们的数据,所以就需要从用户空间拷贝到内核空间,然后写入到磁盘

从上面的图中可以看出,想要提高IO效率,主要是两个方面:一个是等待一个是拷贝

1.在读数据时,当没有数据过来的时候,会出现等待。

2.在读写数据的时候,需要从用户空间的缓冲区拷贝到内核空间的缓冲区,或者反过来,都是比较影响效率的。

所以后续的5种IO模式都是为了解决上述的两点问题来展开(减少等待,减少拷贝)的。接着往下看。

二.阻塞IO(BIO)

三.非阻塞IO

不断的询问,有没有数据。 有没有,有没有。

四.***IO多路复用***

1.介绍

2.Select

select模式存在的缺点

1.需要将整个fd_set从用户空间拷贝到内核空间,select结束还要再次拷贝回用户空间

2.无法得知具体是哪个fd就绪了,需要遍历整个集合

3.监听的fd数量不能超过1024

3.poll

4.***epoll***

1.介绍与对比

2.事件通知机制

3.Web服务流程

serversocket监听端口,如6379等待客户端的连接。

5.异步IO

在高并发的场景下,老板就不管员工的死活,疯狂派任务。IO读写是很慢的,容易崩溃。

6.总结

五.Redis网络模型

1.redis是单线程还是多线程

先问讨论的是什么部分,是核心业务处理部分还是整个redis。

2.网络模型

1.单线程下的网络模型(6.0之前)

 

先主要讲解一下client来了读事件怎么解决:

当客户端来了读事件,Redis会将它封装成一个client实例(包含了客户端的所有信息,包括请求),redis给客户端有里面输入和输出的缓冲区(后面要用),输入缓冲区中缓存了客户端请求参数,但我们读到缓冲区的是一个个的字节,所以还需要将它解析,比如说set name jack,会将它解析成一个个的SDS字符串,然后把它放一个argv数组中,然后通过拿到argv[0],也就是第一个位置的命令,找到可执行的函数,因为在redis内部封装了一个个command函数,维护了一个字典,k就是命令,v就是函数。找到这个函数后,就可以执行命令,也会有对应的响应,比如说ping就是pong,set就是ok。但是并没有将它直接写出,而是放到了client中的输出缓冲区中,然后将一个个的client实例放入了一个队列中,这个队列维护的就是一个个要写出的客户端socket。

这时beforeSleep就派上了用场,在epool_wait之前,beforeSleep会给每一个客户端绑定一个写处理器,就可以将一个个clientsocket写出了。

本质上就是IO的多路复用加时间派发。不断监听,监听serverSocket,监听clientSocket,然后派发下去,总共事件就3种,server读,client读,client写,分别派发给不同的处理器去处理。

2.多线程(6.0后哪些地方变成了多线程)

两个地方涉及到了多线程,一个是从客户端读取数据,解析数据时,另一个就是往客户端写数据。涉及到网络读写IO(涉及网络带宽,网络状态的影响)都很慢,就开启了多线程。

3.通信协议

1.RESP协议

以首字节来区分不同类型

六.内存回收

1.过期策略

1.Redis怎么知道一个key过期了没有

*expire中存的是设置了过期时间的节点

回答上面的问题:

根据key来查就可以了。

2.是不是TTL到期了就立即删除呢

1.惰性删除

存在的问题:如果一个key已经过期了,但我一直没有去访问它(没有进行CRUD的操作),那这个key不就永远不会被删除了嘛

2.周期删除

2.内存淘汰策略

不是过期了才删了,内存不够用了,主动去删。

1.LRU(最久未使用的)

手写LRU算法实现

思路:需要一张哈希表用来存放元素,一个双向链表来看哪些元素长时间未被使用(只要出现CRUD就将元素放到链表的头部)那么链表最后的元素就是最久未使用的元素了。

    static class LRUCache {


        //节点类
        static class Node {
            int key;
            int value;
            Node prev;
            Node next;

            public Node() {

            }

            public Node(int key, int value) {
                this.key = key;
                this.value = value;
            }

        }

        //双向链表
        static class DoublyLinkedList {
            Node head;
            Node tail;

            public DoublyLinkedList() {
                head = tail = new Node();
                head.next = tail;
                tail.prev = head;
            }

            //添加节点到头部
            public void addFirst(Node node) {
                Node oldNode = head.next;
                node.prev = head;
                node.next = oldNode;
                head.next = node;
                oldNode.prev = node;

            }

            //删除节点
            public void remove(Node node) {
                Node prev = node.prev;
                Node next = node.next;
                prev.next = next;
                next.prev = prev;

            }

            //删除链表尾节点
            public Node removeLast() {
                Node last = tail.prev;
                remove(last);
                return last;

            }
        }
        private final Map<Integer, Node> map = new HashMap<>();
        private final DoublyLinkedList list = new DoublyLinkedList();
        private int capacity;

        public LRUCache(int capacity){
            this.capacity = capacity;
        }

        //获取节点
        public int get(int key) {
            if (!map.containsKey(key)) {
                return -1;
            }
            Node node = map.get(key);
            list.addFirst(node);
            return node.value;
        }


        //新增节点,存在就更新,不存在就新增
        public void put(int key , int value){
            if(map.containsKey(key)) { // 更新
                Node node = map.get(key);
                node.value = value;
                list.remove(node);
                list.addFirst(node);
            } else { // 新增
                Node node = new Node(key, value);
                map.put(key, node);
                list.addFirst(node);
                if (map.size() > capacity) {
                    Node removed = list.removeLast();
                    map.remove(removed.key);
                }
            }

        }
    }

2.LFU(最不经常使用)

手写LFU算法实现

思路:需要两张哈希表,一张哈希表用来存放元素,另一张哈希表以执行的次数为key,存放数据,value以双向链表的方式来存(只要出现CRUD就将元素放到链表的头部)。在超过容量时,需要删除元素,找到该表中最小的存放元素的key中最后一个元素删除即可。

package com.itheima.leetcode;



import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

/**
 * <h3>设计 LFU 缓存</h3>
 */
public class LFUCacheLeetcode460 {

    static class LFUCache {
        static class Node {
            Node prev;
            Node next;
            int key;
            int value;
            int freq = 1; // 频度

            public Node() {
            }

            public Node(int key, int value) {
                this.key = key;
                this.value = value;
            }
        }

        static class DoublyLinkedList {
            Node head;
            Node tail;
            int size;

            public DoublyLinkedList() {
                head = tail = new Node();
                head.next = tail;
                tail.prev = head;
            }

            // 头部添加     head<->tail
            public void addFirst(Node newFirst) { // O(1)
                Node oldFirst = head.next;
                newFirst.prev = head;
                newFirst.next = oldFirst;
                head.next = newFirst;
                oldFirst.prev = newFirst;
                size++;
            }

            // 已知节点删除
            public void remove(Node node) { // O(1)
                Node prev = node.prev;
                Node next = node.next;
                prev.next = next;
                next.prev = prev;
                size--;
            }

            // 尾部删除
            public Node removeLast() { // O(1)
                Node last = tail.prev;
                remove(last);
                return last;
            }

            public boolean isEmpty() {
                return size == 0;
            }
        }

        private HashMap<Integer, Node> kvMap = new HashMap<>();
        private HashMap<Integer, DoublyLinkedList> freqMap = new HashMap<>();
        private int capacity;
        private int minFreq = 1; // 最小频度

        public LFUCache(int capacity) {
            this.capacity = capacity;
        }

        /*
            key 不存在
                返回 -1
            key 存在
                返回 value 值
                增加节点的使用频度,将其转移到频度+1的链表当中
         */
        public int get(int key) {
            if (!kvMap.containsKey(key)) {
                return -1;
            }
            Node node = kvMap.get(key);
            DoublyLinkedList list = freqMap.get(node.freq);
            list.remove(node);
            if (list.isEmpty() && node.freq == minFreq) {
                minFreq++;
            }
            node.freq++;
            freqMap.computeIfAbsent(node.freq, k -> new DoublyLinkedList())
                    .addFirst(node);
            return node.value;
        }

        /*
            更新
                将节点的 value 更新,增加节点的使用频度,将其转移到频度+1的链表当中
            新增
                检查是否超过容量,若超过,淘汰 minFreq 链表的最后节点
                创建新节点,放入 kvMap,并加入频度为 1 的双向链表
         */
        public void put(int key, int value) {
            if(kvMap.containsKey(key)) { // 更新
                Node node = kvMap.get(key);
                DoublyLinkedList list = freqMap.get(node.freq);
                list.remove(node);
                if (list.isEmpty() && node.freq == minFreq) {
                    minFreq++;
                }
                node.freq++;
                freqMap.computeIfAbsent(node.freq, k -> new DoublyLinkedList())
                        .addFirst(node);
                node.value = value;
            } else { // 新增
                if (kvMap.size() == capacity) {
                    Node node = freqMap.get(minFreq).removeLast();
                    kvMap.remove(node.key);
                }
                Node node = new Node(key, value);
                kvMap.put(key, node);
                freqMap.computeIfAbsent(1, k->new DoublyLinkedList())
                        .addFirst(node);
                minFreq = 1;
            }
        }



 }
    public static void main(String[] args) {
        LFUCache cache = new LFUCache(2);
        cache.put(1, 1);
        cache.put(2, 2);
        System.out.println(cache.get(1)); // 1 freq=2
        cache.put(3, 3);
        System.out.println(cache.get(2)); // -1
        System.out.println(cache.get(3)); // 3 freq=2
        cache.put(4, 4);
        System.out.println(cache.get(1)); // -1
        System.out.println(cache.get(3)); // 3  freq=3
        System.out.println(cache.get(4)); // 4  freq=2



    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值