关系型和非关系型数据库
特性 | 关系型数据库 (RDBMS) | 非关系型数据库 (NoSQL) |
---|---|---|
数据模型 | 表格(行和列) | 文档、键值、列族、图等多样化模型 |
模式 | 固定(Schema) | 灵活的,无需预定义结构 |
扩展性 | 纵向扩展为主,横向扩展困难 | 横向扩展容易,适合大规模分布式系统 |
事务支持 | 完全支持 ACID 事务 | 多数不支持或仅支持单一操作的事务 |
查询语言 | SQL | 各种 API 或 NoSQL 特定的查询语言 |
数据一致性 | 强一致性 | 最终一致性或可调的一致性级别 |
应用场景 | 结构化数据,关系复杂的事务性应用;当需要数据一致性、事务支持、表间关系和复杂查询时。比如银行、订单系统、企业管理系统 | 非结构化或半结构化数据,大规模、实时应用;当数据结构灵活、需要高扩展性、读写性能要求高时,选择 NoSQL。比如大数据分析、社交网络、实时数据处理 |
接触过的 | MySQL | Redis、ES |
Redis基本介绍
why Redis?Why not SQL?
Redis具备高性能(操作内存速度快)和高并发(单台QPS是MySQL的十倍)两种特性。
为什么Redis比MySQL快:
- 内存存储: Redis是基于内存存储的NoSQL数据库,而MySQL是基于磁盘存储的关系型数据库,需要进行磁盘/O操作。
- 简单数据结构: Redis 是基于键值对存储数据的,支持简单的数据结构(字符串、哈希、列表、集合、有序集合)。相比之下,MySQL 需要定义表结构、引等复杂的关系型数据结构,因此在某些场景下Redis的数据操作更为简单高效,比如Redis用哈希表查询,只需要O(1)时间复杂度,而MySQL弓|擎的底层实现是B+Tree,时间复杂度是O(logN)
- 线程模型:Redis采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能.上的开销,且也不会导致死锁问题。
Redis使用场景
🙂缓存:缓存穿透、击穿、雪崩、双写一致、持久化、数据过期、数据淘汰策略
🙂分布式锁:setnx、redisson
🙂消息队列、延迟队列、保存token:何种数据类型
🙂计数器
数据类型、底层的数据结构及应用场景
🙂String(字符串):最基本的类型,可以存储任何数据,例如文本、图片、二进制数据等。用于缓存计数器、缓存数据、存储配置、分布式锁、共享session信息等。
【简单字符串SDS】
SDS数据结构
len
:记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度O (1)。而且SDS不需要使用"\0"
字符来标识字符串结尾,所以可以存储包含"\0"
的数据
alloc
:分配给字符数组的空间长度。这样在修改字符串的时候,可以通过alloc - len
计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用SDS既不需要手动修改SDS的空间大小,也不会出现缓冲区溢出的问题。
flags
: 表示不同类型的SDS。5种分别是sdshdr5、 sdshdr8、 sdshdr16、sdshdr32和sdshdr64
buf[]
,字符数组,来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。
🙂List(列表):可以存储多个有序的元素,用于消息队列(但是有两个问题: 1.生产者需要自行实现全局唯一 ID; 2.不能以消费组形式消费数据)
【双向链表(短小)、压缩列表(长小)、快速列表(长大)】
🙂Set(集合):可以存储多个无序的元素,元素不能重复,用于去重、标签、点赞、共同关注、抽奖活动等。
【哈希表、整数集合】
🙂Hash(哈希):键值对存储,可以存储多个键值对,存储用户信息、缓存对象、购物车等。
【哈希表】
Hash表如何扩容的
Redis中用到的是渐进式rehash的方法来进行扩容,好处是解决了大量数据拷贝造成的阻塞问题。具体步骤:1. 先给新哈希表分配空间;2. 在rehash期间,每次哈希表元素进行增删改查操作时,redis除了会执行对应的操作以外,还会顺序把原哈希表中索引位置上所有Key-Value迁移到新哈希表上; 3. 随着客户端发起的哈希表操作请求数量增多,最终会在某个时间点把原哈希表上所有的Key-Value迁移到新哈希表上,从而完成了rehash操作。【查找时会现在原哈希表查,如果没有再去新哈希表查;新增时只会新增到新哈希表】本质:把一次性大量迁移数据的工作分摊到了多次处理请求的过程中
🙂Zset(有序集合):可以存储多个有序的键值对,排行榜、时间线和统计
【跳表或压缩列表】
- Zset在 set 的基础上增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列
- 排行榜:将用户的 ID 作为元素,用户的分数作为分数
- 时间线:将发布的消息作为元素,消息的发布时间作为分数
- 延时队列:将需要延时处理的任务作为元素,任务的执行时间作为分数
- 压缩列表和跳表的区别:
👉当 Zset 存储的元素数量小于zset-max-ziplist-entries(128)
的值,且所有元素的最大长度小于zset-max-ziplist-value(64)
的值时,会选择使用压缩列表。占用的内存较少,但是在需要修改数据时,可能需要对整个压缩列表进行重写,性能较低。不过在Redis 7.0中,压缩列表数据结构已经废弃了,交给listpack数据结构实现了
👉 当 Zset 存储的元素数量超过zset-max-ziplist-entries
的值,或者任何元素的长度超过zset-max-ziplist-value
的值时,会将底层结构从压缩列表转换为跳跃表。跳跃表的查找和修改数据的性能较高,但是占用的内存也较多。
跳表实现原理
跳表基于链表基础改进,实现了一种多层的有序链表。每一层有可以包含多个节点,每个节点通过指针连接起来。每一层里会存储指向下一个跳表节点的指针
以及跨度
(记录两个节点之间的距离),每个跳表节点都有一个后向指针
指向前一个节点。
时间复杂度O(logN)。好处是能够快速定位数据。
跳表如何设置层高?
跳表在创建节点的时候,会生成一个0~1之间的随机数,如果这个随机数小于0.25(相当于概率是25%,那么层数就会增加一层,然后继续生成下一个随机数,直到随机数的结果大于0.25就确定了该节点的层高。这样保证了层数越高、概率越小,且层高的最大限制是64.
为什么用跳表而不是B+树?
a. 跳表用于存储指针的内存更少:平衡树每个节点包含2个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33 个指针,比平衡树更有优势。
b. 跳表在增删查操作方面逻辑和实现更加简单:比如说在做范围查找的时候,平衡树的实现是,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。而对于插入和删除操作,在平衡树可能回引发子树的调整逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。
🙂BitMap:二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
🙂HyperLogLog :海量数据基数统计的场景,比如百万级网页UV计数等;
🙂GEO:存储地理位置信息的场景,比如滴滴叫车;
🙂Stream :消息队列,相比于基于List类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
Redis网络模型
- Redis 6.0之前:单Reactor单线程的模式:全部工作都在同一个进程内完成,实现起来比较简单,不需要考虑进程间通信,也不用担心多进程竞争。但是,这种方案存在2个缺点: 1. 因为只有一个进程,无法充分利用 多核 CPU 的性能;2. Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟。所以,单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景
- Redis 6.0 之后:多线程,目的是为了这是因为随着网络硬件的性能提升,Redis 的性能瓶预有时会出现在网络 I/O 的处理上。(但是对于命令的执行Redis仍然使用单线程来处理)
Redis性能和原理问题
Redis为什么快
- Redis的大部分操作都在内存中完成,并且采用了高效的数据结构,因此Redis瓶颈可能是机器的内存或者网络带宽,而并非CPU上
- Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
- Redis 采用了I/O多路复用机制处理大量的客户端Socket请求,I/O多路复用机制是指一个线程处理多个IO流,就是我们经常听到的
select/epoll
机制。简单来说,在Redis只运行单线程的情况下,该机制允许内核中,同时存在多个监听Socket和已连接Socket。内核会一直监听这些Socket上的连接请求或数据请求。一旦有请求到达,就会交给Redis线程处理,这就实现了一个Redis线程处理多个IO流的效果。
Redis如何实现高并发
Redis虽然是单线程的,但还是可以通过I/O多路复用技术和事件驱动模型来实现并发效果,避免了多线程的锁竞争和上下文切换的开销。同时,Redis的高效内存管理和快速请求处理能力使得它在单线程模式下依然可以达到极高的性能。具体机制如下:
-
I/O多路复用(I/O Multiplexing)
🎵通过一个单线程处理多个I/O操作。这种机制允许Redis在单个线程中同时监听多个文件描述符(通常是网络连接的套接字),并在这些文件描述符上有事件发生时,将其转发到事件处理器中进行处理。
👉实现方式:Redis内部使用select、poll、epoll等系统调用来实现I/O多路复用(epoll在Linux系统中最常用,因为它在处理大量并发连接时具有更高的效率)。比如,当多个客户端连接服务端时,Redis 会将客户端socket对应的文件描述符(FD)注册进epoll,然后epoll 同时监听多个FD是否有数据到来或者是其中一个客户端达到读或些的抓过你太,如果有数据来了就通知事件处理器赶紧处理,这样就不会存在服务端一直等待某个客户端给数据的情形。使用的是Reactor设置模式的方式来实现。 -
事件驱动模型
🎵事件循环:Redis通过一个事件循环(event loop)来驱动整个系统。在这个循环中,Redis等待I/O事件(如新连接的建立、数据的读取和写入)发生,当事件发生时,调用相应的事件处理器。
👉任务处理:当有客户端发来请求时,事件驱动模型会将这个请求放入队列中,然后按顺序处理这些请求。由于所有请求都在单个线程中按序执行,避免了传统多线程模型中的上下文切换和竞争条件问题。 -
单线程处理的优势
👉避免了锁竞争:因为Redis是单线程的,所以它不需要使用锁来保护数据结构的并发访问。这避免了多线程环境中常见的死锁和竞态条件问题。
👉快速上下文切换:在多线程环境中,线程之间的上下文切换是昂贵的,尤其是在高并发情况下。而在单线程的Redis中,没有线程上下文切换的开销,使得处理速度非常快。 -
高效的内存管理
👉内存分配: Redis使用自定义的内存分配器(如jemalloc),优化了内存管理的效率和性能。它能快速地进行内存分配和回收,降低内存碎片的影响。
👉数据持久化:Redis在处理I/O操作和持久化操作时采用了异步方式,避免了持久化操作对数据处理效率的影响。 -
请求的快速处理
👉请求处理:Redis的单个请求处理速度非常快,通常在微秒级别。这意味着即使在单线程的情况下,Redis每秒也能处理数十万到数百万的请求。
👉内存存储:由于Redis的数据全部存储在内存中,访问速度极快。这使得它在单线程处理请求时,可以达到极高的吞吐量。 -
分布式架构支持并发
👉Redis Cluster: 虽然单个Redis实例是单线程的,但通过Redis Cluster,可以将数据分布到多个Redis实例上,从而实现并行处理和水平扩展,进一步提高系统的并发处理能力。
高并发场景,Redis单节点+ MySQL单节点能有多大的并发量?
如果缓存命中的话,4核心8g内存的配置, redis可以支撑10w的qps ;
如果缓存没有命中的话,4核心8g内存的配置,mysql 只能支持5000左右的qps
Redis如何实现高可用
主从复制、Redis Sentinel、Redis Cluster、持久化机制、多AZ/多数据中心部署
- 多AZ/多数据中心部署
- 概念: 在大规模系统中,Redis可以跨多个可用区(Availability Zones)或数据中心进行部署,以提高容灾能力。
- 高可用性:
- 跨地域复制: Redis的主从架构可以跨多个数据中心部署,从而即使一个数据中心完全失效,其他数据中心的节点仍能继续提供服务。
- 地理分布: 通过在不同地理位置部署Redis实例,可以减小网络延迟并增强故障恢复能力。
Redis如何保证操作的原子性(事务)
- 使用原子操作命令:如SET、HSET、SADD等。 Redis 是使用单线程串行处理客户端的请求来操作命令,这些命令在执行过程中不会被其他操作打断(相当于互斥)。
- 使用事务:Redis支持事务操作,即一系列原子操作被封装为一个事务。当事务开始时,Redis会锁住数据,防止其他进程或线程对其进行修改。当事务执行完毕,锁才会被释放。这就保证了在事务执行期间,其他进程无法修改数据。
- 锁机制:在多进程或多线程环境中,Redis通过使用锁机制来保证原子性。当一个进程或线程需要访问或修改数据时,它会先获取锁。只有当锁被成功获取,且没有其他进程或线程拥有锁时,该进程或线程才能执行数据操作。一旦操作完成,它就会释放锁,让其他进程或线程有机会获取。
- Lua脚本
【场景】两个客户端同时对[key1]执行自增操作,如何保证不会相互影响
👉使用单命令操作:比如用Redis的INCR
,DECR
,SETNX
命令,把RMW三个操作转变为一个原子操作
👉加锁: 调用SETNX
命令对某个键进行加锁(如果获取锁则执行后续RMW操作,否则直接返回未获取锁提示)-> 执行RMW业务操作 -> 调用DEL
命令删除锁
👉Lua脚本:多个操作写到一个 Lua 脚本中(Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性),限制所有客户端在一定时间范围内对某个方法(键)的访问次数。客户端 IP 作为 key,某个方法(键)的访问次数作为 value
然后调用执行:local current current = redis.call("incr",KEYS[1]) //从Redis中获取名为 KEYS[1] 的键的当前值,并将其递增。 if tonumber(current) == 1 // 如果递增后的值为1,则设置该键的过期时间为60秒。 then redis.call("expire",KEYS[1],60) end
redis-cli --eval lua.script keys , args
Redis缓存三件套
缓存穿透
🎶是指查询数据的时候,数据既不存在于redis中,也不存在于数据库中,导致查不到数据也写不到redis中,每次请求都会去请求数据库,导致数据库挂掉,这种情况大概率是遭受到了攻击
👉有两种解决方式,一个是返回空值,当我们查询不到数据的时候,也将这个null值写到redis中,还有一个方法是布隆过滤器,在查询数据的时候先查布隆过滤器中是否存在该数据,不存在则直接返回,存在再进入下一步查询redis
布隆过滤器
基于redisson实现的底层:布隆过滤器它是一个只存放二进制的数组, 通过对id值进行三次不同的哈希运算,得到三个哈希值,修改哈希值索引的数组元素为1。这样在每次查询id的时候,只需要查它对应的三个数组值是否为1,就能知道他是否存在了。但存在一个数据误判的情况,这时候我们可以扩大数组大小或者选择多个哈希函数来减少误判率,但这也是牺牲了空间换来的,一般我们设置误判率在5%左右即可
👉为什么不能用哈希表要用布隆过滤器?
哈希表考虑到负载因子的存在,对空间的利用率不高;而且哈希表有链表查询,在哈希冲突严重的情况下,会比纯数组查询的布隆慢
👉优点
存储空间和插入/查询时间都是常数;散列函数相互之间没有关系,方便并行实现;可以表示全集,不需要存储元素本身,在某些对保密要求非常严格的场合有优势
👉缺点
存在误算率,数据越多,误算率越高;一般情况下无法从过滤器中删除数据;二进制数组长度和 hash 函数个数确定过程复杂
缓存击穿
🎶是指key过期时刚好有大量的请求访问key,导致所有的请求都访问到数据库,增大了数据库的压力
👉解决方式:看我们的业务场景是需要数据强一致还是无需强一致。如果需要的话就使用互斥锁,不需要就为key设置逻辑过期时间。
- 互斥锁是一个线程在访问redis中的key发现过期的时候,用setnx设置一个互斥锁,当同步redis和数据库中的操作完成后,再释放锁资源,这样就算有另外的线程访问这个key,也会因为没有拿到锁资源而被阻塞。这样能保证数据的强一致性,但是性能不高。
- 逻辑过期时间是指一个线程访问key发现过期时,开启一个新的线程进行数据同步,当前线程直接返回redis中的过期数据。而当新的线程同步完成后也会重新设置key的过期时间。这样会导致当前线程拿到的是一个过期的数据,无法保证数据强一致性,但是性能高。
缓存雪崩
🎶是指很多的key同时到期了,导致访问这些key的请求都到达了数据库端。
👉解决方式:随机过期时间:尽量不要设置相同的key过期时间,而是采取随机值。互斥锁:如果访问的数据不在redis里就加一个互斥锁保证同一时间内只有一个请求来构建缓存(最好设置一个超时时间)。后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交给后台线程定时更新
Redis数据过期和数据淘汰
数据过期策略(惰性删除和定期删除)
🎶Redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉,Redis的删除策略是惰性删除 + 定期删除两种策略进行配合使用
👉惰性删除:访问key的时候判断是否过期,如果过期则删除。对CPU友好但是对内存不友好
👉定期删除:每隔一段时间,就对一些key进行检查,删除里面过期的key。可以通过限制删除操作的执行频率和时长来减少对CPU的影响。但是难以确定合适的频率和时长【随机抽查的数量:写死在代码中,20;如果本轮检查的已过期key的数量,超过5个(20/4) ,也就是「已过期key的数量」 占「随机抽取key的数量」> 25%,则继续重复步骤1;如果已过期的key比例小于25%,则停止继续删除过期key,然后等待下一轮再检查】
SLOW
:定时任务,执行频率10hz,每次不超过25msFAST
:执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
👉定时过期:定时过期是指在设置键值对的时候,同时指定一个过期时间。一旦超过这个时间,键值对就会自动被删除。这种策略可以确保数据的实时性,但是它的效率并不是很高,因为Redis需要为每一个设置了过期时间的键维护一个定时器。
数据淘汰策略(LRU和LFU)
🎶针对Redis内存不足时,仍然需要向Redis中添加策略的场景,此时需要按照特定规则来淘汰内存中的数据,将其删除掉
🎶LRU(Least Recently Used):最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高;
🎶LFU(Least Frequently Used):最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
👉八种数据淘汰策略和使用建议
noeviction
【默认】- 不淘汰任何key,但是内存满时不允许写入新数据
- 内存用完了再添加新数据时会直接报错
volatile-ttl
- 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰。
allkeys-random
- 对全体key ,随机进行淘汰。
- 访问频率差别不大,没有明显冷热数据区分
volatile-random
- 对设置了TTL的key ,随机进行淘汰。
allkeys-lru
- 对全体key,基于LRU算法进行淘汰。
- 优先使用,特别是如果业务有明显的冷热数据区分
- 场景:数据库有1000万数据,Redis只能缓存20w数据,保证Redis中数据都是热点数据
volatile-lru
- 对设置了TTL的key,基于LRU算法进行淘汰。
- 有置顶需求,置顶数据不设置过期时间
allkeys-lfu
- 对全体key,基于LFU算法进行淘汰。
- 短时高频访问
volatile-lfu
- 对设置了TTL的key,基于LFU算法进行淘汰。
- 短时高频访问
Redis持久化
RDB(Redis DataBase)快照
🎶数据快照,把内存中的所有数据记录到磁盘,当Redis宕机恢复数据的时候,从RDB的快照文件中恢复数据
👉怎么做
有两个命令,save
和bgsave
,save
会阻塞Redis服务器进程,直到RDB文件创建完成;bgsave
会fork
一个子进程来负责创建RDB文件,只有fork的时候会阻塞,创建RDB的时候父进程可以继续处理命令请求,所以一般用bgsave
。在redis.config
文件中配置Redis内部触发RDB的机制,比如save 900 1
表示900s内,如果至少一个key被修改,执行bgsave
命令
👉执行原理
bgsave
开始时会fork
主进程得到子进程,子进程共享主进程中的内存数据。fork
采取的是copy-on-write技术:当主进程执行读操作时,访问共享内存,当主进程执行写操作时,则会拷贝一份内存数据的副本,在副本中执行写操作,这样就不会出现脏读现象
【扩展:怎么共享】Redis读写数据的时候不能直接处理物理内存,而是处理虚拟内存,通过页表来找到虚拟地址和物理地址之间的映射关系实现的。因此子进程只需要fork一份主进程的页表,即可完成内存共享。
👉优缺点:RDB是二进制压缩文件,占用空间小,便于传输,恢复数据速度较快。两次RDB期间有空档期,此期间若Redis宕机了可能会造成数据的丢失。
AOF(Append Only File)日志
🎶当redis操作写命令的时候,都会将命令存储在追加文件AOF中,当redis实例宕机恢复数据的时候,会从AOF中再次执行一遍命令来恢复数据。
👉怎么做
- AOF默认是关闭的,在
redis.config
文件中配置appendonly yes
开启,记录的频率通过appendfsync always/everysec/no
修改,这三种指令分别代表了 同步刷盘/每秒刷盘/操作系统控制刷盘,数据完整性由好到差,速度由慢到快,一般选用everysec
- 使用
bgrerwriteaof
命令,让AOF文件执行重写功能,比如说一个key执行了多次写操作,但只有最后一次写操作有用,开启这个命令就可以只记录最后一次写操作。 - 自动重写AOF:
auto-aof-rewrite-percentage 100/ auto-aof-rewrit-min-size 64mb
AOF文件比上次文件增长超过多少(100%)则触发重写/AOF文件体积到达(64mb)触发重写
👉优缺点:数据的完整性较高,文件较大,恢复速度较慢。
RDB-AOF 混合持久化
该模式会将生成相应的RDB数据,写入AOF文件中,重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。这样用户可以同时获得RDB持久化和AOF持久化的优点。
【场景题】缓存一致性问题如何解决(比说断电导致缓存丢失)
- redis有持久化机制;2. 可以延迟双删解决
Redis集群与主从
主从复制
单个Redis节点的并发能力是有上限的,可以搭建主从集群,实现读写分离来提高并发能力,一般是一主多从,主节点负责写数据,从节点负责读数据
全量同步
从节点第一次与主节点建立连接的时候使用全量同步
- 从节点请求主节点同步数据:从节点会携带自己的
replication id
和offset
偏移量。 - 主节点判断是否是第一次请求,主要判断依据就是,主节点与从节点是否是同一个
replication id
,如果不是,就说明是第一次同步,那主节点就会把自己replication id
和offset
发送给从节点,让从节点与主节点的信息保持一致。 - 主节点执行
bgsave
指令生成rdb
文件,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb
文件。 - 在
rdb
生成的期间,主节点会以命令的方式记录到缓冲区(一个日志文件repl_baklog
),会把这个日志文件也发送到主节点进行同步。
👉 其他的同步时机:从节点数据丢失、从节点长时间未和主节点同步
增量同步
slave重启或后期数据变化使用增量同步
- 简单版:从节点请求主节点同步数据,主节点还是判断是不是第一次请求,不是第一次就获取从节点的
offset
值,然后主节点从命令日志repl_backlog
中获取offset
值之后的数据,发送给从节点进行数据同步。 - 进阶版:从节点需要增量同步时,会发送
psync
令给主节点,此时的psync
命令里的offset
参数不是-1
👉主节点收到该命令后,然后用CONTINUE
响应命令告诉从节点接下来采用增量复制的方式同步数据,然后主节点将主从节点断线期间,所执行的写命令发送给从节点,然后从节点执行这些命令。
主服务器怎么知道要将哪些增量数据发送给从服务器呢?
👉repl_backlog_buffer
:是一个环形缓冲区,用于主从服务器断连后,从中找到差异的数据。在主节点进行命令传播时,不仅会将写命令发送给从节点,还会将写命令写入到这个缓冲区,因此这个缓冲区里会保存着最近传播的写命令。【默认大小是1M,当缓冲区写满后,主节点继续写入的话,就会覆盖之前的数据。因此,当主服务器的写入速度远超于从服务器的读取速度,缓冲区的数据一下就会被覆盖。在网络恢复时,如果从节点想读的数据已经被覆盖了,主服务器就会采用全量同步,这个方式比增量同步的性能损耗要大很多。因此,为了避免在网络恢复时,主服务器频繁地使用全量同步的方式,我们应该调整下repl backlog_ buffer
缓冲区大小(尽可能大)】
👉replication offset
: 标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量。主服务器使用master_ repl_offset
记录自己写的位置,从服务器使用slave_repl_offset
来记录自己读到的位置。网络断开后,当从节点重新连上主节点时,从节点会通过psync
命令将自己的复制偏移量slave_repl_offset
发送给主节点,主节点根据自己的master_ repl_offset
和slave_repl_offset
之间的差距,来决定对从服务器执行哪种同步操作:- 如果判断出从节点要读取的数据还在
repl_backlog_buffer
缓冲区里,那么主服务器将采用增量同步的方式:在repl_backlog_buffer
中找到主从服务器差异(增量)的数据后,就会将增量的数据写入到replication buffer
缓冲区,这个缓冲区是缓存将要传播给从服务器的令。 - 如果不存在,那么主节点将采用全量同步的方式。
- 如果判断出从节点要读取的数据还在
哨兵sentinel
实现主从集群的自动故障恢复
作用:监测/选主/通知
- 监测:Sentinel 会基于心跳机制不断检查master和slave是否按预期工作。【主观下线】如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。【客观下线】若超过一半的sentinel都认为该实例主观下线,则该实例客观下线。
- 选主:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主。首先判断主与从节点断开时间长短,如断开时间太长则不选举该从节点。然后判断从节点的
slave-priority
值,越小优先级越高。如果优先值相等,则判断从节点的offset值,越大优先级越高 - 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端。
哨兵模式从client发起请求后的具体过程是什么样的
客户端通过与哨兵节点通信来获取主节点信息,并在主节点故障时自动切换到新的主节点。整个过程中,客户端需要具备自动重连和获取新主节点信息的能力,以确保在主节点发生故障后能够继续正常操作。
具体过程:
- 客户端首先会连接到一个或者多个哨兵节点,以获取当前的主节点地址,并且通过配置多个哨兵节点的地址,确保即使某些哨兵节点不可用,客户端仍然能够获取到主节点的信息。
- 客户端向哨兵节点发起请求后询问当前的主节点地址,哨兵节点响应客户端的请求,返回当前主节点的IP地址和端口信息。此时,客户端已经知道了哪个Redis实例是主节点。获取到主节点地址后,客户端直接连接到Redis主节点,进行正常的读写操作。所有的写操作会发送到主节点,主节点负责将数据同步到从节点。
- 在后台,哨兵持续监控Redis主节点和从节点的健康状态。如果哨兵检测到主节点不可用(例如主节点宕机),哨兵会执行自动故障转移。哨兵会选举出一个新的主节点(通常是一个从节点),并重新配置其他从节点与新的主节点进行同步。哨兵更新了集群中的主从结构后,它会通知所有已连接的客户端新的主节点地址。
- 如果在操作过程中,客户端发现与主节点的连接中断或请求失败,客户端可以重新联系哨兵节点,获取新的主节点信息。哨兵在完成故障转移后,会向客户端返回新的主节点地址。客户端随后连接到新的主节点,继续执行读写操作。
- 客户端更新与新主节点的连接后,可以继续进行数据操作,而不需要关心底层的故障转移细节。整个过程对应用程序是透明的,应用只需处理重新连接的逻辑。
需要实现的逻辑:
- 哨兵配置: 客户端配置时需要指定哨兵节点的地址列表,以便在连接失败时可以尝试其他哨兵。
- 自动重连: 在连接主节点失败时,客户端应自动联系哨兵获取新的主节点信息,并重新连接。
- 连接池管理: 客户端可以使用连接池来管理与Redis主节点的连接,确保在故障转移时及时更新连接池中的节点信息。
脑裂问题
🎶由于网络等原因,使得哨兵无法心跳感知到主节点,于是通过选举的方式产生了一个新的主节点,于是就有了两个主节点,这样会导致客户端在老主节点那更新数据,新的主节点无法同步更新数据,产生数据丢失。
👉解决方案,配置参数:一个主节点至少需要有一个从节点,才允许写入。或者缩短主从数据同步的延迟时间。
分片集群
用来解决高并发写和海量存储问题
👉介绍
- 集群中有多个master,每个master保存不同数据
- 每个master可以有多个slave节点
- master之间通过
ping
检测彼此健康状态,就无需哨兵了 - 客户端可以访问任意节点,最终都会经过路由转发到正确节点
👉存储和读取数据的原理:16384个哈希槽分配到不同的master节点
- 根据key的有效部分计算哈希值(按照CRC16算法计算一个16bit的值),对16384取模【有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分】模数作为插槽,寻找插槽所在的节点
- 哈希槽映射到Redis节点:【平均分配】在使用
cluster create
命令创建Redis集群时,Redis会自动把所有哈希槽平均分布到集群节点上。比如集群中有9个节点,则每个节点上槽的个数为16384/9个。【手动分配】可以使用cluster meet
命令手动建立节点间的连接,组成集群,再使用cluster addslots
命令,指定每个节点上的哈希槽个数。
👉优缺点
- 优点:
高可用性:Redis集群节点之间采用主从复制机制,可以保证数据的持久性和容错能力,哪怕其中一个节点挂掉,整个集群还可以继续工作。
高性能:Redis集群采用分片技术,将数据分散到多个节点,从而提高读写性能。当业务访问量大到单机Redis无法满足时,可以通过添加节点来增加集群的吞吐量。
扩展性好:Redis集群的扩展性非常好,可以根据实际需求动态增加或减少节点,从而实现可扩展性。集群模式中的某些节点还可以作为代理节点,自动转发请求,增加数据模式的灵活度和可定制性。 - 缺点:
部署和维护较复杂:Redis集群的部署和维护需要考虑到分片规则、节点的布置、 主从配置以及故障处理等多个方面,需要较强的技术支持,增加了节点异常处理的复杂性和成本。
集群同步问题:当某些节点失败或者网络出故障,集群中数据同步的问题也会出现。数据同步的复杂度和工作量随着节点的增加而增加,同步时间也较长,导致一定的读写延迟。
数据分片限制:Redis集群的数据分片也限制了一些功能的实现,如在一个key上修改多次,可能会因为该key所在的节点位置变化而失败。此外,由于将数据分散存储到各个节点,某些操作不能跨节点实现,同节点之间的一些操作需要额外注意。
Redis一致性
一致性需要注意的点:
- 在我们用Redis主从复制或者集群模式的时候,需要确保主从节点的同步,并考虑节点失效和故障恢复
- 读写操作的一致性
- 用Redis分片需要选择合适的分片策略,确保数据能够均匀分布,避免负载不均衡
- 使用Redis的事务和乐观锁保证原子性和一致性
- Redis用作缓存的时候要考虑与数据库的缓存一致
主从和集群可以保证数据一致性吗?
主从和集群在CAP理论都属于AP模型,即在面临网络分区时选择保证可用性和分区容忍性,而牺牲了强一致性。这意味着在网络分区的情况下,Redis主从复制和集群可以继续提供服务并保持可用,但可能会出现部分节点之间的数据不一致。
双写一致性(延迟双删)
🎶是指数据库中的数据应该与Redis中的数据保持一致。如何保证双写一致性也分为强一致业务和允许延时一致的业务
👉允许延时一致的业务场景:使用延迟双删,即缓存中删除数据后,再到数据库中修改数据,然后延迟一段时间再到缓存中删除数据。【why延迟?】因为数据库是有两章主从分离的表,从表更新主表的数据也是需要时间的
👉需要强一致性的场景:使用读写锁和排他锁,读的时候上读写锁,这样其他线程来只能读不能写。写的时候上排他锁,其他线程都被阻塞
【场景题】有多个 Redis 节点,当 MySQL 发生更新时,需要确保更新各个节点的缓存,其中一个节点下线的情况下如何保持系统的正常运行
- 事务:在 MySQL 更新操作之前,开启 Redis 事务。在事务中执行更新各个 Redis 节点的缓存操作,包括写入新的数据、删除旧的数据等。提交事务以确保所有操作原子性
- 管道:使用 Redis 管道可以将多个命令一次性发送到 Redis 服务器,并在一次通信中获取所有命令的执行结果,从而减少通信开销和延迟。在 MySQL 更新之前,创建一个 Redis 管道。将更新各个节点的缓存操作添加到管道中。执行管道以一次性提交所有操作
高并发如何保证缓存数据一致性
推荐使用场景
- 强一致性需求:选择分布式锁、缓存更新策略(如双删、延时双删)、或者使用缓存一致性协议。
- 最终一致性需求:可使用消息队列或读写分离异步更新策略。
- 高并发读场景:适合使用缓存淘汰策略结合分布式锁或布隆过滤器等策略来确保性能。
- 缓存更新策略
- 缓存淘汰(Cache Eviction):在更新数据库时,同时删除缓存中的数据,确保数据不一致时强制从数据库中获取最新的数据。常用的做法是先更新数据库,再删除缓存。
- 优点:简单易行,避免缓存数据与数据库数据不一致。
- 缺点:可能引发缓存击穿问题(Cache Penetration),特别是在缓存高并发场景下。
- 缓存更新(Cache Update):在更新数据库时,同时更新缓存。
- 优点:缓存和数据库始终保持一致。
- 缺点:实现复杂,容易导致数据竞争和数据同步的延迟问题。
- 双写一致性控制
- 先删后写(Delete-then-Write):在数据更新时,先删除缓存,然后更新数据库。
- 优点:缓存一致性高,简单有效。
- 缺点:在高并发场景下,如果删除缓存后,多个并发请求同时查询数据库,可能导致缓存击穿。
- 延时双删(Delayed Double Deletion):在更新数据库之前,先删除缓存,然后执行数据库更新。更新完数据库后,经过一段延时(通常是基于业务的平均读写延时)再次删除缓存。
- 优点:能有效避免缓存失效的情况下读取到脏数据。
- 缺点:增加了系统的复杂性,且依赖于延时时间的准确设定。
-
分布式锁:在对数据库和缓存进行操作时使用分布式锁(如基于Redis或ZooKeeper的锁),确保同一时间只有一个线程能够对数据进行修改和更新缓存。
-
消息队列实现异步一致性:在数据库更新后,将数据变更消息写入队列,缓存更新订阅该消息队列并进行更新。 优点:可实现最终一致性,并且不影响主线程性能。 缺点:可能存在短时间内的数据不一致性。
-
读写分离和异步更新:将读操作与写操作分离,写操作直接更新数据库,而读操作先检查缓存。异步线程或定时任务负责将数据库更新的数据异步同步到缓存。优点:高性能,降低数据库压力。 缺点:实现复杂,需要处理异步更新的时序问题。
-
乐观锁/版本控制:利用数据版本号或时间戳,在更新数据前先比对数据的版本号/时间戳是否一致,确保只有最新版本的数据才能被更新。优点:避免了锁的开销,确保数据最终一致性。 缺点:在高并发情况下可能会导致大量重试和性能损失。
-
缓存一致性协议:在分布式缓存系统中使用一致性协议,确保在数据修改后,各节点上的缓存一致性。优点:系统内置支持,适用于强一致性需求。 缺点:依赖缓存系统的实现,复杂度高。
Redis分布式事务
分布式事务是指在一个分布式系统中,涉及多个资源管理器(如数据库、消息队列等)的事务操作。与单机事务不同,分布式事务需要在不同的节点或服务之间保持一致性,确保所有参与的操作要么全部成功,要么全部失败,避免部分成功导致的数据不一致问题。
Redis 本身不直接支持分布式事务,但可以通过一些技巧和设计模式实现类似的分布式事务功能。在分布式系统中,事务需要跨多个节点或系统,确保数据一致性。下面是一些在 Redis 中实现分布式事务的常见方案:
-
使用 Lua 脚本实现原子性操作:因为 Redis 会在单线程中执行 Lua 脚本,这保证了脚本中的所有操作要么全部成功执行,要么全部失败。通过 Lua 脚本可以在 Redis 中执行多个命令,并确保这些命令是不可分割的原子操作。
示例:-- Lua 脚本 local val1 = redis.call("GET", KEYS[1]) local val2 = redis.call("GET", KEYS[2]) if val1 and val2 then redis.call("SET", KEYS[1], val1 + 1) redis.call("SET", KEYS[2], val2 - 1) end
这种方法保证了脚本中的所有命令都在一个事务中执行,从而避免了分布式系统中常见的中途失败问题。
-
基于 Redis 的分布式锁实现分布式事务:可以用于在分布式环境中确保多个操作以某种顺序执行,实现最终一致性。一个典型的分布式锁实现是使用 Redis 的 SETNX(Set if Not Exists)命令。
典型实现:
进程 A 尝试获取分布式锁,使用 SETNX 命令设置一个锁键。
如果锁键不存在,A 获取锁并设置一个过期时间防止死锁。
进程 B 尝试获取锁,但由于 A 持有锁,B 会等待或重试。
进程 A 完成操作后,释放锁。
Redis 的分布式锁可以通过实现如 Redlock 来保证可靠的锁机制。 -
Redis 事务(MULTI、EXEC)
Redis 提供了简单的事务机制,虽然不支持回滚,但可以使用 MULTI 和 EXEC 进行多命令的批量执行。Redis 的事务保证命令按照顺序执行,但不能保证隔离性,因为在事务的执行期间,其他客户端可以进行并发修改。而且 Redis 事务没有像传统数据库那样的回滚机制,如果某个命令失败,其他命令依然会继续执行。流程:
使用 MULTI 命令开启事务。
执行多个 Redis 命令,这些命令会被放入队列。
使用 EXEC 命令提交事务,批量执行所有命令。MULTI SET key1 value1 SET key2 value2 EXEC
-
TCC(Try-Confirm-Cancel) 模式: 是一种分布式事务模式,通常用于复杂的分布式场景。可以使用多个 Redis 键来分别存储这些状态,并根据不同的结果执行相应的确认或回滚操作。TCC 的核心思想是将事务分为三个步骤:
Try:准备阶段,尝试执行操作并预留资源。通过设置 Redis 键来预留资源。
Confirm:确认阶段,在所有操作成功后,提交操作。如果所有系统成功执行,则修改 Redis 键的状态以确认。
Cancel:取消阶段,如果有任何异常,则回滚操作。如果任何系统失败,则释放或回滚 Redis 中的预留资源。
Redis应用实践
分布式锁
集群架构下,用分布式锁解决线程之间的互斥性,有两种实现:setnx和Redisson
setnx
SET lock val NX EX 10
:NX
表示互斥:set if not exists,EX
表示设置超时时间(PX
有也可以)
DEL key
:释放锁
👉底层原理:如果key不存在,才会插入成功,加锁;如果Key存在会显示插入失败。对于加锁条件需要满足。解锁的过程就是把锁键删除,但需要判断执行删除锁操作的客户端是否为加锁的客户端,是的话才可以解锁。因此这里涉及两步,需要用到Lua脚本保证解锁的原子性
if redis.call("get",KEY[1]) == ARGC[1]) then
return redis.call("del",KEY[1])
else
return 0
end
👉锁变量的值需要满足的条件:设置过期时间(避免客户端拿到锁之后发生异常导致锁一直无法释放)和唯一(区分来自不同客户端的加锁操作)
Redisson(基于setnx和lua)
RLock lock = redissonClient.getlock("锁名");
boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
if (islock){
try{ 线程要执行的具体业务 } finally{ lock.unlock(); }
}
👉控制锁时间的合理性:提供了一个watch dog机制来合理控制锁的有效时长,一个线程获得锁成功后,watch dog会给持有锁的线程续期【默认10s】
👉流程:一个线程来尝试加锁,成功后可以操作Redis,同时另开了一个线程进行监控,也就是watch dog,它会不断监听持有锁的线程,每隔(releaseTime / 3)的时间做一次续期,增加锁的使用时间,手动释放锁后,还需要通知watch dog不再监听。如果此时又有另外一个线程来尝试加锁,它会循环等待持有锁的线程释放锁,在高并发情况下增加了性能,但是等待时间超过阈值了以后也会停止
👉可重入吗?
可以,用hash结构记录线程id和重入次数【key是锁名,值是线程id和重入次数】
👉能解决主从一致性吗?
不能,但是可以用redisson提供的红锁,但不推荐,如果非要保证强一致性可以用zookeeper实现的分布式锁。
👉 执行了SETNX命令加锁后的风险和解决思路
- 假如某个客户端在执行了
SETNX
命令加锁之后,在后面操作业务逻辑时发生了异常,没有执行DEL
命令释放锁。该锁就会一直被这个客户端持有,其它客户端无法拿到锁,导致其它客户端无法执行后续操作。- 解决:给锁变量设置一个过期时间,到期自动释放锁
SET key value [EX seconds | PX milliseconds] [NX]
- 解决:给锁变量设置一个过期时间,到期自动释放锁
- 如果客户端 A 执行了
SETNX
命令加锁后,客户端 B 执行DEL
命令释放锁,此时,客户端 A 的锁就被误释放了。如果客户端 C 正好也在申请加锁,则可以成功获得锁。- 解决:加锁操作时给每个客户端设置一个唯一值(比如UUID),唯一值可以用来标识当前操作的客户端。在释放锁操作时,客户端判断当前锁变量的值是否和唯一标识相等,只有在相等的情况下,才能释放锁。(同一客户端线程中加锁、释放锁)
SET lock_key unique_value NX PX 10000
- 解决:加锁操作时给每个客户端设置一个唯一值(比如UUID),唯一值可以用来标识当前操作的客户端。在释放锁操作时,客户端判断当前锁变量的值是否和唯一标识相等,只有在相等的情况下,才能释放锁。(同一客户端线程中加锁、释放锁)
消息队列
Redis 可以通过多种方式实现消息队列,主要包括使用 List 结构、Pub/Sub(发布/订阅)模式、Stream 数据类型等。List: 简单且常用,适合基本的队列场景,但不支持消息广播和持久化。Pub/Sub: 适合实时消息广播的场景,但不适合需要消息持久化的场景。Stream: 功能最强大,适合需要高可靠性、持久化和消费组的复杂消息队列场景。
- 基于 List 的消息队列
👉核心命令:
LPUSH/RPUSH
: 向列表左侧/右侧插入一个元素。
RPOP/LPOP
: 从列表右侧/左侧弹出一个元素。
BLPOP/BRPOP
: 阻塞弹出元素,直到列表有元素可供弹出。
👉工作原理:
消息生产者通过 LPUSH 或 RPUSH 向列表(队列)插入消息。
消息消费者通过 RPOP 或 LPOP 从列表中获取消息。
如果消费者希望在队列为空时阻塞等待,可以使用 BLPOP 或 BRPOP。
👉优点:简单易用,适合单生产者-单消费者或少量消费者的场景。# 生产者:向队列 myqueue 添加消息 LPUSH myqueue "message1" LPUSH myqueue "message2" # 消费者:从队列 myqueue 获取消息 RPOP myqueue # 返回 "message1"
👉缺点:无法轻松处理消息的广播(即同一消息被多个消费者处理)。消费者可能需要显式轮询(如果不使用阻塞操作)。 - 基于 Pub/Sub 的消息队列
👉核心命令
PUBLISH
: 将消息发布到一个频道。
SUBSCRIBE
: 订阅一个或多个频道,接收发布的消息。
UNSUBSCRIBE
: 取消订阅。
👉工作原理
消息生产者通过 PUBLISH 命令将消息发布到一个频道。
订阅了该频道的所有消费者都会接收到该消息。
适用于广播消息,所有订阅者都会收到消息。
👉优点:实时消息广播,适合通知系统、聊天应用等需要多消费者同步消息的场景。# 生产者:发布消息到频道 mychannel PUBLISH mychannel "message1" # 消费者:订阅频道 mychannel SUBSCRIBE mychannel
👉缺点:消息是即发即弃的,消息发布后如果没有订阅者接收,消息将丢失。无法持久化,不能处理消费者临时不可用的场景。 - 基于 Stream 的消息队列
👉核心命令
XADD
: 向Stream添加消息。
XREAD
: 从Stream读取消息。
XGROUP
: 创建和管理消费组。
XREADGROUP
: 从消费组读取消息。
👉工作原理
Redis Streams 是一个更为复杂和功能丰富的消息队列系统,支持消息的持久化、消费组、消息确认和跟踪等高级功能。
消息生产者通过 XADD 命令将消息添加到Stream。
消费者可以使用 XREAD 从Stream读取消息,或通过消费组使用 XREADGROUP 来消费消息。
消费组允许多个消费者协作消费同一Stream,并支持消费进度的管理。
👉优点:支持持久化、消费组和消息确认,适用于复杂的消息队列需求。能够处理消息的重发、消息的确认机制以及消息的跟踪。# 生产者:向Stream mystream 添加消息 XADD mystream * key1 value1 key2 value2 # 消费者:从Stream mystream 读取消息 XREAD COUNT 1 STREAMS mystream 0 bash 复制代码 # 创建消费组 XGROUP CREATE mystream mygroup 0 # 消费者:从消费组 mygroup 读取消息 XREADGROUP GROUP mygroup consumer1 COUNT 1 STREAMS mystream >
👉缺点:复杂度较高,适合需要精细化控制和高可靠性的场景。
什么场景下redis做db的缓存
高并发读写场景:通过缓存减少数据库的读写压力,提升系统响应速度。
热点数据访问:缓存频繁访问的数据,减少对数据库的频繁查询。
低一致性需求的统计类业务:Redis 可以作为计数器,缓存统计结果,并在合适的时机同步到数据库。
-
热点数据缓存
👉场景:某些数据访问频率极高,例如电商系统的商品详情页、热门文章、用户个人信息等。
这些数据如果每次都从数据库中查询,会对数据库造成极大压力,影响系统性能。
👉缓存策略:Redis 缓存热点数据,直接从缓存中返回数据,极大提高查询效率。
设置适当的过期时间(TTL),定期从数据库更新热点数据。 -
数据读多写少的场景
👉场景:某些业务中,数据更新频率较低,但读取频率非常高,例如用户信息、商品信息、文章、博客内容等。对于这类数据,如果每次读取都访问数据库,效率低且会增加数据库负载。
👉缓存策略:Redis 用于缓存这类数据,读取操作可以直接从缓存中获取,更新操作时同步更新缓存和数据库。设置合理的过期时间,确保缓存中的数据定期更新。 -
秒杀、抢购等高并发场景
👉场景:秒杀、抢购等活动通常会在短时间内涌入大量流量,对数据库造成巨大压力,尤其是频繁的库存查询和更新操作。
👉缓存策略:采用 Redis 缓存库存信息,在用户秒杀请求发起时,先访问缓存中库存数据,减少数据库压力。使用分布式锁(如 Redis 分布式锁)来控制并发,防止库存超卖。 -
排名、计数器、排行榜
👉场景:一些业务场景中需要对某类数据进行排序,例如热度榜单、游戏排行榜、点赞数、浏览量计数等。如果每次排序或计数操作都通过数据库来完成,效率较低,特别是涉及到实时性要求时。
👉缓存策略:使用 Redis 的有序集合(ZSet)等数据结构实现排行榜,能够高效地支持排名和计数操作。Redis 在内存中操作排名数据,性能非常高。 -
分布式会话管理
👉场景:分布式系统中,用户的会话信息需要在多台服务器之间共享,如果每次都访问数据库读取会话信息,性能较差。将会话信息存储在本地内存又无法跨服务共享。
👉缓存策略:使用 Redis 存储用户会话信息,每次访问时从 Redis 读取会话数据,减少对数据库的查询,同时实现分布式共享。通过设置过期时间,确保会话信息在用户不活动时自动过期。 -
频繁更新的计数类业务
👉场景:例如页面浏览量(PV)、点赞数、用户行为统计等,更新频率高,但数据的准确性要求不高,最终一致即可。如果每次计数都写入数据库,会给数据库带来极大压力。
👉缓存策略:Redis 可以作为计数器,将这些数据先缓存起来,减少频繁写入数据库的压力。可以定期批量同步缓存数据到数据库,确保最终一致性。 -
内容缓存(页面缓存、全文检索)
👉场景:当系统中某些页面的内容变化较少,但访问量非常大时,可以使用缓存来减少页面内容的动态生成。另外,一些需要全文检索的场景,通过缓存检索结果,减少对后端数据库的频繁查询。
👉缓存策略:使用 Redis 缓存整个页面的渲染结果或部分页面数据块,避免每次访问都动态生成页面内容。对于全文检索,缓存部分热点检索结果。 -
分布式锁、限流、秒杀防护
👉场景:在高并发场景下,系统需要防止并发操作引发数据不一致或资源竞争,例如订单创建、库存扣减等。
👉缓存策略:使用 Redis 实现分布式锁,确保在多服务器、多线程环境下同一资源只能被一个线程操作。结合 Redis 实现限流、频率控制,防止系统过载。
大key问题
指的是某个key对应的value值所占的内存空间比较大,导致Redis的性能下降(内存碎片增加)、内存不足(可能会出发内存淘汰策略,极端case下内存耗尽,redis实例崩溃)、阻塞其他操作(删除大key时)、数据不均衡(某个分片的内存使用率远超其他分片)、网络拥塞(获取大key的流量大,导致机器或者局域网带宽被打满)以及主从同步延迟(同步过程传输大量数据)等问题。
多大的数据量才算是大key?
没有固定的判别标准,通常认为字符串类型的key对应的value值占用空间大于1M,或者集合类型的k元素数量超过1万个,就算是大key。不过这个问题的定义及评判准则并非一成不变,而应根据Redis的实际运用以及业务需求来综合评估。例如,在高并发且低延迟的场景中,仅10kb可能就已构成大key;然而在低并发、高容量的环境下,大key的界限可能在100kb。
如何解决
- 对大Key进行拆分。例如将含有数万成员的一个HASH Key拆分为多个HASH Key,并确保每个Key的成员数量在合理范围。在Redis集群架构中,拆分大Key能对数据分片间的内存平衡起到显著作用。
- 对大Key进行清理。将不适用Redis能力的数据存至其它存储,并在Redis中异步删除此类数据。
- 监控Redis的内存水位。 可以通过监控系统设置合理的Redis内存报警阈值进行提醒,例如Redis内存使用率超过70%、Redis的内存 在1小时内增长率超过20%等。
- 对过期数据进行定期清理。堆积大量过期数据会造成大Key的产生,例如在HASH数据类型中以增量的形式不断写入大量数据而忽略了数据的时效性。可以通过定时任务的方式对失效数据进行清理。
热key问题
通常以其接收到的Key被请求频率来判定,例如:
- QPS集中在特定的Key:比如 Redis实例的总QPS (每秒查询率)为10,000, 而其中一个Key的每秒访问达到了7,000。
- 带宽使用率集中在特定的Key:对一个拥有上千个成员且总大小为1MB的HASH Key每秒发送大量的
HGETALL
操作请求。 - CPU使用时间占比集中在特定的Key:对一个拥有数万个成员的Key (ZSET类型) 每秒发送大的
ZRANGE
操作请求。
如何解决
4. 在Redis集群架构中对热Key进行复制。 在Redis集群架构中,由于热Key的迁移粒度问题,无法将请求分散至其他数据分片,导致单个数据分片的压力无法下降。此时,可以将对应热Key进行复制并迁移至其他数据分片,例如将热Key foo复制出3个内容完全一样的Key并名为foo2、 foo3、 foo4, 将这三个Key迁移到其他数据分片来解决单个数据分片的热Key压力。
5. 使用读写分离架构。如果热Key的产生来自于读请求,可以将实例改造成读写分离架构来降低每个数据分片的读请求压力,甚至可以不断地增加从节点。但是读写分离架构在增加业务代码复杂度的同时,也会增加Redis集群架构复杂度。不仅要为多个从节点提供转发层(如Proxy, LVS等) 来实现负载均衡,还要考虑从节点数量显著增加后带来故障率增加的问题。Redis集群架构变更会为监控、运维、故障处理带来了更大的挑战。
Elastic Search
ES是一个分布式、高扩展、近实时的搜索和数据分析引擎。用于海量数据的检索引擎、时序分析、大数据分析、NoSQL的索引引擎,日志存储灯
术语 | 描述 | 示例用法 | 数据库对比概念 |
---|---|---|---|
字段 Field | 描述每一个列的名字,是文档的组成单元,包含字段名称、字段属性和字段内容 | 商户名 、城市名 分别是一个字段 | 列 |
字段属性 Attributes | 描述字段的属性,包含是否建倒排、正排、分词、值的类型(int \ String) | 城市名 是一个字符串类型,不需要分词等 | 类似varchar(16)、int(20),是否index,primary_index等 |
文档 Document | 可搜索的结构化数据单元,用于描述一整条记录,由多个字段组成。使用JSON格式存储 | 湖南省长沙市韶山南路KFC商户 可以作为一个文档 | 一个记录行 |
索引 Index | 描述多个行记录的集合 | 所有的商户 放在一个索引里 | 表 |
正排 | 文档到字段对应关系组成的链表,勾选可过滤后会构成正排链表。需要设置docvalues=true | doc1 -> id, type, create_time | 行记录 |
倒排 | 词组到文档的对应关系组成的链表,勾选可搜索后会构建倒排链表。需要设置index=true,如果为false,对应的字段与将不能进行检索(即执行各种查询后返回的结果为空或者直接报错) | term1 -> doc1, doc2, doc3; term2 -> doc1, doc2 | 类似B+树索引。MySQL不设置索引还是可以进行查询,但是ES不设置查询的话,结果为空 |
召回 | 通过用户查询的关键词进行分词,将分词后的词组通过查找倒排链表快速定位到文档的过程 | 查询过程 | |
召回量 totalhits | 召回得到的文档数量 | 查询返回的结果数 | |
分片 Shard | 一个索引由多个分片组成,每个分片都具备索引相同的数据结构 | 一个索引分成N个shard,每一个shard的内容就是这个完整索引内容的1/N | |
副本 Replica | 每个主分片(Primary Shard)可以有多个副本分片(Replica Shard),用于提高可用性和查询性能。 | ||
段 Segment | 分片的组成单元,多个段构成一个分片,段是检索的基本单元,所有的更新/查询都是基于段 | ||
段合并 | Lucene的删除是标记删除,更新是先删后增,随着数据不断地更新,一个分片中会累计很多段(这些段里存在很多已经删掉的文档),段太多会导致查询性能变慢,因此我们需要一个段合并的过程,把哪些没有用的数据清除 |
底层原理
基于 Lucene 的强大搜索和索引能力,结合了分布式架构、反向索引、近实时处理和自动分片与副本管理等技术,提供通用查询语法、摘要、高亮、相关性(BM25等算法)查询基础库,非常适合需要快速检索和分析大规模数据的应用场景。
Lucene倒排索引
底层基于Lucene
,使用倒排索引为核心检索原理(允许快速查找文档中包含特定词语的所有位置。倒排索引类似于书的索引,通过词汇快速定位包含该词的文档。文本被分割成一个个词条(Term),每个词条被记录到倒排索引中,以便快速查找。)
分布式架构
Elasticsearch 的设计是分布式的,支持自动分片和副本管理,集群可以由多个节点组成。
- 节点(Node): Elasticsearch 集群中的一个实例,可以存储数据并参与集群的索引和查询任务。
- 集群(Cluster): 由多个节点组成,共享同一个集群名称。集群中的节点协同工作以存储数据和分布查询负载。
- 分片管理: 分片和副本的分配、迁移和重新分配是由集群管理器(Master Node)自动完成的。当集群增加或减少节点时,Elasticsearch 自动平衡分片和副本以优化性能和容错。
近实时搜索(NRT)
Elasticsearch 提供近实时(Near Real-Time, NRT)搜索能力,这意味着数据的索引和查询之间的延迟非常短。
- 刷新机制: Elasticsearch 定期将内存中的索引缓冲区刷新到磁盘上的段(Segment)中。默认刷新间隔为 1 秒,因此新数据几乎可以实时搜索。
- 段合并: 随着索引数据的增加,Lucene 会将小段合并为较大的段,以优化查询性能。这一过程在后台自动进行。
查询与过滤
- Query DSL: Elasticsearch 提供了一种基于 JSON 的查询语言,称为 Query DSL,用于构建复杂的查询请求。
- 查询处理: 查询请求被路由到集群中的相关分片,分片并行执行查询,并将结果聚合后返回给客户端。
- 评分(Scoring): Elasticsearch 使用基于 TF-IDF(词频-逆文档频率)和其他算法的评分机制,来评估文档与查询的匹配程度。
写入和更新机制
- 写入流程: 写入数据时,数据首先被写入到内存中的缓冲区,并被记录到事务日志中,以确保数据的持久性。
- 更新与删除: 更新操作实际上是删除旧文档并插入新文档。删除操作将文档标记为“已删除”,并在后续段合并时物理删除。
副本与高可用
- 高可用性: 每个主分片有一个或多个副本分片。主分片或副本分片失效时,集群会自动从可用的副本中提升一个为新的主分片,以确保数据的高可用性。
- 故障恢复: 如果节点故障,Elasticsearch 会重新分配该节点上的分片到其他节点,以恢复集群的正常运行。
聚合与分析
- 聚合框架: Elasticsearch 提供了一个强大的聚合框架,用于执行复杂的数据分析操作,如统计、分组、求和、平均、最大最小值计算等。
- 分布式聚合: 聚合查询是分布式执行的,结果在各分片上部分计算,再由协调节点进行合并,最终返回给用户。
Demo
最佳实践
整体流程
- 创建集群 -> 启动 -> 通过8080访问查看返回信息
- 创建索引(相当于建表) -> 添加数据 -> 查询
public class ElasticsearchDemo {
public static void main(String[] args) {
// 创建 ES 客户端
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("localhost", 9200, "http"))
);
try {
// 1. 创建索引
CreateIndexRequest createIndexRequest = new CreateIndexRequest("products");
CreateIndexResponse createIndexResponse = client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
System.out.println("索引创建成功:" + createIndexResponse.index());
// 2. 添加数据
Map<String, Object> jsonMap = new HashMap<>();
jsonMap.put("name", "Elasticsearch Guide");
jsonMap.put("price", 29.99);
jsonMap.put("category", "Books");
IndexRequest indexRequest = new IndexRequest("products")
.id("1") // 设置文档 ID
.source(jsonMap, XContentType.JSON);
IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
System.out.println("文档添加成功,ID:" + indexResponse.getId());
// 3. 查询数据
SearchRequest searchRequest = new SearchRequest("products");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchQuery("name", "Elasticsearch"));
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
System.out.println("查询结果:");
for (SearchHit hit : searchResponse.getHits().getHits()) {
System.out.println(hit.getSourceAsString());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭客户端
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
ES查询语句
Elasticsearch 查询语句使用 JSON 格式的 DSL(Domain Specific Language)来描述,支持多种查询方式,可以执行复杂的搜索和过滤操作。
1. 基本查询(match\term\range)
match
查询:匹配一个字段中的文本,适用于全文搜索。
GET /products/_search
{
"query": {
"match": {
"name": "Elasticsearch Guide"
}
}
}
term
查询:精确匹配某个字段的值,通常用于结构化数据。
GET /products/_search
{
"query": {
"term": {
"category": "Books"
}
}
}
range
查询:范围查询,常用于数值或日期范围的搜索。
GET /products/_search
{
"query": {
"range": {
"price": {
"gte": 20,
"lte": 50
}
}
}
}
2. 组合查询(bool)
组合多个查询条件,包含 must
、should
、must_not
、filter
。
GET /products/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "Elasticsearch" } }
],
"filter": [
{ "term": { "category": "Books" } },
{ "range": { "price": { "lte": 50 } } }
]
}
}
}
3. 聚合查询(terms\avg\嵌套)
用于执行统计、分组等操作,类似 SQL 中的 GROUP BY
。
terms
聚合:按某个字段进行分组,并计算每组的文档数量。
GET /products/_search
{
"size": 0,
"aggs": {
"category_count": {
"terms": {
"field": "category.keyword"
}
}
}
}
avg
聚合:计算字段的平均值。
GET /products/_search
{
"size": 0,
"aggs": {
"average_price": {
"avg": {
"field": "price"
}
}
}
}
- 嵌套聚合:组合多种聚合操作,比如先按类别分组,再计算每组的平均价格。
GET /products/_search
{
"size": 0,
"aggs": {
"category_avg_price": {
"terms": {
"field": "category.keyword"
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
4. 排序和分页(sort\from)
- 排序:按字段排序结果,默认升序,使用
"order": "desc"
设置降序。
GET /products/_search
{
"query": {
"match_all": {}
},
"sort": [
{ "price": { "order": "asc" } }
]
}
- 分页:通过
from
和size
控制返回结果的起始位置和数量。
GET /products/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 10
}
5. 全文检索(multi_match\fuzzy)
multi_match
查询:在多个字段中执行全文搜索。
GET /products/_search
{
"query": {
"multi_match": {
"query": "guide",
"fields": ["name", "description"]
}
}
}
fuzzy
查询:模糊查询,用于查找拼写错误或相似的文本。
GET /products/_search
{
"query": {
"fuzzy": {
"name": {
"value": "Elasticsearch",
"fuzziness": "AUTO"
}
}
}
}
6. 复杂查询(nested)
- 嵌套查询(nested):用于查询嵌套的 JSON 对象。
GET /products/_search
{
"query": {
"nested": {
"path": "attributes",
"query": {
"bool": {
"must": [
{ "match": { "attributes.name": "color" } },
{ "match": { "attributes.value": "blue" } }
]
}
}
}
}
}
应用场景
全文搜索
-
网站搜索: 用于实现网站、电子商务平台、内容管理系统的搜索功能,如电商网站的商品搜索、博客网站的文章搜索等。
-
文档搜索: 支持对各种格式的文档(如 PDF、Word、文本文件等)的全文搜索,适用于文档管理系统。
-
特点
- 支持快速、精确的全文搜索,能够处理模糊匹配、拼写纠正、同义词匹配等。
- 提供高亮显示、分词、排序和过滤等功能,提升用户搜索体验。
实时日志和事件数据分析
-
日志监控: 通过与
Logstash
和Kibana
组成 ELK(Elasticsearch, Logstash, Kibana)或 EFK(Elasticsearch, Fluentd, Kibana)栈,用于日志收集、存储、分析和可视化。适用于系统日志、应用日志、网络日志等监控。 -
安全事件管理: 用于安全信息和事件管理(SIEM)系统,实时监控和分析安全事件和威胁。
-
特点
- 高性能的实时数据写入和查询,适合处理大量日志数据。
- 支持复杂的聚合分析,可以实时生成监控指标和报警。
实时分析和可视化
-
业务数据分析: 例如网站访问量统计、用户行为分析、销售数据分析等,通过 Kibana 实现实时数据可视化。
-
IoT 数据处理: 处理来自物联网设备的大量实时数据,用于分析设备状态、性能监控、预测性维护等。
-
特点
- 提供强大的聚合和分析能力,可以快速执行统计、汇总、平均、最大最小值等操作。
- 与 Kibana 无缝集成,支持多种数据的图形化展示。
分布式应用的分片存储
-
分布式数据存储: 支持分布式索引和存储,自动分片和复制数据,保证高可用性和可扩展性。
-
大型数据集存储: 适合存储和查询 PB 级别的大型数据集,广泛用于大数据平台。
-
特点
- 支持横向扩展,可以动态增加节点来扩展存储和计算能力。
- 提供高可用的数据分片和副本机制,确保数据的可靠性和查询的负载均衡。
推荐系统
-
个性化推荐: 在电子商务、内容平台等场景中,基于用户的历史行为和搜索历史,提供个性化的商品、内容推荐。
-
特点
- 通过查询和过滤条件,可以快速检索与用户兴趣匹配的数据。
- 结合其他机器学习算法,提升推荐的精准度。
地理空间搜索
-
地理位置服务: 适用于需要处理地理数据的应用场景,如“附近的商家”搜索、基于位置的广告投放等。
-
特点
- 支持地理坐标的索引和查询,可以执行地理围栏、距离计算、地理聚合等操作。
指标监控和性能分析
-
系统性能监控: 收集和分析系统性能指标,如 CPU、内存、磁盘 I/O、网络带宽等,用于性能调优和瓶颈识别。
-
应用监控: 追踪应用性能指标,如响应时间、错误率等,用于 SLA 管理和问题诊断。
-
特点
- 实时的指标采集和分析,支持复杂的聚合和时序数据处理。
- 与 APM(应用性能管理)系统集成,提供对应用的端到端监控。
大数据分析
-
数据仓库: 作为一个轻量级的数据仓库,支持结构化和非结构化数据的快速查询和分析。
-
数据湖: 可以与 Hadoop、Spark 等大数据生态系统集成,用于数据湖的查询和数据索引。
-
特点:
- 通过 RESTful API 和多种编程语言的客户端,便于与大数据框架集成。
- 高并发的查询性能和复杂的聚合分析能力,支持对大数据集的快速探索。