Redis单线程为什么这么快
Redis基于Reactor模式开发了网络事件处理器、文件事件处理器。它是单线程的,所以Redis才叫做单线程的模型(后台也有其他线程,之所以称为单线程,是因为里面的网络事件处理器这些是单线程),采用IO多路复用机制来同时监听多个socket,根据socket上的事件类型来选择对应的事件处理器来处理事件,可以实现高性能的网络通信模型,又可以跟内部其他单线程模块对接,保证了redis内部的线程模型的简单性
文件事件处理器:包含多个socket、io多路复用程序、文件事件分派器,以及事件处理器
多个Socket可能并发产生不同事件,io多路复用程序会监听多个socket,会将socket放入一个队列中排队,每次从队列中有序、同步取出一个socket给事件分派器,事件分派器把Socket给对应的事件处理器,然后当一个socket事件处理完之后,IO多路复用程序才会将队列中的下一个Socket给事件分派器,文件事件分派器会根据每个socket当前产生事件,来选择对应的事件处理器来处理
单线程快的原因:
- 纯内存操作
- 核心是基于非阻塞的IO多路复用机制
- 单线程反而避免了多线程的频繁上下文切换带来的性能问题
redis持久化机制
RDB:redis database 将某一个时刻的内存快照,以二进制的方式写入磁盘
-
手动触发
- save命令,使得Redis处于阻塞状态,直到RDB持久化完成,才会响应其他客户端发来的命令,所以在生产环境一定要慎用
- bgsave命令:fork出一个子进程执行出就话,主进程只有在fork过程有短暂的阻塞,子进程创建之后,主进程就可以响应客户端请求了(如何保证子进程的正确性:cow,copy on write,写入时复制(Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时要求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。)
-
自动触发:
- save m n:在m秒内,如果有n个键发生改变,则自动触发持久化,通过bgsave执行,如果设置多个,只要满足其一就会触发,配置文件有默认配置(可以注释掉)
- flushall: 用于清空redis所有的数据库,flushdb清空当前redis所在库数据,默认是零号数据,会清空RDB文件,同时会生成dump.rdb(内容为空)
- 主从同步:全量同步时会自动触发bgsave命令,生成rdb发送给从节点
优点:
- 整个Redis数据库将只包含一个文件dump.rdb,方便持久化
- 容灾性好,方便备份
- 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化,使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis高性能
- 相对于数据集大时,比AOF启动效率高
缺点:
- 数据安全性低,RDB时间隔一段时间进行持久化,如果持久化之间发生故障,会丢失数据,所以这种方式更加适合数据要求不严谨的时候
- 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此当数据集较大时,可能会导致整个服务器停止服务几百毫秒甚至一分钟,会占用CPU
AOF:Append Only File ,以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录,调操作系统命令进程刷盘
- 所有的写命令会追加到AOF缓冲中
- AOF缓冲区根据对应的策略向硬盘进行同步操作
- 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩目的
- 当Redis重启时,可以加载AOF文件进行数据恢复
同步策略:
- 每秒同步:异步完成,效率非常高,一旦系统出现宕机现象,那么这一秒钟内修改的数据都将会丢失
- 每修改同步:同步持久化,每次发生的数据变化都会被立刻记录到磁盘中,最多丢一条
- 不同步:由操作系统控制,可能丢失较多数据
优点:
- 数据安全
- 通过Append模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过redis-check-aof工具解决数据一致性问题
- AOF机制的rewrite模式,定期对AOF文件重写,达到压缩目的
缺点:
- AOF文件比RDB文件大,且恢复速度慢
- 数据集大的时候,比RDB启动效率低
- 运行效率没有RDB高
简述Redis主从同步机制
- 从节点执行slaveof masterip port,保存主节点信息
- 从节点中的定时任务发现主节点信息,建立和主节点的socket链接
- 从节点发送信号,主节点返回,两边能互相通信
- 连接建立后,主节点将所有数据发送给从节点(数据同步)
- 主节点把当前数据同步给从节点后,便完成了复制过程,接下来主节点会持续地把写命令发送给从节点,保证主从数据一致
runId:每个redis节点启动都会生成唯一的uuid,每次redis重启后,runId都会发生变化
offset:主从节点各自维护自己的复制偏移量offset,当主节点有写入命令时,offset=offset+命令字节长度,从节点收到主节点发送的命令后,也会增加自己的offset,并把自己的offset发送给主节点,主节点同时保存自己的offset和从节点的offset来判断主从节点是否一致
redis高可用方案
redis最开始使用主从模式做集群,若master宕机需要手动配置slave转为master;后来为了高可用提出来哨兵模式,该模式下有一个哨兵监视master和slave,若master宕机可自动将slave转为master,但它也有一个问题,就是不能动态扩充;所以在3.x提出cluster集群模式。
哨兵模式:
- 集群监控:负责监控redis 主从进程是否正常工作
- 消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为警报通知给管理员
- 故障转移:如果master node挂掉了,会自动转移到slave nde上
- 配置中信:如果故障转移发生,会通知客户端新的主节点地址
哨兵用于实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工资
- 故障转移时,判断主节点是否宕机,需要大部分哨兵同意才行,也就是分布式选举
- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作
- 哨兵通常需要三个实例来保证自己的健壮性
- 哨兵+redis主从的部署,不保证数据零丢失,只能保证redis高可用
redis cluster:采用slot(槽)的概念,一共分成16384个槽,将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行。
方案说明:
- 通过哈希的方式,将数据分片,每个节点均分存储一定哈希区间的数据
- 每份数据分片会存储在多个互为主从的多节点
- 数据写入先写主节点,再同步到从节点
- 同一分片多个节点的数据不保持强一致性
- 读取数据的时候,当客户端操作的key没有分配到该节点上时,redis会返回转向指令,指向正确的节点
- 扩容时需要把旧节点的数据迁移一部分到新节点
在redis cluster架构下,每个redis要开放两个端口,比如一个是6379,另一个就是加1W的端口号,比如16379
16379端口号是用来节点间通信的,也就是cluster bus的通信,用来进行故障检测,配置更新,故障转移授权,采用了二进制协议,占用更少的网络带宽与处理时间
优点:
- 无中心架构,支持动态扩容,对业务透明
- 具备监控和故障转移能力
- 客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
- 高性能,客户端直接连redis服务,免去proxy代理的损耗
缺点:
- 运维复杂,数据迁移需要人工干预
- 只能使用0号数据库
- 不支持批量操作
- 分布式逻辑和存储模块耦合等
redis与mysql
mysql是经典的关系型数据库,而redis是非关系型数据库。
这两者的主要差异在于查询,redis只能用key去获取value,不支持sql语句来进行查询。而mysql则只能通过sql语句来进行查询,不能直接通过key来获取value。
因为redis的数据读取过程的时间复杂度是O(1),也就是说和数据量无关。再加上数据保存在内存,所以读取速度在理论上已经达到了上限。而mysql的查询则是通过扫描表来进行,读取的速度取决于数据量,以及是否有合适的索引。
MySQL体积小、速度快、成本低、结构稳定、便于查询,可以保证数据的一致性,但缺乏灵活性。 NoSQL高性能、高扩展、高可用,不用局限于固定的结构,减少了时间和空间上的开销,却又很难保证数据一致性。
Redis过期键的删除策略
惰性过期:只有当访问一个key时,才会判断key是否已经过期,过期则清除,该策略可以最大化节省cpu资源,但是对内存不友好
定期过期:每隔一定时间,会扫描一定数量的数据库的expires字典中的一定数量的key,并清除其中已经过期的key,通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得cpu和内存资源达到最优的平衡效果
缓存雪崩、缓存穿透、缓存击穿
缓存雪崩:指缓存同一时间大面积失效,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉
解决方案:
- 给缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生
- 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存
- 缓存预热
- 互斥锁
缓存穿透:指缓存和数据库中都没有对应数据,导致所有请求都落在数据库上,数据库短时间承受大量请求而崩掉
解决方案:
- 接口层增加校验,比如用户鉴权校验,id做基础校验,id<=0直接拦截
- 从缓存取不到的数据,在数据库中也取不到,可以将key-value设置为key-null,缓存有效时间可以设置短点,(时间太长会导致正常情况也没法使用),可以防止攻击用户反复用同一个id暴力攻击
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被bitmap过滤,避免了对底层存储系统的查询压力
缓存击穿:缓存中没有但数据库中有的数据(一般是缓存时间到期),这时候由于并发用户特别多,同时读缓存没有读到数据,又同时去数据库取数据,引起数据库压力增大,与缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库
解决方案:
- 设置热点数据永不过期
- 加互斥锁
redis与数据库一致性问题
先更新数据库,再更新缓存
问题1:如果先更新数据库,然后再去更新缓存失败时,则将导致数据库是新数据,而缓存的数据还是老数据,出现了数据不一致性的问题
解决:事务,更新缓存失败则数据库也失败,达到数据一致性
问题2:线程A先更新数据库,然后再更新缓存,这个时候线程B也更新数据库,由于网络延迟,线程B在线程A之前更新缓存,线程A最后更新缓存,则缓存中的结果和数据库的结果不一致
解决:互斥锁/队列+事务
先更新数据库,再删除缓存
问题1:删除缓存失败,则缓存是老数据 解决:事务特性
问题2:缓存失效,线程A读,从数据库中读取数据,接着线程B写操作且使缓存失效,再接着线程A将读到的数据写入缓存,则缓存是老数据
解决:互斥锁/队列+事务特性
先删除缓存,再修改数据库
问题:删完缓存,线程把老数据再读取到缓存,然后再更新数据库,缓存中依然是老数据。
解决方案1:延时双删:先删除缓存,再修改数据库,修改完数据库之后,再延迟删除一次缓存
进一步问题:
- 1、第二次删除,如果失败,那么缓存中的数据依旧是老数据
- 2、无法确定延迟多久
- 3、存在一段时间的数据不一致
解决方案2:串行化:先删除,再更新,再读,保证缓存是从数据库最新获取的
进一步问题:
- 1、串行化会降低系统吞吐量
- 2、处理时间拉长:用户读取数据必须等待更新完毕之后才能读取得到
先更新缓存,再修改数据库
问题1:更新完毕缓存(即使存在事务),再修改数据库失败,则缓存与数据不一致。
问题2:线程A更新缓存,线程B更新缓存,线程B更新数据库,线程A更新数据库。导致缓存不一致。缓存中的数据是新的,但数据库中的数据是老的
这个问题可以用互斥锁/队列解决,但是无法保证更新db失败。更新db失缓存无法撤销更新,所以无解!或许可以先记录之前的缓存数据,然后更新数据库失败再回退缓存数据,但是这样的复杂性操作还不如直接【先更新数据库】然后利用事务来达到效果方便,没必要做这样的无用功,所以算是无解!
总结
- 如果数据库不采用事务,那么第一的方案是「先删除缓存,再更新数据库」;
- 如果数据库采用了事务,但是是读的场景为主,那么方案优先次序为:「先更新数据库,再更新缓存」、「先更新数据库,再删除缓存」、「先删除缓存,再更新数据库」
- 如果数据库采用了事务,但是是写的场景为主,那么方案优先次序为:「先更新数据库,再删除缓存」、「先更新数据库,再更新缓存」、「先删除缓存,再更新数据库」(之所以把「先更新数据库,再更新缓存」放在「先删除缓存,再更新数据库」前面是因为后者既对代码侵入性、和提高复杂,而且效率降低(在维护redis穿透和击穿时需要互斥),而前者只是效率降低)
- 无论哪种情况都不应该采取的方案:「先更新缓存,再更新数据库」\