如何解决高并发秒杀的问题
https://blog.csdn.net/liangkaiping0525/article/details/80836104
优化方向:
(1)尽量在系统上游拦截请求(不要让锁冲突落在数据库上)。传统的秒杀系统 挂,请求压倒后端数据层,数据读写锁冲突严重,响应缓慢,几乎所有请求都超过 虽然流量大,但成功下单的有效流量很小。以12306为例,一列火车实际上只有2000张票和200张票 0w个人买,基本没人能成功买,请求效率为0。 (2)充分利用缓存,秒杀买票,是典型的读多写少的应用场景。大部分要求都是列车 查询、票务查询、订单和付款是写请求。事实上,一列火车只有2000张票,2000张票w个人买,最 多2000人下单成功,其他人查询库存,写作比例只有0.1%,阅读率占99.9%,非常适合 优化缓存。
如何提高缓存的命中率?
1.通过缓存预加载(预热),尽量关注高频访问、时效性要求低的热点业务。
2.增加存储容量,因为缓存容量有限,容易导致缓存故障和淘汰()
3.调整缓存粒度以提高命中率。缓存粒度越小,命中率越高。当缓存单个对象(如单个用户信息)时,只有当对象对应的数据发生变化时,我们才需要更新缓存或删除缓存。当缓存集合(如所有用户数据)时,任何对象对应的数据都需要更新或删除缓存。
Redis五种用途
- 缓存
- 因为Redis在内存中Set数据结构可以快速高效地增减,性能远高于SQL查询
- 会话Session存储:我看到的Redis最常见的用途是会话存储。与其他会话存储(如Memcache)不同,Redis在缓存停止时,所有数据仍然存在
- 队列:无论是电子邮件队列还是其他应用程序使用的数据,您都可以使用Redis创建一个高效的队列
- pub/sub:
Redis为什么是单线程?
- redis它本身是基于内存操作的,因此每个操作都执行得非常快。如果使用多线程,需要解决多线程同步的问题,这将涉及频繁切换和消耗线程CPU。单线程的使用避免了不必要的上下文切换和竞争条件,不考虑各种锁的问题,不存在锁释放操作,不存在锁可能导致的性能消耗。
- redis数据结构相对简单,数据操作相对较快。
- 多路复用IO,即非阻塞IO。这样提高了redis吞吐量。多路是指可以处理多个网络连接产生的流量,复用是指一个线程可以服务多个线程IO流
缺点:
在多处理器的情况下,不能充分利用其他处理器CPU。解决方案是打开多个解决方案redis通过复制和修改配置文件,可以在多个端口打开多个服务实例redis其他服务实例可以使用CPU来处理连接流。
SDS的设计
分为3部分:
头(包括字符串长度len,free:没用,字符串buf 指针) buf:指向字符串 '/0' SDS与c的区别
Redis字典是如何实现的?
字典是一种非常明显的数据类型: 哈希节点->哈希表->字典 哈希节点(dictEntry): dictEntry是哈希表节点,也就是我们存储数据 其保护成员包括:key,v,next指针。key 保存键值对中键,v保存键值对中值, 值可以是指针或指针uint64_t或者是int64 _t。next指向另一个哈希表节点的指针 一个指针可以连接多个哈希值相同的键 一次,以此来解决哈希冲突的问题 哈希表(dictht) 从源码上看,哈希表包括成员table、size、us ed、sizemask。table它是数组中的每个数组 所有元素都是指向dictEntry结构指针, 每个dictEntry结构保存键对;size 哈希表记录了属性table的大小,而used属性则 记录了哈希表现有节点的数量。sizemask 等于size-计算哈希值的一个键table数组的 索引,即计算index时用到的。 字典(dict) 从源码中看dict结构是字典的定义,包 含的成员有type,privdata、ht、rehashidx。 其中dictType指针类型的type指向操作字典 的api,理解为函数指针,ht是包含2个dic tht字典包含两个哈希表,reh ashidx进行rehash变量,privdata配 合dictType指向函数被用作参数,因此 初步了解字典的几个成员。
讲讲4.0之前版本的Redis单线程运行模式
本质上Redis它不是一个简单的单线程服务模型。持久刷盘、惰性删除等辅助和惰性删除,是由BIO这里提到的单线程主要是指与客户端交互完成命令请求和回复的工作线程。 为什么要用单线程看?redis核心技术与实现
Redis文件事件和时间事件
Redis单线程服务有很多工作要处理,Redis它是由事件驱动的服务器。主要事件类型为:文件事件类型和时间事件类型,其中时间事件是理解单线程逻辑模型的关键。
时间事件:
1.定时事件 等待指定大小的等待时间后执行任务, 执行完成后不再执行,只触发一次; 2.周期事件 任务每隔一定时间就执行,执行完成之后等待 下一次执行将定期触发 Redis大多数是周期性事件,周期事件主要是 定期检测和调整服务器的运行状况, 为了保证稳定性,这项工作主要是ServerCron 函数完成,周期事件的内容主要包括: 1.删除数据库过期key 2.触发RDB和AOF持久化 3.主从同步 4.集群保存 5.关闭并清理死客户端链接 6.统计更新服务器的内存,key数量等信息 Redis时间事件存储在链表中,并且是按照I D新事件存储在头部和尾部,但不存在 按即将执行的顺序存储。
Reactor模式在Redis中的实现
一般流程使用epoll-IO监控描述符的重用机制, 当事件发生时,将事件放在任务队列中。 事件分发后交给分发器 Redis任务事件队列
由于Redis是单线程处理业务,所以IO复用程 如果当前队列同步将读写事件放入队列,序列将读写事件逐一放入队列 列已满,只能出一个进一个,但因为Re dis正常情况下处理得很快,队列满迟不太可能出现 任务延迟,但在执行某些阻塞操作时 会导致长期堵塞,无法处理新任务。 Redis事件分派器
从服务器的角度来看,事件的可读写类型包括: 1.AE_READABLE客户端编写数据,关闭连接,新连接到达 2.AE_WRITEABLE 客户端读取数据 特别是当套接字连接可读可写时,服务 在处理写作事件之前,设备将优先考虑阅读事件,即阅读优先考虑 Redis事件处理器
Redis对文件事件进行分类,编写多个事件处理器函数,包括: 1.连接应答处理器:建立新连接 2.命令请求处理器:处理客户端的新命令 3.命令回复处理器:返回客户端的请求结果 4.复制处理器:主服务器的数据复制
Redis C/S一次完整的交互
Redis此时,服务器的主线程正在循环中Client向R edis如果是6379端口,服务器将发起连接请求进行监控 端口在IO在复用工具下检测到AE_READABLE事件,并 把事件放进去TaskQueue中等待处理,事件分割 派器获取此读取事件,进一步确定是新的连接请求, 将事件交给连接应答处理器建立连接; 建立连接后Client向服务器发送一个get命令, 仍然被IO重用检测处理放入队列,被事件分配器处理 指派给命令请求处理器,并调用相应程序执行 服务器将套接字AE_WRITEABLE事件和命令回复 理器相关,当客户端试图读取结果时,会产生可写的东西 此时,服务器端触发命令回复响应并结数据 如果接字写入套接字,服务端完成后,套接字与生命结束 回复处理器之间的关联;
Redis如何做持久化及其基本原则
一般来说,持久性是将内存中的数据写入非易失介质中,如机械磁盘和SSD
当服务器停机时,作为内存数据库Redis所有数据都会丢失,所以Redis提供两种持久的利器:RDB和AOF
1.RDB以二进制的方式将数据库快照保存到磁盘中 2.AOF以协议文本的形式写下所有数据库 记录进入的命令和参数AOF记录数据库 状态。1.查看RDB配置 [redis@abc]$ cat /abc/redis/conf/redis.conf save 900 1 save 300 10 save 60 10000 dbfilename "dump.rdb" dir "/data/dbs/redis/rdbstro" 前三行都是对触发RDB的一个条件, 如第一行表示每900秒钟有一条数据被修改则 触发RDB,依次类推;只要一条满足就会进行R DB持久化; 第四行dbfilename指定了把内存里的数据库写 入本地文件的名称,该文件是进行压缩后的二 进制文件; 第五行dir指定了RDB二进制文件存放目录 2.RDB的SAVE和BGSAVE RDB文件适合数据的容灾备份与恢复,通过RDB文件 恢复数据库耗时较短,可以快速恢复数据。 RDB持久化只会周期性的保存数据,在未触发下一 次存储时服务宕机,就会丢失增量数据。当数据量 较大的情况下,fork子进程这个操作很消耗cpu, 可能会发生长达秒级别的阻塞情况。 SAVE是阻塞式持久化,执行命令时Redis主进程把 内存数据写入到RDB文件中直到创建完毕,期间Red is不能处理任何命令。 BGSAVE属于非阻塞式持久化,创建一个子进程把内 存中数据写入RDB文件里同时主进程处理命令请求 BGSAVE实现细节: RDB方式的持久化是通过快照实现的,符合条 件时Redis会自动将内存数据进行快照并存储 在硬盘上,以BGSAVE为例,一次完整数据快照 的过程: 1.Redis使用fork函数创建子进程; 2.父进程继续接收并处理命令请求,子进程将 内存数据写入临时文件; 3.子进程写入所有数据后会用临时文件替换旧 RDB文件; 执行fork的时OS会使用写时拷贝策略,对子进程进行快照过程优化。 Redis在进行快照过程中不会修改RDB文件,只 有快照结束后才会将旧的文件替换成新的,也 就是任何时候RDB文件都是完整的。 我们可以通过定时备份RDB文件来实现Redis数 据库备份,RDB文件是经过压缩的,占用的空 间会小于内存中的数据大小。 除了自动快照还可以手动发送SAVE或BGSAVE命 令让Redis执行快照。通过RDB方式实现持久化 ,由于RDB保存频率的限制,如果数据很重要 则考虑使用AOF方式进行持久化。在使用AOF持久化方式时,Redis会将每一个收到的写命令都通过Write函数追加到文件中,类似于MySQL的binlog。换言之AOF是通过保存对redis服务端的写命令来记录数据库状态的
当开启AOF后,服务端每执行一次写操作就会把该条命令追加到一个单独的AOF缓冲区的末尾,然后把AOF缓冲区的内容写入AOF文件里,由于磁盘缓冲区的存在写入AOF文件之后,并不代表数据已经落盘了,而何时进行文件同步则是根据配置的appendfsync来进行配置:
always:服务器在每执行一个事件就把AOF缓冲区的内容强制性的写入硬盘上的AOF文件里 ,保证了数据持久化的完整性,效率是最慢的但最安全的; everysec:服务端每隔一秒才会进行一次文件同步把内存缓冲区里的AOF缓存数据真正写入 AOF文件里,兼顾了效率和完整性,极端情况服务器宕机只会丢失一秒内对Redis数据库的 写操作; no:表示默认系统的缓存区写入磁盘的机制,不做程序强制,数据安全性和完整性差一些 AOF比RDB文件更大,并且在存储命令的过程中增长更快,为了压缩AOF的持久化文件,Redi s提供了重写机制以此来实现控制AOF文件的增长。子进程在进行 AOF 重写期间, 主进程还需要继续处理命令, 而新的命令可能对现有的数据进行修改, 会出现数据库的数据和重写后的 AOF 文件中的数据不一致。 因此Redis 增加了一个 AOF 重写缓存,这个缓存在 fork 出子进程之后开始启用, Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF 文件之外, 还会追加到这个缓存中。 当子进程完成 AOF 重写之后向父进程发送一个完成信号,父进程在接到完成信号之后会调 用信号处理函数,完成以下工作: 1.将 AOF 重写缓存中的内容全部写入到新 AOF 文件中 2.对新的 AOF 文件进行改名,覆盖原有的 AOF 文件 3.AOF重写的阻塞性 整个 AOF 后台重写过程中只有最后写入缓存和改名操作会造成主进程阻塞, 在其他时候AOF 后台重写都不会对主进程造成阻塞, 将 AOF 重写对性能造成的影响降到了最低。Redis的数据恢复
Redis的数据恢复优先级
AOF优先级高于RDB,如果开了AOF就使用AOFRDB和AOF都有各自的缺点:
1.RDB是每隔一段时间持久化一次,故障时就会丢失宕机时刻与上一次持久化之间的数据,无法保证数据完整性 2.AOF存储的是指令序列, 恢复重放时要花费很长时间并且文件更大新型的混合型持久化
创建出一个同时包含 RDB 数据和 AOF 数据的 AOF 文件, 其中 RDB 数据位于 AOF 文件的开头, 它们储存了服务器开始执行重写操作时的数据库状态,至于那些在重写操作 执行之后执行的 Redis 命令, 则会继续以 AOF 格式追加到 AOF 文件的末尾, 也即是 RDB 数据之后。压缩列表ziplist的设计与实现
结构
分为:zlbytes、zltail、zllen、zlentry、zlend 压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,在末尾有一个zlend,中间就是列表节点 zlbytes: 记录了整个压缩列表占用的内存字节数,用来重新分配内存以及计算zlend时使用 zltail:记录尾结点距离压缩列表的起始地址有多少字节 zllen:记录压缩列表的节点数量 zlend: 标记压缩列表的末端 因此在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)实现:
谈谈Redis的skiplist的底层设计和实现
ziplist总体结构
1.zskiplistnode level(包含一个前进指针,一个跨度),后退指针,score(分值),成员对象obj 2.zskiplist: header(指向跳跃表的表头节点),tail(指向跳跃表的表尾节点),level(记录最大的层 数),length(记录跳跃表的长度,节点个数)谈谈对Redis的内存回收机制的理解
存储键值对消耗和本身运行消耗。显然后者我们无法回收,因此只能从键值对下手了,键 值对可以分为几种:带过期的、不带过期的、热点数据、冷数据。对于带过期的键值是需 要删除的,如果删除了所有的过期键值对之后内存仍然不足怎么办?那只能把部分数据给 踢掉了key-value都是存储在redisDb的dict中的
对于键,我们可以设置绝对和相对过期时间、以及查看剩余时间: 1.使用EXPIRE和PEXPIRE来实现键值对的秒级和毫秒级生存时间设定,这是相对时长的过期设置 2.使用EXPIREAT和EXPIREAT来实现键值对在某个秒级和毫秒级时间戳时进行过期删除,属于绝对过期设置 3.通过TTL和PTTL来查看带有生存时间的键值对的剩余过期时间键值对的过期删除判断 判断键是否过期可删除,需要先查过期字典是否存在该值,如果存在则进一步判断过期时 间戳和当前时间戳的相对大小,做出删除判断
1.定时删除:在设置键的过期时间的同时,创建定时器,让定时器在键过期时间到来时, 即刻执行键值对的删除; 2.定期删除:每隔特定的时间对数据库进行一次扫描,检测并删除其中的过期键值对; 3.惰性删除:键值对过期暂时不进行删除,至于删除的时机与键值对的使用有关,当获取 键时先查看其是否过期,过期就删除,否则就保留; 三种策略都有各自的优缺点:定时删除对内存使用率有优势,但是对CPU不友好,惰性删除 对内存不友好,如果某些键值对一直不被使用,那么会造成一定量的内存浪费,定期删除 是定时删除和惰性删除的折中。 Reids采用的是惰性删除和定期删除的结合,一般来说可以借助最小堆来实现定时器,不过 Redis的设计考虑到时间事件的有限种类和数量,使用了无序链表存储时间事件,这样如果 在此基础上实现定时删除,就意味着O(N)遍历获取最近需要删除的数据。如果执行频率太少就退化为惰性删除了,如果执行时间太长又和定时删除类似了 1.该算法是个自适应的过程,当过期的key比较少时那么就花费很少的cpu时间来处理,如 果过期的key很多就采用激进的方式来处理,避免大量的内存消耗,可以理解为判断过期键 多就多跑几次,少则少跑几次; 2.由于Redis中有很多数据库db,该算法会逐个扫描,本次结束时继续向后面的db扫描,是 个闭环的过程; 3.定期删除有快速循环和慢速循环两种模式,主要采用慢速循环模式,其循环频率主要取 决于server.hz,通常设置为10,也就是每秒执行10次慢循环定期删除,执行过程中如果耗 时超过25%的CPU时间就停止; 4.慢速循环的执行时间相对较长,会出现超时问题,快速循环模式的执行时间不超过1ms, 也就是执行时间更短,但是执行的次数更多,在执行过程中发现某个db中抽样的key中过期 key占比低于25%则跳过; DEL删除键值对,在Redis4.0之前执行del操作时如果key-value很大,那么可能导致阻塞, 在新版本中引入了BIO线程以及一些新的命令,实现了del的延时懒删除,最后会有BIO线程 来实现内存的清理回收。内存淘汰机制
1.noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错; 2.allkeys-lru:当内存不足以容纳新写入数据时,在键空间中移除最近最少使用的 key; 3.allkeys-random:当内存不足以容纳新写入数据时,在键空间中随机移除某个 key; 4.volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key; 5.volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key; 6.volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除; 后三种策略都是针对过期字典的处理,但是在过期字典为空时会noeviction一样返回写入 失败,毫无策略地随机删除也不太可取,所以一般选择第二种allkeys-lru基于LRU策略进 行淘汰。谈谈对Redis数据同步机制和原理的理解
理解持久化和数据同步的关系,需要从单点故障和高可用两个角度来分析:
主从复制 集群等来回答