微信公众平台做微网站,绍兴seo,海口今天的消息,wordpress修改指向域名Redis是单线程程序。单线程的Redis为何还能这么快#xff1f; 1、所有的数据都在内存中#xff0c;所有的运算都是内存级别的运算#xff08;因此时间复杂度为O(n)的指令要谨慎使用#xff09; 2、单线程操作#xff0c;避免了频繁的上下文切换 3、多路复用#xff08;非…Redis是单线程程序。单线程的Redis为何还能这么快 1、所有的数据都在内存中所有的运算都是内存级别的运算因此时间复杂度为O(n)的指令要谨慎使用 2、单线程操作避免了频繁的上下文切换 3、多路复用非阻塞IO多路复用NIO来处理客户端的并发连接 非阻塞IONon-block IO NIO非阻塞模式使一个线程从某通道发送请求数据读取数据如果目前没有数据可读时就什么都 不会获取而不是保持线程阻塞直到有数据可读之前该线程可以继续做别的事情非阻塞写也是如此能写多少取决 于内核为套接字分配的写缓冲区的空闲字节数不必等到完全写入这个线程可以去做别的事情。线程通常将非阻塞IO的空 闲时间用于在其他通道上执行IO操作所以一个单独的线程现在可以管理多个输入和输出通道channel。 阻塞IOBIO如Java IO中的各种流都是BIO阻塞的。当一个线程调用read或者write方法时该线程会被阻塞知道有一些数据 被读取或数据完全写入。该线程在此期间不能再干别的事情了。 事件轮询多路复用 非阻塞IO有个问题那就是线程要读数据结果读了一部分后返回了那么当数据到来时如何通知线程继续读呢写也是一 样如果缓冲区写满了没有写完剩下的数据何时继续写线程也应该得到通知。 事件轮询API就是用来解决这个问题的。最简单的事件轮询API是select函数。它是操作系统提供给用户程序的API。输入是读 写描述符列表read_fdswrite_fds输出是与之对应的可读可写事件。同时还提供了一个timeout参数如果没有任务事件到来 那么就最多等待timeout的值的时间线程处于阻塞状态。一旦期间没有任务事件到来就可以立即返回。时间过了之后没有任务 事件到来也会立即返回。拿到事件后线程就可以挨个处理相应的事件。处理完了继续过来轮询。于是线程就进入了一个死循环 这个死循环称为事件循环一个循环为一个周期。 通过系统提供的epoll函数同时处理多个通道描述符的读写事件因此将这类调用称为多路复用API。 一句话总结就是利用操作系统提供的epoll函数基于事件驱动同时处理多个通道描述符的读写事件来实现多路复用 指令队列Redis会将每个客户端套接字都关联一个指令队列。客户端的指令通过队列来排队进行顺序处理先到先服务。 响应队列Redis会为每个客户端套接字都关联一个响应队列。Redis服务器通过响应队列来将指令的返回结果回复给客户端。如果队列 为空那么意味着连接暂时处于空闲状态不需要去获取写事件。 定时任务服务器除了要响应IO事件外还要处理其他的事情。比如定时任务就是非常重要的一件事。如果线程阻塞在select系统调用上 定时任务无法得到准时调度。Redis的定时任务会记录在一个被称为最小堆的数据结构中。在这个堆中最快要执行的任务排 在堆的最上方每个循环周期里。Redis都会对最小堆里面已经到时间点的任务进行处理。处理完毕后将最快要执行的任务还 需要的时间记录下来这个时间就是select系统地哦暗涌的timeout参数。因为Redis知道未来timeout的值的时间内没有其他定 时任务需要处理所以可以安心睡眠timeout的值的时间。 Redis单线程特性的优缺点 优点 1、代码更清晰逻辑更简单 2、不用因为同步去考虑各种锁的问题不存在加锁和释放锁的操作基本不会出现死锁而导致的性能消耗 3、不存在多线程切换导致的CPU消耗 缺点 无法发挥多核CPU的性能不过可以通过在单机开多个Redis实例来实现 持久化 Redis的数据全部在内存里如果突然宕机数据就会全部丢失因此必须有一种机制来保证Redis中的数据不会因为故障而丢失这 种机制就是Redis的持久化机制。 Redis的持久化机制有两种 一、快照RDB 1、一次全量备份使用 BGSAVE命令 2、保存方式是内存数据的二进制序列化形式在存储上非常紧凑 3、使用操作系统的多进程COWcopy on write机制来实现持久化持久化时调用glibc的函数fork分岔产生一个子进程持久 化完全交给子进程来处理父进程继续处理客户端请求 4、COW机制的数据页面的分离。父进程在对页面的数据进行修改时会将被共享的页面复制一份分离出来然后对复制出来的页面 进行修改。这时子进程的页面是没有变化的还是进程产生那一瞬间的数据所以这种持久化叫做快照的原因。 5、使用fork子进程无法实时宕机会造成数据丢失 二、AOF日志 1、连续的增量备份使用appendonly yes开启 2、存储的是Redis服务器的顺序指令序列只记录对内存进行修改的指令序列 3、记录的是内存数据修改的指令记录文本在长期的运行过程中会变得非常庞大数据库重启时需要加载AOF日志进行指令重放 比较耗时所以需要定期进行AOF重写给AOF日志瘦身 4、Redis收到客户端修改指令后进行参数校验、逻辑处理如果没问题就立即将该指令写到缓冲区中然后每秒钟调用一次 fsync将指令存储到AOF日志中 5、使用bgrewriteaof指令对AOF日志进行瘦身即开辟一个子进程对内存进行遍历转换成一系列Redis的操作指令序列化到一 个新的AOF日志文件中再将操作期间新增的AOF日志追加到这个新的AOF日志文件中替代旧的AOF日志文件完成瘦身。 Redis4.0混合持久化 实际应用中重启Redis时很少使用RDB来恢复内存状态因为会丢失大量数据。所以我们通常使用AOF日志重放但是重放AOF日志 相对于RDB要慢得多。 混合持久化将RDB文件的内容和增量的AOF日志文件存在一起。这里的AOF日志不是全量的日志而是自持久化开始到持久化结束的这 段时间发生的增量AOF日志通常这部分AOF日志很小。因此重启的时候先加在RDB内容然后再重放增量AOF日志替 代之前的AOF的全量文件重放重启效率得到大幅度提升。 事务 Redis的事务模型并不严格不具备原子性事务的命令如果有执行失败并不会回滚 基本的事务操作都有begin、commit和rollback。begin指示事务的开始commit指示事务的提交rollback指示事务的回滚。 Redis事务的指令也差不多分别是multi、exec、discard。multi指示事务的开始exec指示事务的执行discard指示事务的丢弃。 Redis的指令在exec之前不执行而是缓存在服务器的事务队列中服务器一旦收到exec指令才开始执行整个事务。因为Redis是单 线程所以在执行队列中的命令时不会被其他指令打搅。 但是Redis的事务不具备原子性而仅仅满足了事务的隔离性中的串行化当前事务执行不会被其他事务干扰。 优化Redis事务在每发送一个指令到事务缓存队列都要经过一次网络读写当一个事务内部的指令较多时需要的网络IO也会线性增长 所以通常Redis的客户端在执行事务时都会结合pipeline一起使用。 WatchCAS机制 多个客户端并发修改Redis中的一条记录。需要先读再写。为了保证线程安全一种方式是通过Redis分布式锁的方式但是Redis 分布式锁是悲观锁。Redis提供了watch机制是一种乐观锁在multi之前监视某个关键变量若在watch之后被修改了包含当前事务 所在的客户端如果关键字被修改了则exec指令就会返回NULL回复告知客户端事务执行失败这个时候客户端一般会选择重试。 Redis管道技术Pipeline Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。这意味着通常情况下一个请求会遵循以下步骤 1、客户端每发送一个查询请求并监听Socket返回通常是以阻塞模式等待服务端响应 2、服务端处理命令并将结果返回给客户端 Redis管道技术可以在服务端未响应时客户端可以继续向服务端发送请求并最终一次性读取所有的服务端响应。 管道技术显著的提高了Redis的性能尤其是在大量写操作的情况下。 RESP REdis Serialization ProtocolRedis序列化协议。Redis服务端与客户端通过RESP协议进行通信节点交互不使用这个协议。 有如下特性是二进制安全的、在TCP层、基于请求-响应的模式 RESP有五种最小的单元类型单元结束时统一加上回车换行符【\r\n】 1、单行字符串以符号开头如字符串hello world--hello world\r\n 2、多行字符串以$符号开头后跟字符串长度如多行字符串hello world--$11\r\nhello world\r\n 3、整数值以符号开头后跟整数的字符串形式如整数12--:12\r\n 4、错误信息以-符号开头如-WRONGTYPE\r\n 5、数组以*号开头后跟数组的长度如数组[1,2,3]--*3\r\n:1\r\n:2\r\n:3\r\n 客户端向服务端发送的指令只有一种格式多行字符串数组。 比如一个简单的 set 指令set author codehole会被序列化成下面的字符串。*3\r\n$3\r\nset\r\n$6\r\nauthor\r\n$8\r\ncodehole\r\n 服务端对客户端的响应支持多种数据结构即以上5种的基本类型的组合。 发布订阅 PubSub 前面所讲的Redis的消息队列一个消息只能被一个消费者消费不支持消息的多播机制。 消息多播允许生产者只生产一次消息由中间件负责将消息复制到多个消息队列每个消息队列由相应的消费组进行消费是分布式 系统常用的一种解耦方式用于将多个消费组的逻辑进行拆分。支持了消息多播每个消费组对应的不同子系统可以有不同的逻辑处理。 在生产环境中一般将生产者和消费者分离 消费者可通过listen来阻塞监听消息来进行处理。 模式订阅消费者可以同时订阅多个主题的消息但是如果生产者新增了一个主题消费者也必须增加一个订阅指令才能收到新增的 主题的消息。为了简化这种订阅的繁琐Redis提供了模式订阅功能Pattern Subscribe这样就可以一次订阅多个主题即使生产者新增 加了同模式的主题消费者也可以立即收到消息。 如psubscribe code.* 那么所有以code. 开头的主题的消息都能订阅到。 消息结构 1、data消息的内容一般一个字符串 2、channel当前订阅的主题的名称 3、type消息的类I型那个如果是普通的消息那么类型就是message如果是控制消息比如订阅指令的反馈它的类型就是 subscibe如果是模式订阅的反馈它的类I型那个就是psubscribe此外还有取消订阅指令的反馈unsubscribe和punsubscribe。 4、pattern表示当前消息是使用哪种模式订阅到的。如果是通过subscribe指令订阅到的这个字段就是None 缺点 1、消费者挂掉重连后在断连期间生产者发送的消息就丢失了 2、如果Redis停机重启PubSub的消息不会持久化所有的消息都会丢失 小对象压缩存储ziplist Redis的所有数据都放在内存中所以在使用过程中要注意节约内存否则就可能出现Redis内存不足导致崩溃。如果Redis内部管理 的集合list数据结构的数据很小则它会使用紧凑存储形式压缩存储。 Redis的ziplist是一个紧凑的字节数组结构每个元素之间都是紧挨着的 内存回收机制 Redis并不总是将空闲内存立即归还给操作系统。 如果当前Redis内存有10GB当你删除了1GB的key后再去观察内存你会发现内存变化不会太大。这是以为内操作系统是以页为 单位回收内存的这个页上只要还有一个key在使用那么这个页就不能被回收。Redis虽然删除了1GB的key但是这些key分散到了很 多的页中每个页都还有其他的key存在这就导致了内存不会被立即回收。 不过如果你执行flushdb然后再观察内存会发现内存确实被回收了。原因是所有的key都被删掉了大部分之前使用的页都完全 空了就会立即被操作系统回收。 Redis虽然无法保证立即回收已经删除的key的内存但是它会重新使用那些尚未回收的空闲内存。 内存分配算法 内存分配是一个非常复杂的课题需要适当的算法划分内存页需要考虑内存碎片需要平衡性能和效率Redis将内存分配的细节交给 了第三方内存分配库去实现。默认的内存分配库是jemalloc 集群 主从同步当主节点master挂掉的时候从节点slave接管服务使服务可以继续。否则主节点需要经过数据恢复和重启使服务中断 很长时间。 分布式系统存储的理论基石——CAP原理 CConsistent一致性 AAvailability可用性 PPartition tolerance分区容忍性 分布式系统的节点往往都是分布在不同的机器上进行网络隔开的这意味着必然有网络断开的风险这个网络断开的场景的专业词汇叫做 网络分区。 在网络分区发生时两个分布式节点之间无法进行通信我们对一个节点的修改操作无法同步到另一个节点所以数据的一致性将无法满 足因为两个分布式节点的数据不再保持一致除非牺牲可用性也就是暂停分布式节点服务在网络分区发生时不再提供修改数据的 功能直到网络状况完全恢复正常再继续对外提供服务。即当网络分区发生时一致性和可用性不可兼得Redis满足AP。 最终一致 Redis的主从数据是异步同步的所以分布式的Redis系统并不满足一致性要求。当客户端在Redis的主节点修改了数据后立即返回 即使在主从网络断开的情况下主节点依旧可以正常对外提供修改服务所以Redis满足可用性。 Redis保证最终一致性从节点会努力追赶主节点最终从结点的状态会和主节点的状态保持一致。如果网络断开了主从节点的数据会 出现大量不一致但一旦网络恢复从节点会采用多种策略努力追赶继续尽力保持和主节点一致。 增量同步 Redis同步的是指令流主节点会将那些对自己的状态产生修改性影响的指令记录在本地的内存buffer中然后异步将buffer中的指令 同步到从节点从节点一边执行同步的指令流来达到和主节点一样的状态一边向主节点反馈自己同步到哪里了偏移量。 因为内存的buffer是有限的所以Redis主节点不能将所有的指令都记录在内存buffer中Redis的复制内存buffer是一个定长的环形数 组如果数组内容满了就会从头开始覆盖前面的内容如果因为网络状况不好从节点在短时间内无法和主节点进行同步那么当 网络恢复时Redis的主节点中那些没有同步的指令在buffer中有可能被后续的指令覆盖掉了从节点将无法直接通过指令流来进行同 步这时就需要用到更加复杂的同步机制——快照同步。 快照同步 快照同步是一个十分消耗资源的操作它首先需要在主节点上进行一次bgsave将当前内存的数据全部快照到磁盘文件中然后再将 快照文件的内容全部传送到从节点。从节点将快照文件接收完毕后立即执行一次全量加载。加载之前要先将当前内存的数据清空加载 完毕后继续通知主节点继续进行增量同步。 在快照同步的过程中主节点的buffer还在不停的往前移动如果快照同步的时间过长或者复制buffer太小都会导致同步期间的增量指令 在复制buffer中被覆盖这样就会导致快照同步完成后无法进行增量复制然后再次发起快照同步如此极有可能会陷入快照同步的死循环。 所以buffer大小参数一定要设置合适避免快照复制的死循环。 哨兵Sentinel 如果主节点突发宕机那么如何自动主从切换Redis Sentinel哨兵就是一种抵抗结点故障的高可用方案。 可以将Redis Sentinel集群看成是一个zookeeper集群它是集群高可用的核心。一般由3-5个节点组成这样即使个别节点挂了集群还 可以正常运转。 Sentinel负责持续监控主从节点的健康当主节点挂掉时自动选择一个最优的从节点切换成主节点。 客户端来连接集群时会首先连接Sentinel通过Sentinel来查询主节点的地址然后再连接主节点进行数据交互。主节点发生故障时客户 端会重新向Sentinel获取新的主节点的地址如此应用程序将无需重启即可自动完成节点切换。 如果主节点挂掉了原先的主从复制也断开了客户端和损坏的主节点也断开了。一个从节点被提升为新的主节点其他从节点开始和新 的主节点建立复制关系。客户端通过新的主节点继续进行交互。Sentinel会持续监控已经挂掉了的前主节点待它恢复后变成从节点和 新的主节点建立复制关系。 消息丢失 Redis主从采用异步复制意味着当主节点挂掉时从节点可能还未收到全部的同步消息这部分未同步的消息就丢失了。如果主从延 迟特别大那么丢失的数据就可能会特别多。Sentinel无法保证消息完全不丢失但是也尽量保证消息少丢失。有下面两个选项避免主从延 迟过大 min-slaves-to-write 1 表示主节点必须至少有一个从节点在进行正常复制否则就对外停止写服务丧失可用性 min-slaves-max-lag 10 单位秒表示如果在10s内没有收到从节点的反馈就意味着从节点同步不正常。 Sentinel的默认端口是26379不同于Redis的默认端口6379通过Sentinel对象的discover_xxx方法可以发现主从地址主地址只有一个 从地址可以有多个。通过master_for 或者 slave_for方法可以从连接池中获取主节点或者从节点的连接实例。因为从地址有多个所以Redis 客户端对从地址采用RoundRobin轮询方案。 集群Codis 在大数据高并发情况下单个Redis实例往往会显得捉襟见肘。 首先体现在内存上单个Redis的内存不宜过大内存太大会导致rdb文件过大进一步导致主从同步时全量同步时间过长在实例重启恢复 时也会消耗很长的数据加载时间。 其次体现在CPU的利用率上单个Redis实例只能利用单个核心这单个核心要完成海量数据的存取和管理工作压力非常大。 所以Redis集群应运而生。它可以将众多小内存的Redis实例整合起来将分布在多台机器上的众多CPU核心的计算能力聚集在一起完成海 量数据存储和高并发读写操作。 Codis是一个代理中间件和Redis一样也使用Redis协议对外提供服务当客户端向Codis发送指令时Codis负责将指令转发到后面的Redis 实例来执行并将返回结果再转回给客户端。 Codis上挂载的所有Redis实例构成一个Redis集群当集群空间不足时可以通过动态增加Redis实例来实现扩容需求。 因为Codis是无状态的它只是一个转发代理中间件这意味着我们可以启动多个Codis实例供客户端使用每个Codis节点都是对等的。因 为单个Codis代理能支撑的QPS比较有限通过启动多个Codis代理可以显著增加整体的QPS需求还能起到容灾功能挂掉一个Codis代理实 例没有关系还有很多的Codis代理实例可以提供服务。 Codis分片原理 Codis负责将特定的key转发到特定的Redis实例这种对应关系Codis是如何管理的呢Codis默认将所有的key划分为1024个槽位slot 如果集群节点比较多也可以手动设置大一些如2048 它首先对客户端传来的key进行crc32运算计算hash值再将hash后的整数值对1024这个整数进行取模得到一个余数这个余数就是对应 的key的槽位。而每个槽位都会唯一映射到后面的多个Redis实例中的一个。Codis会在内存中维护槽位和Redis实例的映射关系这样有了 key对应的槽位将这个key转发到那个Redis实例就很明确了。 不同的Codis实例之间槽位关系如何同步 如果Codis的槽位映射关系只存储在内存里那么不同的Codis实例之间的映射关系就无法得到同步。所以Codis还需要一个分布式配置存储 数据库专门用来持久化槽位关系Codis支持zookeeper和etcd。 Codis将槽位关系存储在zookeeper中并且提供了一个Dashboard可以用来观察和修改槽位关系当槽位关系变化时Codis Proxy会监听 到变化并重新同步槽位关系从而实现多个Codis Proxy之间共享槽位关系配置。 集群Cluster Redis Cluster 是Redis的作者自己提供的Redis集群化方案。与Codis不同Redis Cluster是去中心化的该集群由三个Redis节点组成每个 节点负责整个集群的一部分数据每个节点负责的数据多少可能不一样这三个节点组成一个对等的集群它们之间通过一种特殊的二进制协议交 互集群信息。 Redis Cluster将所有数据划分为16384个槽位它比Codis的1024个槽位划分的更为精细每个节点负责其中一部分槽位。槽位的信息存储于每个 节点中不像Codis不需要另外的分布式存储空间来存储节点信息。 当Redis Cluster的客户端来连接集群时也会得到一份集群的槽位配置信息。这样当客户端要查找某个key时可以直接定位到目标节点。这一点 于Codis也不同Coids需要通过Proxy来定位目标节点Redis Cluster则是直接定位。 客户端为了直接定位某个具体的key所在的节点需要缓存槽位的相关信息这样才可以准确快速地定位到相应的节点。同时因为可能会存在客户端 与服务端存储槽位的信息不一致的情况还需要纠正机制来实现槽位信息的校验调整。另外Redis Cluster的每个节点会将集群的配置信息持久化到 配置文件中所以必须确保配置文件是可写的而且尽量不要依靠人工修改配置文件。 槽位定位算法 Redis Cluster默认会对key值使用crc16算法进行hash得到 一个整数值然后用这个整数值对16384进行取模来得到槽位。 容错Redis Cluster可以为每个主节点设置若干个从节点当主节点发生故障时集群会自动将其中某个从节点提升为主节点。如果某个主节点没有 从节点那么当它发生故障时集群将完全处于不可用状态。 Info指令 在使用Redis时时长会遇到很多问题需要诊断在诊断之前需要了解Redis的运行状态通过强大的Info指令可以清晰地知道Redis内部一系列运行 参数。Info指令显示的信息繁多分为9大块每个块都有非常多的参数 1、Server 服务器运行的环境参数 2、Clients 客户端相关信息 3、Memory 服务器运行内存统计数据 4、Persistence 持久化信息 5、Stats 通用统计数据 6、Replication 主从复制相关信息 7、CPU CPU使用情况 8、Cluster 集群信息 9、KeySpace 键值对统计数量信息 Info stats|grep ops 每秒操作数 moniter 哪些key被访问得比较频繁 Info clients 连接了多少客户端 Info memory Redis占用了多少内存 分布式锁之Redlock算法 在Sentinel集群中当主节点挂掉时从节点会取而代之但客户端上并没有明显感知。比如第一个客户端在主节点上申请成功了一把锁但是 这把锁还没有来得及同步到从节点主节点突然挂掉了然后从节点变成了主节点这个新的主节点内部没有这个锁所以当另一个客户端过来请 求加锁时立即就批准了。这样导致系统中同样一把锁被两个客户端同时持有不安全性由此产生。 这种不安全仅在主从发生failover失效接管的情况下才会产生持续的时间极短业务系统多数情况下可以容忍。 Redlock的出现就是为了解决这个问题。要使用Redlock需要提供多个Redis实例这些实例之前相互独立没有主从关系。同很多分布式算法 一样Redlock也使用 “大多数机制“ 加锁时它会向过半节点发送 setkeyvaluenxTrueexxxx指令只要过半节点set成功就认为加锁成功。释放锁时需要向所有节点发 送del指令。不过Redlock算法还需要考虑出错重试、时钟漂移时钟抖动频率在10hz一下等很多细节问题。同时因为Redlock需要向多个节点进行 读写意味着其相比单实例Redis的性能会下降一些 Redlock使用场景非常看重高可用性即使Redis挂了一台也完全不受影响就使用Redlock。代价是需要更多的Redis实例性能也会下降需 要引入额外的library运维上也需要区别对待。 分布式锁之过期时间到了锁失效但任务还未执行完毕 某个线程在申请分布式锁的时候为了应对极端情况比如机器宕机那么这个锁就一直不能被释放。一个比较好的解决方案是申请锁的时候 预估一个程序的执行时间然后给锁设置一个超时时间这样即使机器宕机锁也能自动释放。 但是这也带来了一个问题就是在有时候负载很高任务执行的很慢锁超时自动释放了任务还未执行完毕这时候其他线程获得了锁导致程序 执行的并发问题。对这种情况的解决方案是在获得锁之后就开启一个守护线程定时去查询Redis分布式锁的到期时间如果发现将要过期了就 进行续期。 朝生暮死-过期策略 设置了有效期的key到期了怎么删除呢 Redis会将每个设置了过期时间的key放入一个独立的字典中以后会定时遍历这个字典来删除到期的key。除了定时遍历之外还会使用惰性删除 过期的key。所谓惰性删除就是在客户端访问这个key的时候Redis对key的过期时间进行检查如果过期了就会立即删除。所以过期key的删除策略 是 定时删除惰性删除 定时删除Redis默认每秒进行10次过期扫描过期扫描不会遍历过期字典中所有的key而是采用了一种简单的贪心策略步骤如下 1、从过期字典中随机选出20个key 2、删除这20个key中已经过期的key 3、如果过期的key的比例超过1/4那就重复步骤1 同时为了保证过期扫描不会出现循环过度导致线程卡死的现象算法还增加了扫描时间的上限默认不会超过25ms。 假设一个大型的Redis实例中所有的key在同一时间过期了会出现怎么样的结果呢毫无疑问Redis会持续循环多次扫描过期字典直到过期 字典中过期的key变得稀疏才会停止循环次数明显下降。这就会导致线上读写请求出现明显的卡顿现象。导致这种卡顿的另外一种原因是内存 管理器需要频繁回收内存页这也会产生一定的CPU消耗。 如当客户端到来时服务器正好进入过期扫描状态客户端的请求将会等待至少25ms后才会进行处理如果客户端将超时时间设置的比较短如10 ms那么就会出现大量的请求因为超时而关闭。业务端会出现很多异常而且这是你还无法从Redis的slowlog中看到慢查询记录因为慢查询指的是 逻辑处理过程慢而不包含等待时间。所以当客户端出现大量超时而慢查询日志无记录时可能是当前时间段大量的key过期导致的。 所以在开发过程中一定要避免在同一时间内出现大量的key同时过期。尽量给key的过期时间设置一个随机范围使其过期时间均匀分布。 从节点不会进行过期扫描过期的处理是被动的主节点在key到期时会在AOF日志文件中增加一条del指令同步到所有的从节点从节点通过执行 这条del指令来删除过期的key。因为指令同步是异步的所以会出现从节点的key删除不及时的情况。 惰性删除 实际上Redis内部并不是只有一个主线程它还有几个异步线程来处理一些耗时的操作。如果被删除的key是一个非常大的对象那么del指令删除操作就 会导致单线程卡顿。所以4.0版本引入了unlink指令可以对删除操作进行懒处理丢给后台线程来异步回收内存。 在获取某个key的时候Redis会检查一下这个key是否设置了过期时间以及这个是否到期了如果到期了就交给后台线程去删除这个key然后主线程什 么也不会返回。 优胜劣汰-LRU内存淘汰机制 当Redis内存超过物理内存限制时内存的数据会开始和磁盘产生频繁的交换swap交换会让Redis的性能急剧下降对于访问量比较大的Redis来说会 导致响应时间过长。所以在生产环境中不允许有这种交换行为为了限制最大内存Redis提供了配置参数maxmemory参数来限制内存使用阀值当超出这个 阀值时Redis提供了几种可选的内存淘汰策略供用户选择以腾出空间以继续提供读写服务。 1、noeviction 不会继续服务写请求del和读服务可以继续进行这是默认的淘汰策略 2、volatile-lru 尝试淘汰设置了过期时间的最近最少使用的key 3、volatile-ttl 尝试淘汰了设置了过期时间的ttlTime to live最少的key 4、volatile-random 尝试从设置了过期时间的key中随机淘汰一部分key 5、allkeys-lru 尝试淘汰所有的key中最近最少使用的key 6、allkeys-random 尝试从所有的key中随机淘汰一部分key LRU算法 实现LRU算法除了需要key/value字典外还需要附加一个链表链表中的元素按照一定的顺序进行排列。当字典中的某个元素被访问时会将它从在链 表中的某个位置移动到链表头部当空间满的时候会踢掉链表尾部的元素。所以链表元素的排列顺序就是元素最近被访问的顺序。 Redis使用的是近似的LRU算法因为LRU算法需要占用大量的额外内存还需要对现有的数据结构进行比较大的改造。近似LRU算法很简单在现有的数据 结构的基础上使用随机采样法淘汰元素通过给每个key增加一个额外的24bit的小字段存储最后一次被访问的时间戳。而且采用的是惰性策略Redis在执行 写操作时发现内存超过maxmemory就会执行一次近似LRU算法随机采样出5可以设置个key然后淘汰掉最旧的key如果淘汰后内存仍大于 maxmemory继续采样淘汰知道内存小于maxmemory为止。 手写一个LRU算法有三种方案 1、数组用数组来存储数据并给每个数据项标记一个时间戳每次插入新数据项的时候先把数组中存在的数据项对应的时间戳自增并将新 数据项的时间戳置为0并插入到数组中。每次访问数组中新数据项的时候将被访问的数据项的时间戳置为0。当数组空间满时将时间戳最大的数 据项淘汰。 2、链表每次插入新数据的时候将新数据插入到链表的头部每次访问数据也将被访问的数据移动到链表头部当链表满时将链表尾部的数据淘汰 3、链表hashMapLinkedHashMap。当需要插入新的数据项的时候如果新数据项在链表中存在即命中则把该节点移动到链表头部如 果不存在则新建一个节点放到链表头部若缓存满了则把链表最后一个节点删除即可。在访问数据的时候如果数据项在链表中存在则把 该节点移到链表头部否则返回-1这样链表尾部的节点就是最近最少访问的数据项。 分析使用数组需要不停维护数据项的访问时间戳并且在插入数据访问和删除数据不知道数组下标的时候时间复杂度都是O(n)仅使用链表的情况下 在访问定位数据的时间复杂度为O(n)所以一般使用LinkedHashMap的方式。LinkedHashMap的底层就是使用HashMap加双向链表实现的而且本身是有序的 插入和访问顺序相同新插入的元素放入链表的尾部且其有removeEldestEntry方法用于移除最老的元素不过默认返回false表示不移除需要重写此方法当超过map容量时移除最老的元素即可。 LinkedHashMap /*** Returns tttrue/tt if this map should remove its eldest entry.* This method is invoked by ttput/tt and ttputAll/tt after* inserting a new entry into the map. It provides the implementor* with the opportunity to remove the eldest entry each time a new one* is added. This is useful if the map represents a cache: it allows* the map to reduce memory consumption by deleting stale entries.** pSample use: this override will allow the map to grow up to 100* entries and then delete the eldest entry each time a new entry is* added, maintaining a steady state of 100 entries.* pre* private static final int MAX_ENTRIES 100;** protected boolean removeEldestEntry(Map.Entry eldest) {* return size() gt; MAX_ENTRIES;* }* /pre** pThis method typically does not modify the map in any way,* instead allowing the map to modify itself as directed by its* return value. It iis/i permitted for this method to modify* the map directly, but if it does so, it imust/i return* ttfalse/tt (indicating that the map should not attempt any* further modification). The effects of returning tttrue/tt* after modifying the map from within this method are unspecified.** pThis implementation merely returns ttfalse/tt (so that this* map acts like a normal map - the eldest element is never removed).** param eldest The least recently inserted entry in the map, or if* this is an access-ordered map, the least recently accessed* entry. This is the entry that will be removed it this* method returns tttrue/tt. If the map was empty prior* to the ttput/tt or ttputAll/tt invocation resulting* in this invocation, this will be the entry that was just* inserted; in other words, if the map contains a single* entry, the eldest entry is also the newest.* return tttrue/tt if the eldest entry should be removed* from the map; ttfalse/tt if it should be retained.*/protected boolean removeEldestEntry(Map.EntryK,V eldest) {return false;} 构造方法 /*** Constructs an empty ttLinkedHashMap/tt instance with the* specified initial capacity, load factor and ordering mode.** param initialCapacity the initial capacity* param loadFactor the load factor* param accessOrder the ordering mode - tttrue/tt for* access-order, ttfalse/tt for insertion-order* throws IllegalArgumentException if the initial capacity is negative* or the load factor is nonpositive*/public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {super(initialCapacity, loadFactor);this.accessOrder accessOrder;} 自定义LRU实现 package lru;import java.util.LinkedHashMap;
import java.util.Map;/*** LinkedHashMap 实现LRU算法* 〈功能详细描述〉** author 17090889* see [相关类/方法]可选* since [产品/模块版本] 可选*/
public class LRUk, v extends LinkedHashMapk, v {/*** 容量实际能存储多少数据*/private final int MAX_ENTRIES;/*** Math.ceil(cacheSize/0.75f)1 HashMap的initialCapacity* 0.75f 负载因子* accessOrder 排序模式true按照访问顺序进行排序最近访问的放在尾部 false按照插入顺序** param maxEntries*/public LRU(int maxEntries) {super((int) (Math.ceil(maxEntries / 0.75f) 1), 0.75f, true);MAX_ENTRIES maxEntries;}/*** 重写移除最老元素方法* 返回true表示删除最老元素 false 表示不删除** param eldest* return*/Overrideprotected boolean removeEldestEntry(Map.Entryk, v eldest) {// 当实际容量大于指定的容量的时候就自动删除最老的元素链表头部的元素return size() MAX_ENTRIES;}
} 插入测试 LRUString, String lru new LRU(5);lru.put(3, 3);lru.put(1, 1);lru.put(5, 5);lru.put(2, 22);lru.put(3, 33);lru.get(5); 遍历得到的结果1 2 3 5 存储链表结构为 LFU算法 Redis 4.0 引入了一个新的淘汰策略LFULasted Fequently Used 最少频繁使用。按照最近的访问频率进行淘汰比LRU更加精确地表示了一个key被访问的热度。 如果一个key长时间不被访问只是偶尔被访问了一下那么它在LRU算法中就被移动到了链表的头部是不容易被淘汰的因为LRU算法会认为它是一个热点key。 而LFU需要追踪最近一段时间内key的访问频率只有最近一段时间内被访问多次的keyLFU才认为是热点key。 缓存穿透 指查询一个数据库中一定不存在的数据如根据商品编号查询详情首先去查询缓存缓存中自然没有然后去查询数据库如果对这个key的请求 量巨大会直接穿透缓存直接查询数据库给数据库造成很大的压力 解决方案 1、对查询结果为空的情况也进行缓存不过缓存时间设置短一些如60s。如果对该key插入了数据到db之后要清理缓存 2、使用布隆过滤器将所有可能存在数据的key放在布隆过滤器中查询缓存中没有数据之后再使用布隆过滤器进行过滤请求判断查询的key 是否在布隆过滤器中如果不在则直接返回不再查询数据库 缓存击穿 某个key是热点数据扛着高并发请求集中对这个key进行访问避免了访问数据库。那么在这个key失效的瞬间持续的高并发就击穿了缓存直接 请求数据库对数据库造成很大压力。 解决方案 1、热点key缓存永远不过期 不设置过期时间 将过期时间存在key的value中程序判断将要过期时异步线程对改key进行更新 2、互斥锁mutex lock 。当缓存失效的时候线程不是立即去查询数据库而是通过设置锁的方式占坑如Redis的SETNX判断返回值谁拿 到了锁谁去查询数据库然后设置缓存没拿到锁的线程重试get方法。 缓存雪崩 在某个时间段缓存中大量的key集中过期失效 解决方案 1、使用互斥锁 2、数据预热 通过缓存reload机制预先去设置或更新缓存在即将大并发访问前手动在后台触发加载缓存不同的key然后设置不同的过期时间让缓存 过期失效的时间点尽量均匀 3、二级缓存 两个缓存a1为原始缓存a2为备份缓存a1短期a2长期a1过期了再去查询a2 4、缓存永远不过期转载于:https://www.cnblogs.com/yangyongjie/p/10833479.html