Redis网络I/O模型

目录

声明:这里的I/O表示redis从网卡读来自客户端的数据(包括指令、数据)

一、阻塞I/O

阻塞I/O读取数据过程:

  1. 应用程序(服务器)接收到客户端的三次握手,通过accept()建立连接,得到一个socket文件描述符,用于与客户端通信。
  2. 应用程序进程 调用read()函数,触发系统调用 ,进程从用户态切换到内核态 ,自动执行内核中的系统调用处理函数 sys_read()。
  3. 内核态的该进程检查客户端的数据是否已经在内核空间的缓存中(第一次进入内核态肯定不在),如果数据在缓存中,直接拷贝到用户空间,然后切换回用户态。
  4. 如果未命中说明客户端发送的数据尚未到达 ,进程进入阻塞队列让出CPU,持续等待客户端数据。
  5. 客户端发起HTTP请求,并携带数据。
  6. 客户端发送的网络数据包到达服务器网卡,网卡通过DMA将数据包写入内核缓冲区,然后触发硬件中断,抢占CPU,执行中断处理程序
  7. 中断处理程序 将数据进行网络协议栈的处理(如IP、TCP处理),最终将数据放入对应socket的接收缓冲区,CPU会返回到被中断的地方继续执行,唤醒 等待在该socket上的进程,进入就绪态
  8. 当该进程被调度器选中再次运行时,它从之前阻塞的地方继续执行,此时数据已经在内核缓存中,于是将数据从内核缓存拷贝到socket的用户空间
  9. 系统调用返回,进程从内核态切换回用户态,并继续执行用户态代码。
  10. 处理完成后,进程重新进入内核态阻塞。
  • 对于多线程阻塞I/O模型,服务器(应用程序)与每个客户端建立连接后都会创建一个专属线程进入内核态阻塞,持续监听该客户端的请求。一对一
  • 而对于单线程I/O模型,服务器只有一个线程用来监听所有客户端,此时线程阻塞时只能处理刚才通过accept()建立连接的一个客户端的数据,其他客户端的数据包括新的连接请求都无法处理。一对多

但是阻塞I/O的缺点是等待客户端数据、中断处理程序写数据到内核缓冲区、将数据写回用户空间过程中服务器进程会一直阻塞等待。

二、非阻塞I/O

非阻塞I/O相较于阻塞I/O优势在于等待客户端数据、中断处理程序写数据到内核缓冲区 过程不需要阻塞服务器进程,只有将数据写回用户空间过程中服务器进程会阻塞。

不过单纯使用非阻塞I/O效率并不会有提升,虽然"等待客户端数据、中断处理程序写数据到内核缓冲区"过程不需要阻塞服务器进程,但是该进程仍会while轮询 判断数据是否已经在内核空间的缓存中,过程中该进程无法执行其他任务且一直占有CPU,反而会增加CPU占用。

非阻塞I/O读取过程:和阻塞I/O的主要区别在步骤4和8

  1. 应用程序(服务器)接收到客户端的三次握手,通过accept()建立连接,得到一个socket文件描述符,用于与客户端通信。
  2. 应用程序进程 调用read()函数,触发系统调用 ,进程从用户态切换到内核态 ,自动执行内核中的系统调用处理函数 sys_read()。
  3. 内核态的该进程检查客户端的数据是否已经在内核空间的缓存中,如果数据在缓存中,直接拷贝到用户空间,然后切换回用户态。
  4. 如果未命中说明客户端发送的数据尚未到达 ,进程返回用户态并轮询1~4步,此时进程不会阻塞,也不会让出CPU
  5. 客户端发起HTTP请求,并携带数据。
  6. 客户端发送的网络数据包到达服务器网卡,网卡通过DMA将数据包写入内核缓冲区,然后触发硬件中断,抢占CPU,执行中断处理程序
  7. 中断处理程序 将数据进行网络协议栈的处理(如IP、TCP处理),最终将数据放入对应socket的接收缓冲区,CPU会返回到被中断的地方继续执行,唤醒 等待在该socket上的进程,进入就绪态
  8. 进程轮询调用read()时 ,进程从用户态切换到内核态 ,此时数据已经在内核缓存中,于是将数据从内核缓存拷贝到socket的用户空间
  9. 系统调用返回,进程从内核态切换回用户态,并继续执行用户态代码。
  10. 处理完成后,进程重新进入内核态阻塞。

