[redis] 集群
redis集群
redis集群
六、场景 1.缓存 Redis由于性能高效,通常可以做数据库存储的缓存,比如给Mysql做缓存 通常业务都满足二八原则,80%的流量在20%的热点数据上,所以缓存可以很大程度提高系统的吞吐量 1.1缓存基础 一般而言,缓存分为服务器缓存,客户端缓存 缓存一般有以下几种模式: 旁路缓存模式: 读穿透模式: 写穿透模式: 异步缓存写入模式: 旁路缓存模式(适用于读多写少) Cache Aside,旁路缓存模式,是最常见的模式,应用服务把缓存当作数据库的旁路,直接和缓存交互 读操作:服务端收到查询请求,先查询数据是否在缓存上,如果在,就用缓存数据直接打包返回,如果不存在,就去数据库查询,并放到缓存 写操作:cache aside模式一般先更新数据库,再直接删除缓存(更新相比删除更容易造成时序性问题) 适用于读多写少的场景,缺点是可能会出现缓存和数据库不一致的情况 这里的写操作,更新相比删除更容易造成时序性问题,具体举例:线程1更新mysql -> 线程2更新mysql -> 线程2更新缓存 -> 线程1更新mysql,这样就出现了时许性问题 该模型的缺点: 可能出现缓存和数据库不一致的情况,具体见:数据库和缓存如何保证一致性? 读穿透模式 与cache aside模式的区别主要在应用服务不再与缓存直接交互,而是直接去访问数据服务。 这里的数据服务理解为一个**代理服务,**用它来访问缓存和数据库 相比于旁路缓存模式,读穿透模式的优势是缓存对业务是透明的;缺点是缓存命中的性能不如旁路缓存模式,会多一层服务调用 写穿透模式 WriteThrough做了一层封装:有缓存服务先写入Mysql,再同步写入Redis,这样及时加载或更新了缓存数据(理解为,应用程序由一个单独的访问源,而存储服务自己维护访问逻辑) 在使用WriteThrough时,一般都配合使用ReadThrough来使用 适用情况: 对缓存及时性要求更高 不能忍受数据丢失和数据不一致 异步缓存写入模式(Write-Behind) write-Behind和Write-Through相同点是都是写入时会更新数据库、也会更新缓存 不同点是:Write-Behind是先写缓存,后异步把数据一起写入数据库 数据库写操作可以用不同的方式完成: 收集写操作并在某个时间点慢慢写入 合并几个写操作成为一个批量操作,一起批量写入 异步写操作极大降低了请求延迟,并减轻了数据库的负担,但是代价是安全性不够,如果缓存中的数据还没写入数据库,存储服务发生了崩溃,那么数据就丢失了 1.2缓存异常 缓存穿透 问题背景 缓存穿透是指**缓存和数据库都没有的数据,**而用户不断发起请求。 在流量大的时候,DB可能就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞 解决方案 接口层增加校验,如用户鉴权校验,id做击穿校验,id<=0的直接拦截 从缓存取不到的数据,在数据库中也没有取到,这时可以将key-value对写成key-null,缓存有效时间写短点,例如30s 布隆过滤器:bloomfilter类似于一个hash set,用于快速判断某个元素是否存在于集合中,关键在于hash算法和容器大小 布隆过滤器: 原理:布隆过滤器底层是一个64位的整型,将字符串用多个Hash函数映射不同的二进制位置,将整型中对应位置设置为1 优点:空间、时间消耗都很小 缺点:结果不完全准 缓存击穿 问题背景 缓存击穿是指缓存中没有但数据库中有的数据(一般缓存时间到期),这时由于并发的用户过多,同时读缓存没有数据又同时查询数据库,引起数据库压力瞬时增大 解决方案 热点数据支付续期,持续访问的数据不断续期,避免因为过期失效而被击穿 发现缓存失效,重建缓存加互斥锁,当线程查询缓存发现缓存不存在就会尝试加锁,线程抢锁,拿到锁的线程进行查询数据库,然后重建缓存 缓存雪崩 问题背景 指大量的应用请求因为异常无法在Redis缓存中处理,直接打到数据库。这里的异常就是:缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚者宕机 ...
五、持久化 0.持久化介绍 redis是跑在内存里的,当程序重启或者服务崩溃,数据就会丢失,所以需要持久化,即把数据保存到可永久保存的存储设备中 持久化方法 redis提供两种方式来持久化: 1.RDB(Redis Database) 记录Redis某个时刻的全部数据,这个方法的本质就是数据快照,直接保存二进制数据到磁盘,后续通过加载RDB文件恢复数据 2.AOF(Append Only File) 记录执行的每条命令,重启之后通过重放命令来恢复数据,AOF是记录操作日志,后续通过日志重放恢复数据 两种持久化方法的对比(*) 上面两种持久化方法对比:RDB(快照恢复)和AOF(日志恢复) 体积方面:相同数据量下,RDB体积小,因为RDB记录的是二进制紧凑型数据 恢复速度方面:RDB是数据快照,可以直接加载,而AOF文件恢复,相当于重放情况,RDB显然更快 数据完整性:AOF记录了每条日志,RDB是间隔一段时间记录一次,用AOF恢复数据通常会更加完整 RDB好还是AOF好 业务本身需要的是缓存数据并且不是一个海量访问,可以不用开持久化 对数据本身十分重视,可以同时开启RDB和AOF,注意,在同时开启的情况下,只会用AOF来加载,如果只有RDB文件而没有AOF文件,不会用RDB文件去恢复数据,如果逻辑是你自主开启选择AOF,表明要强一点的一致性,但是AOF文件缺失,此时不会去使用RDB,业务RDB会少很多数据,此时启动是一个空库 1.RDB RDB文件的内容是**二进制数据,**记录的是某一瞬间的内存数据,是实际的数据,也叫做快照 在Redis恢复数据时,RDB恢复数据的效率会比AOF高,因为会直接读取RDB文件到内存即可,不需要像AOF那样还需要额外执行操作命令的步骤才能恢复数据 1.1 RDB怎么使用 Redis提供两个命令来使用RDB文件,分别是save 和bgsave save :执行了save命令,会在主线程生成RDB文件,由于和执行操作命令在同一个线程,所以写入RDB文件的时间太长,会阻塞主线程 bgsave :执行了bgsave命令,会创建一个子进程来生成RDB文件,这样可以避免主线程的阻塞 RDB文件的加载工作是在服务器启动时自动执行,Redis并没有提供专门加载RDB文件的命令 在Redis的配置文件中,有以下的选项来实现每隔一段时间自动执行一次bgsave命令 # 这里写的是save,实际上是bgsave save 900 1 # 900s之内,对数据库进行至少1次修改 save 300 10 # 300s之内,对数据库进行至少10次修改 save 60 10000 # 60s之内,对数据库进行至少10000次修改 Redis的快照是全量快照,也就是说每次执行快照,都是把内存中的所有数据记录到磁盘中 RDB快照的缺点:当服务器发送故障时,丢失的数据会比AOF更多 1.2 执行快照时(bgsave),修改数据会发生什么? 在执行bgsave时,主线程是可以继续执行操作命令,由子线程来构建RDB文件,所以内存里的数据是可以修改的 实现这一功能靠的是写时复制****技术 流程如下: 执行bgsave命令时,会fork()创建子进程,此时子进程和父进程是共享同一片内存数据 主进程创建子进程时,子进程会复制父进程的页表,但是页表指向的物理内存跟主进程是同一个物理内存 此时如果主线程由写命令的执行,就会发生写时复制,物理内存才会被复制一次 发生写时复制后,RDB快照保存的是原本的内存数据 这是因为创建bgsave子进程后,由于父子进程共享所有内存数据,所以可以直接将数据写入到RDB文件 当主进程对共享的内存数据是只读****操作,那么主进程和bgsave子进程是相互不影响 当主进程对共享的内存数据进行写操作时,就会发生写时复制,这块数据的物理内存就会被复制一份,然后主线程在这个数据副本进行写操作,此时bgsave子进程继续把原来的数据(原物理内存)写入到RDB文件 执行bgsave时,在极端情况下,如果所有的共享内存都被修改,则此时的内存占用是原先的两倍 1.3 什么时候执行RDB持久化 主动执行命令save 主动执行命令bgsave ...
四、执行流程 内存结构 核心执行是单线程 多线程负载一些异步任务 1.Redis在内存中是怎么存储的 redis是内存存储,将数据放在redis时,都是以键值对形式存到内存 数据库结构 redisDb代表Redis数据库结构,各种操作对象,都是存储在dict数据结构里 // redisDb 结构 type struct redisDb { dict *dict; //字典 dict *expires; // 过期键 dict *blocking_keys; dict *ready_keys; dict *watched_keys; int id; long long avg_ttl; list *defrag_later; } redisDb; // dict 结构 typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; unsigned long iterators; } dict; redisDb即数据库对象,指向了数据字典,字典包含我们平常存储的k-v数据,v支持任意redis对象 在增加、查询、更新、删除的操作后,分别在内存存储是怎么体现的? 增删改查在Redis内存中的体现 添加数据 即添加键值对,添加到dict结构字典中,Key必须为String对象,value为任何类型的对象 添加数据后,会在redisDb里字段dict上添加dict对象 查询数据 直接在dict找到对应的key,即完成查询 更新数据 对已经Key对象的任何变更操作,都是更新 删除数据 删除即把key和value从dict结构里删除 过期键 Redis可以设置过期键,到达一定时间,这些对象会被自动过期并回收 **过期键存储在expires字典上,**expires字典中,value就是过期时间 在redisDb中,dict和expires中Key对象,实际都是存储String对象指针,两个的key都会指向内存相应的字符串地址 2.Redis是单线程?还是多线程? redis是一个能高效处理请求的组件 核心处理逻辑,Redis一直都是单线程,其他辅助模块会有一些多线程、多进程的功能,例如:复制模块用的多进程;某些异步流程从4.0开始用多线程;网络I/O解包从6.0开始用多线程; 核心处理逻辑:Redis在处理客户端的请求时,包括获取(socket写)、解析、执行、内容返回等都是由一个顺序串行的主线程处理,这就是所谓的单线程 Redis为什么选择单线程 redis的定位是内存k-v存储,是做短平快的热点数据处理,一般来说执行会很快,执行本身不会成为瓶颈,瓶颈通常在网络I/O,处理逻辑多线程并不会有太大收益 同时Redis本身秉持简洁高效的理念,代码的简单性、可维护性是redis一直依赖的追求,执行本身不应该成为瓶颈,而且多线程本身也会引起额外成本 1.多线程引入的复杂度是极大的 多线程引入后,redis原来的顺序执行特性就不复存在,为了事务的原子性、隔离性,redis就不得不引入一些很复杂的实现 redis的数据结构是极其高效,在单线程模式下做了很多特性的优化,如果引入多线程,那么所有底层数据都要改为线性安全,这很复杂 多线程模式使得程序调试更加复杂和麻烦,会带来额外的开发成本及运营成本 2.多线程带来额外的成本 除了引入复杂度,多线程还会带来额外成本,包括 上下文切换成本,多线程调度需要切换线程上下文,这个操作先存储当前线程的本地数据,程序指针,然后载入另一个线程数据,这种内核操作的成本不可忽略 同步机制的开销,一些公共资源,在单线程模式下直接访问就行,多线程需要通过加锁等方式进行同步 一个线程本身也占据内存大小,对redis这种内存数据库来说,内存非常珍贵,多线程本身带来的内存使用的成本也需要谨慎决策 总结 多线程会引入额外的复杂度和成本,而redis是追求简洁高效的存储组件,而且事实也证明,虽然redis是单线程处理架构,redis性能还是经受住了考验 3.Redis单线程为什么能这么快 Redis单线程 Redis核心的请求处理是单线程,但是Redis却能使用单线程模型达到每秒数万级别的处理能力,这是Redis多方面极致设计的一个综合结果 ...
三、对象 Redis Object 是什么? redis是key-value存储,key-value在redis中被抽象为对象(Object),key只能是String对象,value支持丰富的对象类型{String, List, Set, Hash, Sorted Set, Stream…} Object在内存中的样子 #define LRU_BITS 24 typedef struct reidsObject { unsigned type:4; unsigned encoding:4; unsigned lru:LRU_BITS; int refcount; void *ptr; } robj; Type: 查看redis对象 Encoding: 表明使用哪种底层编码 Lru: 记录对象访问信息,用于内存淘汰 Refcount: 引用计数,用来描述有多少指针,指向该对象 Ptr: 内容指针,指向实际内容 对象与数据结果 实际操作的对象有6个Redis对象,他们的底层依赖一些数据对象,包括字符串、跳表、哈希表、压缩列表、双端列表等 1.String String是什么 String是字符串,是Redis中最基本的数据对象,最大为512MB,可以通过配置项proto-max-bulk-len修改它 String可以存储各种类型的字符串(包括二进制文件) 适用场景 使用场景:一般用来存放字节数据、文本数据、序列化****后的对象数据 例子: 缓存场景:Value存Json字符串等信息 计数场景:因为Redis处理命令是单线程,所以执行命令的过程是原子的,因此String数据类型适合计数场景 在redis中怎么使用: 常用操作:创建、查询、更新、删除 创建 –> set, setnx SET key value # 设置一个key值为特定的value set命令扩展参数:EX(键过期时间秒)、PX(键过期时间毫秒)、NX(只有键不存在时才对键进行操作,基本替代下面的SETNX操作)、XX(键存在时才对键进行操作) SETNX key value # 用于在指定的key不存在时,为key设置指定的值 查询 –> get, mget Get key # 查询某个key,存在就返回对应的value,不存在返回nil ...