一、Redis单线程和多线程
1.Redis为什么选择单线程:
主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取(socket 读)、解析、执行、内容返回(socket 写)等都由一个顺序串行的主线程处理,这就是所谓的"单线程"。这也是Redis对外提供键值存储服务的主要流程。但Redis的其他功能,比如持久化RDB、AOF、异步删除、集群数据同步等等其实是由额外的线程执行的。Redis命令工作线程是单线程的,但是,对于整个Redis来说是多线程的;
**(1).基于内存操作:**Redis的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能比较高;
**(2).数据结构简单:**Redis的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分复杂度都是O(1),因此性能比较高;
**(3).多路复用和非阻塞I/O:**Redis使用I/O多路复用功能来监听多个socket连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免了I/O阻塞操作;
**(4).避免上下文切换:**因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生
简单来说,Redis4.0之前一直采用单线程的主要原因有以下三个:
a.使用单线程模型是Redis的开发和维护更简单,因为单线程模型方便开发和调试:
b.即使使用单线程模型也并发的处理多客户端的请求,主要使用的是IO多路复用和非阻塞IO
c.对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽而并非CPU
**2.Redis为什么这么快:**采用了IO多路复用技术
对于Redis主要的性能瓶颈是内存或者网络带宽并非CPU
Redis6/7采用多个IO线程处理网络请求,提高网络请求处理的并行度;但是Redis的多IO线程只是用来处理网络请求的,对于读写操作命令Redis仍然使用单线程来处理。这是因为,Redis处理请求时,网络处理经常是瓶颈,通过多个IO线程并行处理网络操作,可以提升实例的整体处理性能。而继续使用单线程执行命令操作,就不用为了保证Lua脚本、事务的原子性,额外开发多线程互斥加锁机制(不管加锁操作处理),这样一来Redis线程模型实现就简单
阶段一:服务端和客户端建立Socket连接,并分配处理线程
首先,主线程负责接收建立连接请求,当有客户端请求和实例建立Socket连接时,主线程会创建和客户端的连接,并把Socket放入全局等待队列中。紧接着主线程通过轮询方法把Socket连接分配给IO线程
阶段二:IO线程读取并解析请求
主线程一旦把Socket分配给IO线程,就会进入阻塞状态,等待IO线程完成客户端请求读取和解析。因为有多个IO线程在并行处理,所以这个过程很快就可以完成。
阶段三:主线程执行请求操作
等到IO线程解析完请求,主线程还是会以单线程的方式执行这些命令操作
阶段四:IO线程回写Socket和主线程清空全局队列
当主线程执行完请求操作后,会把需要返回的结果写入缓冲区,然后主线程会阻塞等待IO线程,把这些结果回写到Socket中,并返回给客户端,和IO线程读取和解析请求一样,IO线程回写Socket时,也是有多个线程在并发执行,所以回写Socket的速度也很快。等到IO线程回写Socket完毕主线程会清空全局队列,等待客户的后续请求。
UNIX网络编程中的五种IO模型:
**a.BlockingIO:**阻塞IO
**b.NoneBlockingIO:**非阻塞IO
**c.IO multiplexing:**IO多路复用,一种同步的IO模型,实现一个线程监视多个文件句柄;一旦某个文件句柄就绪就能够通知到对应应用程序进行相应的读写操作,没有文件句柄就绪时就会阻塞应用程序从而释放CPU资源;IO多路复用是指一个或一组线程处理多个TCP连接,使用单进程就能够实现同时处理多个客户端连接,无需创建或者维护过多的进程或线程;实现IO多路复用的模型有三种,可以分为select、poll、epoll三个阶段来描述
**d.signal drivenIO:**信号驱动IO
**e.asynchronouslO:**异步IO
总结:
二、Redis中的BigKey
string类型控制在10KB以内,hash,list,set,zset元素个数不要超过5000;非字符串的bigkey不要使用del删除;使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会出现在慢查询中(latency可查))
string是value,最大512KB但是大于等于10KB就是BigKey,list、hash、set和zset个数超过5000就属于bigkey
bigkey会带来内存不均,集群迁移困难,超时删除以及网络流量阻塞等危害
string类型通过del删除,如果过大则使用unlink删除
hash类型使用hscan每次获取少量field-value,再使用hdel删除每个field
list类型使用ltrim渐进式逐步删除,直到全部删除完成
set类型使用sscan每次获取部分元素,再使用screm命令删除每个元素
zset类型使用zscan每次获取部分元素,再使用ZREMRANGEBYRANK命令删除每个元素
三、缓存双写一致性
1.缓存双写一致性的理解:
如果redis中有数据,需要和数据库中的值相同,如果redis中无数据,数据库中的值要是最新值,且准备回写redis;缓存分为只读缓存和读写缓存,读写缓存分为同步读写策略和异步读写策略,同步读写策略写入数据库后也同步写入到redis缓存中,缓存和数据库中的数据保持一致,对于读写缓存来说,要想保证缓存和数据库中的数据一致就要采用同步直写策略;异步缓写策略在正常业务中当数据库中的数据发生变动后允许业务上出现一定时间后才作用于redis,当异常情况出现后不得不将失败的动作重新修补,需要借助kafka或者rabbitMQ等消息中间件实现重写
2.更新策略:
给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准。