三、I/O多路复用*

我们现在从单线程服务器 的角度来考虑阻塞I/O模型和非阻塞I/O模型的问题:服务器单线程会监听多个客户端的请求,但同一时刻只能监听一个客户端的数据,其他客户端的数据和新的连接请求无法被处理,当正在监听的客户端A迟迟没有数据到来时,单线程会一直阻塞,其他客户端可能已经通过HTTP发送了数据但是由于单线程一直处于对客户端A的阻塞监听状态,所以无法处理其他客户端的请求。

I/O多路复用是利用单个线程同时监听多个文件描述符 (每个文件描述符对应一个socket[也就是客户端]),也就是说单线程可以 同时 监听多个socket的网络I/O请求。与阻塞I/O不同的是,阻塞I/O使用的是recvfrom系统调用函数 ,该函数的功能是读取数据,如果没有数据那么就会一直阻塞直到读到数据,一次只能监听一个socket 。而I/O多路复用使用的是select、poll、epoll系统调用函数 ,该函数可以同时监听多个socket[监听socket、客户端socket],当某一个socket的数据到达时,通过硬件中断来执行中断处理程序读取网络I/O数据,然后唤醒等待的进程调用recvfrom来读取已经到达的数据到对应socket的用户空间,解决了阻塞I/O模型和非阻塞I/O模型的问题。

Redis采用I/O多路复用技术,通过一个主线程 同时监听所有请求的文件描述符 。当任意请求有数据到达时,操作系统内核通过多路复用机制通知Redis主线程。主线程随即读取该客户端的数据,在内存中执行对应的命令,然后将结果写入该客户端的输出缓冲区。整个过程中,单个线程以事件驱动的方式高效管理成千上万个并发连接,避免了多线程的上下文切换和同步开销,实现了高性能和高并发

1.select系统调用

select为读请求、写请求、异常事件 创建三个独立的数组,每个数组有32个元素,每个元素占32bit,使用bit位作为标记,因此每个数组可监听1024 个请求,其中请求来自不同的线程。通过timeout设定select阻塞等待的超时时间

单线程下select I/O多路复用读取网络数据过程:

  1. 单线程(服务器)创建监听socket ,用来监听新客户端的连接请求,将监听socket的fd记录到read数组 中的某个bit位上。
  2. 单线程 调用select()函数,触发系统调用 ,进程从用户态切换到内核态 ,并将三个数组拷贝到内核态
  3. 内核态的该线程 检查缓存中是否有数据,如果有,判断数据是哪个fd 的,并判断是读请求、写请求还是出现异常 ,将fd对应数组位置的bit置0 ,切换回用户态,并将修改后的数组拷贝到用户态
  4. 如果未命中说明没有客户端发送数据 ,线程进入阻塞队列让出CPU,持续监听客户端数据。
  5. 如果阻塞时间超过了timeout,那么唤醒该线程,回到用户态。
  6. 客户端A或B发起HTTP请求,并携带数据。
  7. 客户端A或B发送的网络数据包到达服务器网卡,网卡通过DMA将数据包写入内核缓冲区,然后触发硬件中断,抢占CPU,执行中断处理程序
  8. 中断处理程序 将数据进行网络协议栈的处理(如IP、TCP处理),最终将数据放入对应socket的接收缓冲区,修改对应socket的三个数组 ,CPU会返回到被中断的地方继续执行,唤醒单线程,进入就绪态
  9. 当该线程被调度器选中再次运行时,回到用户态,并将修改后的数组拷贝到用户态
  10. 线程遍历三个数组筛选出哪些客户端的数据已经在内核空间的缓存中,记为t[fd1,fd3...]。
  11. 如果t[]中有监听socket的fd,说明有新的客户端请求连接 ,那么单线程调用accept()系统调用与客户端建立连接,得到一个客户端socket文件描述符fd,用于与该客户端通信,将fd添加到三个数组的某个bit位上。
  12. t[]中除了监听socket之外的其他fd说明是已建立连接的客户端发送的数据,单线程对t[]调用read()函数,进程从用户态切换到内核态 ,此时t[]的数据一定在内核缓存中,直接拷贝到对应fd的用户空间,然后切换回用户态。
  13. 系统调用返回,线程从内核态切换回用户态,并继续执行用户态代码。
  14. 处理完成后,线程传入添加新客户端fd后的三个数组重新进入内核态阻塞监听。

