https://blog.csdn.net/liangkaiping0525/article/details/80836104
优化方向:
(1)将请求尽量拦截在系统上游(不要让锁冲突落到数据库上去)。传统秒杀系统之所以
挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超
时,流量虽大,下单成功的有效流量甚小。以12306为例,一趟火车其实只有2000张票,20
0w个人来买,基本没有人能买成功,请求有效率为0。
(2)充分利用缓存,秒杀买票,这是一个典型的读多写少的应用场景,大部分请求是车次
查询,票查询,下单和支付才是写请求。一趟火车其实只有2000张票,200w个人来买,最
多2000个人下单成功,其他人都是查询库存,写比例只有0.1%,读比例占99.9%,非常适合
使用缓存来优化。
1.尽可能的聚焦在高频访问且时效性要求不高的热点业务上,通过缓存预加载(预热)
2.增加存储容量,因为缓存的容量有限,则容易引起缓存失效和被淘汰(LRU算法)
3.调整缓存粒度等手段来提高命中率。缓存的粒度越小,命中率会越高,当缓存单个对象的时候(例如:单个用户信息),只有当该对象对应的数据发生变化时,我们才需要更新缓存或者让移除缓存。而当缓存一个集合的时候(例如:所有用户数据),其中任何一个对象对应的数据发生变化时,都需要更新或移除缓存。
缺点:
就是在多处理器情况下,不能充分利用其他CPU。可以的解决方法是开启多个redis服务实例,通过复制和修改配置文件,可以在多个端口上开启多个redis服务实例,这样就可以利用其他CPU来处理连接流。
分为3部分:
头部(包含字符串长度len,free:未用,字符串buf
指针)+buf:指向字符串+'/0'
SDS与c的区别
字典是个层次非常明显的数据类型:
哈希节点->哈希表->字典
哈希节点(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-1和哈希值计算一个键在table数组的
索引,也就是计算index时用到的。
字典(dict)
从源码中看到dict结构体就是字典的定义,包
含的成员有type,privdata、ht、rehashidx。
其中dictType指针类型的type指向了操作字典
的api,理解为函数指针即可,ht是包含2个dic
tht的数组,也就是字典包含了2个哈希表,reh
ashidx进行rehash时使用的变量,privdata配
合dictType指向的函数作为参数使用,这样就
对字典的几个成员有了初步的认识。
本质上Redis并不是单纯的单线程服务模型,一些辅助工作比如持久化刷盘、惰性删除等任务是由BIO线程来完成的,这里说的单线程主要是说与客户端交互完成命令请求和回复的工作线程。
为什么使用单线程看redis核心技术与实现
Redis作为单线程服务要处理的工作一点也不少,Redis是事件驱动的服务器,主要的事件类型就是:文件事件类型和时间事件类型,其中时间事件是理解单线程逻辑模型的关键。
时间事件:
1.定时事件
任务在等待指定大小的等待时间之后就执行,
执行完成就不再执行,只触发一次;
2.周期事件
任务每隔一定时间就执行,执行完成之后等待
下一次执行,会周期性的触发
Redis中大部分是周期事件,周期事件主要是
服务器定期对自身运行情况进行检测和调整,
从而保证稳定性,这项工作主要是ServerCron
函数来完成的,周期事件的内容主要包括:
1.删除数据库的过期key
2.触发RDB和AOF持久化
3.主从同步
4.集群化保活
5.关闭清理死客户端链接
6.统计更新服务器的内存、key数量等信息
Redis的时间事件是存储在链表中的,并且是按照I
D存储的,新事件在头部旧事件在尾部,但是并不
是按照即将被执行的顺序存储的。
大致流程使用epoll-IO复用机制进行监控描述符,
当有事件到来,就将事件放到一个任务队列中,然
后交由分发器进行事件的分发
Redis的任务事件队列
由于Redis的是单线程处理业务的,因此IO复用程
序将读写事件同步的逐一放入队列中,如果当前队
列已经满了,那么只能出一个入一个,但是由于Re
dis正常情况下处理得很快,不太会出现队列满迟
迟无法放任务的情况,但是当执行某些阻塞操作时
将导致长时间的阻塞,无法处理新任务。
Redis事件分派器
事件的可读写是从服务器角度看的,分派看到的事件类型包括:
1.AE_READABLE客户端写数据、关闭连接、新连接到达
2.AE_WRITEABLE 客户端读数据
特别地,当一个套接字连接同时可读可写时,服务
器会优先处理读事件再处理写事件,也就是读优先
Redis事件处理器
Redis将文件事件进行归类,编写了多个事件处理器函数,其中包括:
1.连接应答处理器:实现新连接的建立
2.命令请求处理器:处理客户端的新命令
3.命令回复处理器:返回客户端的请求结果
4.复制处理器:实现主从服务器的数据复制
Redis服务器的主线程处于循环中,此时Client向R
edis服务器发起连接请求,假如是6379端口,监听
端口在IO复用工具下检测到AE_READABLE事件,并
将该事件放入TaskQueue中,等待被处理,事件分
派器获取这个读事件,进一步确定是新连接请求,
就将该事件交给连接应答处理器建立连接;
建立连接后Client向服务器发送了一个get命令,
仍然被IO复用检测处理放入队列,被事件分派器处
理指派给命令请求处理器,调用相应程序进行执行
服务器将套接字的AE_WRITEABLE事件与命令回复处
理器相关联,当客户端尝试读取结果时产生可写事
件,此时服务器端触发命令回复响应,并将数据结
果写入套接字,完成之后服务端结束该套接字与命
令回复处理器之间的关联;
通俗讲持久化就是将内存中的数据写入非易失介质中,比如机械磁盘和SSD
在服务器发生宕机时,作为内存数据库Redis里的所有数据将会丢失,因此Redis提供了持久化两大利器:RDB和AOF
1.RDB将数据库快照以二进制的方式保存到磁盘中
2.AOF以协议文本方式,将所有对数据库进行过写
入的命令和参数记录到AOF文件,从而记录数据库
状态。
RDB相关知识
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详解
在使用AOF持久化方式时,Redis会将每一个收到的写命令都通过Write函数追加到文件中,类似于MySQL的binlog。换言之AOF是通过保存对redis服务端的写命令来记录数据库状态的
当开启AOF后,服务端每执行一次写操作就会把该条命令追加到一个单独的AOF缓冲区的末尾,然后把AOF缓冲区的内容写入AOF文件里,由于磁盘缓冲区的存在写入AOF文件之后,并不代表数据已经落盘了,而何时进行文件同步则是根据配置的appendfsync来进行配置:
appendfsync选项:always、everysec和no:
always:服务器在每执行一个事件就把AOF缓冲区的内容强制性的写入硬盘上的AOF文件里
,保证了数据持久化的完整性,效率是最慢的但最安全的;
everysec:服务端每隔一秒才会进行一次文件同步把内存缓冲区里的AOF缓存数据真正写入
AOF文件里,兼顾了效率和完整性,极端情况服务器宕机只会丢失一秒内对Redis数据库的
写操作;
no:表示默认系统的缓存区写入磁盘的机制,不做程序强制,数据安全性和完整性差一些
AOF比RDB文件更大,并且在存储命令的过程中增长更快,为了压缩AOF的持久化文件,Redi
s提供了重写机制以此来实现控制AOF文件的增长。
AOF重写
子进程在进行 AOF 重写期间, 主进程还需要继续处理命令,
而新的命令可能对现有的数据进行修改, 会出现数据库的数据和重写后的 AOF
文件中的数据不一致。
因此Redis 增加了一个 AOF 重写缓存,这个缓存在 fork 出子进程之后开始启用,
Redis 主进程在接到新的写命令之后, 除了会将这个写命令的协议内容追加到现有的 AOF
文件之外, 还会追加到这个缓存中。
当子进程完成 AOF 重写之后向父进程发送一个完成信号,父进程在接到完成信号之后会调
用信号处理函数,完成以下工作:
1.将 AOF 重写缓存中的内容全部写入到新 AOF 文件中
2.对新的 AOF 文件进行改名,覆盖原有的 AOF 文件
3.AOF重写的阻塞性
整个 AOF 后台重写过程中只有最后写入缓存和改名操作会造成主进程阻塞,
在其他时候AOF 后台重写都不会对主进程造成阻塞, 将 AOF
重写对性能造成的影响降到了最低。
Redis的数据恢复优先级
AOF优先级高于RDB,如果开了AOF就使用AOF
RDB和AOF都有各自的缺点:
1.RDB是每隔一段时间持久化一次,故障时就会丢失宕机时刻与上一次持久化之间的数据,无法保证数据完整性
2.AOF存储的是指令序列, 恢复重放时要花费很长时间并且文件更大
新型的混合型持久化
创建出一个同时包含 RDB 数据和 AOF 数据的 AOF 文件, 其中 RDB 数据位于 AOF
文件的开头, 它们储存了服务器开始执行重写操作时的数据库状态,至于那些在重写操作
执行之后执行的 Redis 命令, 则会继续以 AOF 格式追加到 AOF 文件的末尾, 也即是
RDB 数据之后。
结构
分为:zlbytes、zltail、zllen、zlentry、zlend
压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,在末尾有一个zlend,中间就是列表节点
zlbytes: 记录了整个压缩列表占用的内存字节数,用来重新分配内存以及计算zlend时使用
zltail:记录尾结点距离压缩列表的起始地址有多少字节
zllen:记录压缩列表的节点数量
zlend: 标记压缩列表的末端
因此在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)
实现:
ziplist总体结构
1.zskiplistnode
level(包含一个前进指针,一个跨度),后退指针,score(分值),成员对象obj
2.zskiplist:
header(指向跳跃表的表头节点),tail(指向跳跃表的表尾节点),level(记录最大的层
数),length(记录跳跃表的长度,节点个数)
回收的内存从哪里来
存储键值对消耗和本身运行消耗。显然后者我们无法回收,因此只能从键值对下手了,键
值对可以分为几种:带过期的、不带过期的、热点数据、冷数据。对于带过期的键值是需
要删除的,如果删除了所有的过期键值对之后内存仍然不足怎么办?那只能把部分数据给
踢掉了
如何实施过期键值对的删除
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策略进
行淘汰。
理解持久化和数据同步的关系,需要从单点故障和高可用两个角度来分析:
主从复制 集群等来回答