select缺点:

  • fd_set由于可变,需要频繁的修改用户空间和内核空间中的fd_set
  • 在用户态需要遍历原fd_set和修改后的fd_set才能知道那些数据就绪
  • 监听数量不超过1024

2.poll系统调用

poll仍然使用数组的方式监听不同的请求,但数组中每个元素都是一个结构体,记录了请求的文件描述符fd(socket、客户端)、请求类型events、返回值revents 。单线程在内核中监听时(等待中断响应),在超时时间内若监听到某个文件描述符的中断响应,就将响应类型记录到revents中,否则revents置0表示未收到相应

单线程下poll I/O多路复用读取网络数据过程:

  1. 监听socket接收到客户端A的三次握手,通过accept()建立连接,得到一个socket文件描述符将文件描述符、要监听的事件类型记录到polled数组中。
  2. 单线程 调用poll()函数,触发系统调用 ,进程从用户态切换到内核态 ,并将polled数组拷贝到内核态
  3. 内核态的该线程 检查缓存中是否有数据,如果有,判断数据是哪个fd 的,并判断是读请求、写请求还是出现异常 ,将fd对应结构体的revents置为相应值 ,切换回用户态,并将修改后的数组拷贝到用户态 ,返回就绪文件描述符数量n
  4. 线程判断n是否大于0,大于0则遍历polled数组,找到就绪的文件描述符。
  5. ...同select

3.epoll系统调用

epoll维护一棵红黑树和一个链表 ,红黑树记录要监听 的文件描述符fd,就绪链表记录已就绪 的文件描述符fd。与select最大的不同在于,epoll在内核态初始化epoll_create系统调用),不会频繁的在用户态和内核态拷贝。

由于在内核态,所以当线程通过accept()与客户端建立连接后,需要通过epoll_ctl()系统调用 向epoll的红黑树中添加fd 。此外,添加时会对fd设置一个回调函数,回调函数会在fd的数据写入内核缓存(触发中断响应)时将fd添加到就绪链表中。

线程使用epoll_wait()系统调用 持续监听就绪链表 ,若在超时时间内有fd加入到就绪链表中那么唤醒该线程,将就绪的fd拷贝到用户空间

epoll方案很好的解决了select的三个问题。

单线程下epoll I/O多路复用读取网络数据过程:

  1. 单线程调用epoll_create系统调用进入内核态,在内核态初始化一棵红黑树和一个链表,回到用户态。
  2. 单线程(服务器)创建监听socket,用来监听新客户端的连接请求。
  3. 单线程调用epoll_ctl()系统调用进入内核态,向红黑树中添加监听socket的fd,回到用户态。
  4. 单线程 调用epoll_wait()函数,触发系统调用 ,进程从用户态切换到内核态
  5. 内核态的该线程 检查就绪链表 中是否有fd,如果有,切换回用户态,并将就绪链表拷贝到用户态
  6. 如果未命中说明没有客户端发送数据 ,线程进入阻塞队列让出CPU,持续等待客户端数据。
  7. 如果阻塞时间超过了timeout,那么唤醒该线程,回到用户态。
  8. 客户端A或B发起HTTP请求,并携带数据。
  9. 客户端A或B发送的网络数据包到达服务器网卡,网卡通过DMA将数据包写入内核缓冲区,然后触发硬件中断,抢占CPU,执行中断处理程序
  10. 中断处理程序 将数据进行网络协议栈的处理(如IP、TCP处理),最终将数据放入对应socket的接收缓冲区,CPU会返回到被中断的地方继续执行,触发回调函数将数据对应的socket(fd)加入就绪链表 ,唤醒等待在该socket上的线程,进入就绪态
  11. 当该线程被调度器选中再次运行时,切换回用户态,并将就绪链表拷贝到用户态
  12. 如果就绪链表中有监听socket的fd,说明有新的客户端请求连接 ,那么单线程调用accept()系统调用与客户端建立连接,得到一个客户端socket文件描述符fd ,用于与该客户端通信,调用epoll_ctl()系统调用向红黑树中添加新客户端socket的fd,回到用户态。
  13. 就绪链表中除了监听socket之外的其他fd说明是已建立连接的客户端发送的数据,单线程对就绪链表中的fd 调用read()函数,进程从用户态切换到内核态 ,此时就绪链表中fd的数据一定在内核缓存中,直接拷贝到对应fd的用户空间,然后切换回用户态。
  14. 系统调用返回,线程从内核态切换回用户态,并继续执行用户态代码。
  15. 线程解析并处理命令,然后将响应结果写入客户端socket(fd)的发送缓冲区,调用write或send系统调用将响应结果通过DMA从内核的发送缓冲区由网卡发送出去。
  16. 处理完成后,重新进入内核态阻塞。

事件通知机制:

  • LevelTriggered:LT,每次将就绪队列中的fd拷贝到用户空间时保留就绪队列中的fd。
    • 可以实现重复读取,适用于fd一次读不全的情况。
  • EdgeTriggered:ET,每次将就绪队列中的fd拷贝到用户空间时清空就绪队列。
    • 适用于一次性读取fd的情况。
    • 可以通过epoll_ctl函数的修改功能手动将fd添加回就绪链表实现LT的效果

不太理解为什么用LT,每次都从网络读到用户内存就算一次读不全早晚也能读完吧,没必要每次都重复读吧,可能是因为数据在对应用户空间中不连续使用起来麻烦。

四、信号驱动I/O

信号驱动I/O通过sigaction()系统调用对fd设置回调函数 ,此时线程立即返回用户态 执行其他任务。当fd的数据到达内核缓存时会触发 回调函数,将就绪的fd拷贝到用户态的信号队列通知 线程。线程调用recvfrom进入内核态将数据拷贝到用户内存。

与非阻塞I/O不同的是,线程返回用户态后不会轮询数据是否准备好,而是去执行其他任务,收到来自内核的通知才调用read读数据。

感觉这种方式实际上比多路复用I/O更好,因为不用阻塞,且回调函数的思想与多路复用I/O类似,能够实现1个线程同时监听多个fd。但是由于信号驱动I/O下每有一个fd就绪内核线程都会执行内核态与用户态切换通知用户线程 ,影响性能;而多路复用I/O可以一次性获取多个就绪的fd才切换到用户态,性能更好(多路复用I/O中虽然每次fd到达就绪链表都会唤醒线程,但是线程获得CPU前仍然可以有fd进入就绪链表,且调用wait()进入内核态时如果就绪链表有多个fd也可以一次性返回)。因此多路复用I/O的性能更好更适用于有高并发需求的redis。

五、异步I/O

异步I/O整个过程都是非阻塞 的,用户进程调用aio_read()系统调用函数声明要读的fd和读到用户内存空间的地址 就直接返回到用户态,执行其他请求。而读取数据和将数据从内核缓存拷贝到用户内存的任务完全交由内核线程来完成。

异步I/O比I/O多路复用更高效,因为用户线程对I/O操作完全解耦,可以实现更高并发的处理请求,使用频率较高但是不如多路复用I/O:由于所有任务都交由内核完成,每个任务内核都要开辟新线程来处理 (内核是多线程,CPU也是多核,只有用户应用redis是单线程),对内核负载 太大。解决方法是在用户应用进行并发控制,限制单位时间向内核分配的任务数量

六、Redis网络模型*

1.纯单线程模型:

Redis通过I/O多路复用来提高网络性能 ,支持各种不同的多路复用实现,将这些实现封装为统一的接口:

  • ae_epoll:LinuxOS多路复用实现方案
  • ae_kqueue:MacOS
  • ae_select:所有OS

Redis提供了通用的API接口 ,针对不同操作系统使用不同的实现方案。

c 复制代码
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif
#endif
  • AddEvent():注册fd,例如epoll_ctl()
  • Create():创建多路复用监听器,例如epoll_create()
  • DelEvent():删除fd,例如epoll_ctl()
  • Poll():监听fd就绪,例如epoll_wait()、select()、poll()

Linux下redis(单线程) epoll 处理网络数据过程:(相较于三.3没有内核态的切换,但是流程更完整)

  1. 单线程调用epoll_create在内核态初始化一棵红黑树和一个链表。
  2. 单线程创建监听socket,用来监听新客户端的连接请求。
  3. 单线程调用epoll_ctl 向红黑树中添加监听socket的fd,设为可读状态,并添加回调函数。
  4. 为监听socket添加监听读处理函数
  5. 单线程调用epoll_wait ,阻塞并监听就绪链表,让出CPU。
  6. 如果阻塞时间超过了timeout,那么唤醒该线程。
  7. 新客户端A和B发起HTTP请求,请求建立连接
  8. DMA和中断处理程序将客户端A的数据读到监听socket 的接收缓冲区,触发回调函数将监听socket的fd加入就绪链表,标记为读请求
  9. 单线程监听到就绪链表中数据发生变化,进入就绪态。
  10. 单线程就绪态到运行态过程中可能DMA和中断处理程序将客户端B的数据读到监听socket 的接收缓冲区,触发回调函数将监听socket的fd加入就绪链表
  11. 单线程获取CPU,此时就绪链表中只有监听socket的fd ,会触发监听socket的监听读处理函数 :单线程调用accept()系统调用与客户端A和B建立连接,分别得到客户端A和B的socket文件描述符fd ,为客户端fd分别设为可读状态 添加读处理函数 ,调用epoll_ctl系统调用向红黑树中添加客户端A和B的fd。
  12. 单线程调用epoll_wait ,检查就绪链表中是否有fd,如果有,说明step11过程中有新的网络数据到达了内核缓存,切换回用户态处理。
  13. 如果未命中说明没有I/O请求,线程阻塞并监听就绪链表,让出CPU。
  14. 客户端A发来 HTTP请求,并携带数据。
  15. DMA和中断处理程序将客户端A的数据读到客户端A socket 的接收缓冲区,触发回调函数将客户端A的fd加入就绪链表,并设为读请求
  16. 单线程监听到就绪链表中数据发生变化,进入就绪态。
  17. 单线程获取CPU,此时就绪链表中有客户端A的fd,且为读请求 ,会触发客户端A socket的读处理函数 :单线程调用read系统调用将请求携带的数据从内核缓冲区读到为客户端A分配的内存输入缓冲区中。
  18. 单线程解析并执行数据中的指令,然后将响应结果写入客户端A的等待队列 ,调用epoll_ctl将客户端A的fd状态修改为可读+可写类型 ,并添加写处理函数
  19. 单线程调用epoll_wait ,检查就绪链表中是否有fd...
  20. 当客户端A的内核发送缓冲区有空闲就会发出硬件中断将客户端A的fd加入到就绪链表,并设为写请求
  21. 单线程获取CPU,此时就绪链表中有客户端A的fd,且为写请求 ,会触发客户端A socket的写处理函数 :单线程调用send()系统调用从客户端A的等待队列 中取数据,将数据写入客户端A的内核发送缓冲区,单线程检查客户端A的等待队列 是否还有数据,如果没有那么调用epoll_ctl将客户端A的fd状态修改为可读类型
  22. 内核协议栈会将数据通过网卡发送出去,发送成功后触发中断将客户端A的fd加入到就绪链表,并设为写请求(这个过程是异步的,由内核和网卡负责,不需要redis单线程参与)。
  23. 单线程调用epoll_wait ,检查就绪链表中是否有fd...

因此,Redis中的I/O多路复用可以理解为:复用 epoll同时监听客户端连接请求、客户端读请求、服务器向客户端的写请求 ,并每隔一段时间接收多条请求,针对不同请求使用不同的分支监听读处理函数step11、读处理函数step17+18、写处理函数step21 处理请求。

Linux下redis(单线程) epoll 处理网络数据过程:(从用户态调用的角度分析,不涉及内核态,多路复用的思想更直观)

c 复制代码
//redis网络I/O入口函数
main {
	server.el = aeCreateEventLoop();// 创建epoll
	listenToPort(server.port,server.ip);// redis创建监听socket,给定监听的ip和port(服务器的ip)
	createSocketAcceptHandler(acceptTcpHandler);// 将监听socket添加到epoll,并添加监听读处理函数(监听读处理函数就是step11) 
	
	// 死循环,该线程一直网络I/O
	while(true){
		// 为epoll中所有客户端socket依次添加写处理函数(写处理函数就是step21)
		foreach(el){
			connSetWriteHandlerWithBarrier(sendReplyToClient);
		}
		
		/* epoll_wait等待中断信号,返回就绪链表
		客户端发送的连接请求引发的中断会将监听fd添加到就绪链表,设为读请求
		客户端发送的读请求引发的中断会将客户端fd添加到就绪链表,设为读请求
		服务器向客户端发送返回结果引发的中断会将客户端fd添加到就绪链表,设为写请求
		*/
		numevents = aeApiPoll();
		// 依次处理就绪链表中的socket
		for(j = 0; j < numevents; j++){
			if(j is 监听socket){
				fd = accept(newClient);// 接收新客户端的连接请求,得到客户端socket的fd
				connSetReadHandler(fd, readQueryFromClient);// 将客户端socket添加到epoll,并添加读处理函数(读处理函数就是step17+18)
			}
			
			if(j is 客户端读socket){
				connRead(c);// 将数据从内核缓冲区读到内存中该客户端的输入缓冲区
				processInputBuffer(c);// 解析数据中的命令为字符串数组[set, name, jack]
				cmd = lookupCommand(c->argv[0]);// redis中命令是以键值对方式存储的,通过key=set就能找到对应的函数体
				proc(cmd,c);// 执行cmd命令,传入数据c
				addReply();// 将命令执行结果写入该客户端的等待队列
			}	

			if(j is 客户端写socket){
			    if (!clientHasPendingReplies(c)) {// 检查是否有数据要写
			        aeDeleteFileEvent(server.el, fd, AE_WRITABLE);// 没有数据,立即取消写事件监听
			        return;
			    }
			    int nwritten = write(c->fd, c->buf + c->sentlen, c->bufpos - c->sentlen);// 有数据,直接写入socket
			}	
		}
	}
}

2.命令处理单线程+网络I/O多线程模型:

2.1 Redis单线程网络模型的瓶颈:

Redis命令执行部分必须是单线程

为什么redis要做成单线程:

  • 除了持久化操作外,redis是纯内存 操作,因此执行速度非常快 ,限制redis性能的是网络I/O延迟 而不是指令执行速度,短板效应下多线程也不会带来性能提升。
  • 多线程会导致上下文切换,会带来额外开销。
  • 单线程是为了保证命令的隔离性,而多线程会有线程安全问题,使用锁机制可以解决线程安全问题但是也会带来额外的开销。

经过上面的分析,redis的执行过程为:接收网络请求(step12~17)->执行命令(step18)->返回响应结果(step19~22)

  • 接收请求:redis单线程阻塞等待,并行的网络请求由内核的中断处理程序并行 处理,性能很强,但是redis单线程返回用户态后需要串行 执行read系统调用将数据从内核态读到用户态,且需要频繁切换用户态和内核态,这是主要瓶颈
  • 执行命令:redis单线程串行执行命令,因为命令必须串行才能保证原子操作,必须保持现状。
  • 返回响应:由redis单线程串行 调用send()函数发送响应,且需要频繁切换用户态和内核态(虽然内核态发送响应可以异步并行执行,但串行send效率太低),这是主要瓶颈

由于redis性能收到网络I/O的限制,经过上述分析,网络I/O的接受请求、返回响应部分可以设计成多线程,且这两部分不涉及命令的执行,所以不会出现并发问题。

2.2 Redis多线程网络模型:

对于接收请求,由redis单线程分发"read系统调用将数据从内核态读到用户态 (step17)"这一操作给多个子线程执行,大大提高了读取速度,且不会出现线程安全问题(不涉及请求数据中的命令执行)。

数据读取并解析完成后,由redis单(主)线程来顺序执行命令,内存执行速度快,切能保证原子操作。虽然接受请求使用多线程,但速度上仍然无法保证指令执行的主线程有100%的利用率。

对于返回响应,由redis单线程分发"send系统调用将数据从用户靠读到内核态 (step21)"这一操作给多个子线程执行,大大提高了读取速度,且不会出现线程安全问题。

虽然如此,但网络I/O依旧是短板。

相关推荐
量子物理学1 小时前
openssl自建CA并生成自签名SSL证书
网络·网络协议·ssl
成空的梦想1 小时前
除了加密,它还能验明正身:SSL如何防范网络钓鱼?
网络·https·ssl
数据库学啊2 小时前
专业的国产时序数据库哪个好
数据库·时序数据库
爱吃面条的猿2 小时前
MySQL 随机日期/时间生成
数据库·mysql
2501_939909052 小时前
Mysql 主从复制、读写分离
数据库·mysql
潇湘秦2 小时前
ORACLE_PDB_SID和ORACLE_SID的区别
数据库·oracle
honsor3 小时前
精准监测 + 实时传输!网络型温湿度传感器,筑牢环境数据管理防线
网络·物联网
0***86333 小时前
SQL Server2019安装步骤+使用+解决部分报错+卸载(超详细 附下载链接)
javascript·数据库·ui
wstcl3 小时前
通过EF Core将Sql server数据表移植到MySql
数据库·mysql·sql server·efcore