网上看到不错的:

20道经典Redis面试题_CSDN砖家的博客-CSDN博客

SQL 与 NoSQL

1、结构化 与 非结构化:

2、sql查询 与 非sql查询

认识redis

[Redis常见面试题_俺叫啥好嘞的博客-CSDN博客](https://blog.csdn.net/qq_42946963/article/details/109717584?ops_request_misc=%7B%22request%5Fid%22%3A%22169106869216800222834576%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=169106869216800222834576&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~baidu_landing_v2~default-1-109717584-null-null.142^v92^insert_down28v1&utm_term=redis 键值对的有效期&spm=1018.2226.3001.4187)

redis是一个基于内存的NoSQL数据库。

特征:

  1. 键值(key-value)型,value支持多种不同数据结构,功能丰富

  2. 单线程,每个命令具备原子性

3) 低延迟,速度快(基于内存、IO多路复用、良好的编码)。适合存储热点数据(热点商品、新闻)

4) 支持数据持久化 (RDB,AOF)

  1. 支持主从集群、分片集群

  2. 支持多语言客户端

redis为什么快?

redis为什么快?_redis为什么速度快_乱糟的博客-CSDN博客

  1. redis的操作都是基于内存的。

  2. 使用单线程可以省去多线程时CPU上下文会切换的时间,也不用去考虑各种锁的问题,不存在加锁释放锁操作,没有死锁问题导致的性能消耗。

3) IO多路复用

4) Redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表、跳表。

使用场景:

适合存储热点数据(热点商品、新闻)、秒杀的库存扣减、APP首页的访问流量高峰

redis数据结构

Redis是一个key-value的数据库,key一般是String类型,不过value的类型多种多样:

key的结构

Redis的key允许有多个单词形成层级结构,多个单词之间用':'隔开,格式如下:

例如我们的项目名称叫 heima,有user和product两种不同类型的数据,我们可以这样定义key:

  • user相关的key:heima:user:1

  • product相关的key:heima:product:1

string类型

如果Value是一个Java对象,例如一个User对象,则可以将对象序列化为JSON字符串后存储:

hash类型

Hash类型,其value是一个无序字典,Hash结构可以将对象中的每个字段独立存储,非常适合存储对象,可以针对单个字段做CRUD:

List类型

Redis中的List类型与Java中的LinkedList类似,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。特征也与LinkedList类似:

quicklist

在Redis3 .2版本之前,存储列表(list)数据结构使用的是 压缩列表(ziplist)和链表(linkedlist),在Redis3 .2版本开始对列表数据结构进行了改造,使用 快速列表(quicklist)代替了压缩列表(ziplist)和链表(linkedlist)。

quicklist它是由链表和压缩链表结合起来的一种结构, 即**是一个双向链表, 并且链表中的每一个节点是一个压缩链表.**

quicklist优点:

  1. 内存优化:Quicklist的一个主要优点是它可以节省内存。它使用了一种紧凑的数据结构,可以在存储大量列表元素时减少内存的使用。Quicklist的内部结构允许Redis有效地存储大型列表,而不会导致内存浪费。

  2. 分片存储:Quicklist使用分片存储的方法,将大的列表分割成多个小块。这有助于降低内存占用,并且在插入和删除元素时不需要移动大量的数据,因为只需修改适当的分片即可。

3) 高性能:Quicklist设计用于高性能。它可以快速执行列表操作,如插入、删除、查找等。这对于Redis的主要用途之一,即作为高性能缓存存储系统,非常重要。

4) 写入优化:Quicklist使用ziplist(一种紧凑的、可变长度的编码格式)来存储列表元素,这有助于优化写入操作。Redis可以在不必完全解码整个列表的情况下执行一些写入操作,从而提高了性能。

  1. 有序

  2. 元素可以重复

3) 插入和删除快

4) 查询速度一般

常用来存储一个有序数据,例如:朋友圈点赞列表,评论列表等。

set类型

Redis 中集合是通过哈希表实现的,可以看做是一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征:

  1. 无序

  2. 元素不可重复

3) 查找快

4) 支持交集、并集、差集等功能

SortedSet类型

Redis的SortedSet是一个可排序的set集合,SortedSet中的每一个元素都带有一个score属性,可以基于score属性对元素排序。

SortedSet具备下列特性:

  1. 可排序

  2. 元素不重复

3) 查询速度快

因为SortedSet的可排序特性,经常被用来实现排行榜这样的功能。

Redis通用命令

  1. KEYS:查看符合模板的所有key

  2. DEL:删除一个指定的key

3) EXISTS:判断key是否存在

4) EXPIRE:给一个key设置有效期,有效期到期时该key会被自动删除

5) 例如,要将键为mykey的元素设置为在60秒后过期:

EXPIRE mykey 60
  1. 也可以使用SETEX命令添加一个键值对时设置其有效期:

SETEX mykey 60 "Hello"
  1. TTL:查看一个KEY的剩余有效期

5) 例如,如果要查看键为mykey的元素的剩余有效期,可以执行以下命令:

TTL mykey

通过help [command] 可以查看一个命令的具体用法

Redis是基于内存的,使用时要注意考虑哪些问题:

  1. 内存使用量

  2. 持久化、数据备份、数据恢复、和数据丢失

3) 内存淘汰策略

4) 内存碎片问题

Redis中的zset的底层实现:

zset底层的存储结构包括ziplist或skiplist,在同时满足以下两个条件的时候使用ziplist,其他时候使用skiplist,两个条件如下:

  • 有序集合保存的元素数量小于128个

  • 有序集合保存的所有元素的长度小于64字节

ziplist:

ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存。

第一个节点保存元素的成员,第二个节点保存元素的分值。

并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。

skiplist:

O(logN)

为什么不用红黑树:

  • 跳表的实现很简单,至少比红黑树简单的多。

  • 多范围查询的支持,跳表完胜红黑树。

Redis的Hash 冲突怎么办:

Redis 作为一个K-V的内存数据库,它使用用一张全局的哈希来保存所有的键值对。

Redis为了解决哈希冲突,采用了链式哈希

为了保持高效,Redis 会对哈希表做rehash操作,也就是增加哈希桶,减少冲突。为了rehash更高效,Redis还默认使用了两个全局哈希表,一个用于当前使用,称为主哈希表,一个用于扩容,称为备用哈希表

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表:
keys pre*

这个时候面试官会追问该命令对线上业务有什么影响,直接看下一个问题。

如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

redis 的单线程的。keys 指令会导致线 程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时 候可以使用 scan 指令,

scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间 会比直接用 keys 指令长。

keys和scan:

keys命令有两个缺点:

  1. 没有limit,我们只能一次性获取所有符合条件的key,如果结果有上百万条,那么等待你的就是“无穷无尽”的字符串输出。

  2. keys命令是遍历算法,时间复杂度是O(N)。如我们刚才所说,这个命令非常容易导致Redis服务卡顿。因此,我们要尽量避免在生产环境使用该命令。

相比于keys命令,scan命令有两个比较明显的优势:

  1. scan命令的时间复杂度虽然也是O(N),但它是分次进行的,不会阻塞线程。

  2. scan命令提供了limit参数,可以控制每次返回结果的最大条数。

scan用法:

格式

SCAN cursor [MATCH pattern] [COUNT count]

其中,各参数的含义如下:

  • cursor:迭代的游标,表示当前迭代的位置。开始时通常使用0作为起始游标。

  • MATCH pattern(可选):用于匹配键的模式。只有与指定模式匹配的键才会被返回。

  • COUNT count(可选):每次迭代返回的键的最大数量。这有助于分批获取数据,以减轻服务器负载。

SCAN命令返回一个包含两个元素的数组:第一个元素是下一个迭代的游标,第二个元素是迭代期间返回的键数组。

MATCH参数可以用于过滤特定模式的键,例如:

127.0.0.1:6379> SCAN 0 MATCH "user:*"
1) "0"
2) 1) "user:1"
   2) "user:2"
   3) "user:3"

COUNT参数可以用于指定每次迭代返回的键的数量,以控制返回结果的大小:

127.0.0.1:6379> SCAN 0 COUNT 2
1) "0"
2) 1) "key1"
   2) "key2"

为什么Redis 6.0 之后改多线程呢?

redis使用多线程并非是完全摒弃单线程,只是使用多线程来处理数据的读写和协议解析,执行命令还是使用单线程。

这样做的目的是因为redis的性能瓶颈在于IO而非CPU,使用多线程能提升IO读写的效率,从而整体提高redis的性能。

Redis的过期策略

我们在set key的时候,可以给它设置一个过期时间,比如expire key 60。指定这key60s后过期,60s后,redis是如何处理的嘛?我们先来介绍几种过期策略:

  1. 定时过期

1) 每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。

  1. 优点:该策略可以立即清除过期的数据,对内存很友好;

1) 缺点:但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

2) 惰性过期

3) 只有当访问一个key时,才会判断该key是否已过期,过期则清除。

4) 优点:该策略可以最大化地节省CPU资源

5) 缺点:对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。

  1. 定期过期

3) 每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。

  1. expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。

Redis中同时使用了惰性过期和定期过期相结合的过期策略:

  1. 每隔100ms就随机抽取一定数量的key来检查和删除。但是呢,最后可能会有很多已经过期的key没被删除。

  2. 这时候,redis采用惰性删除。在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间并且已经过期了,此时就会删除。

lazy-free :指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。

缓存

什么情况下考虑使⽤缓存:

  1. 不需要实时更新又极其消耗数据库的数据

  2. 需要实时更新,但更新的频率不⾼

缓存一致性问题:

1.只读:

不会有一致性问题

2.读写并发:

四种操作:

  1. 先更新缓存,再更新数据库

  2. 先删除缓存,再更新数据库

3) 先更新数据库,再更新缓存

4) 先更新数据库,再删除缓存 (推荐)

2.1 建议删除缓存:

1.删除相比更新逻辑更简单,成本更低。

2.更新的缓存并不一定会马上用到,相比删除缓存可能会产生冗余

2.2 先删除缓存,还是先操作数据库?

因为缓存操作的速度远超操作mysql,虽然两种策略都可能出现问题,但是先操作数据库后删除缓存出现故障的概率更低。

2.2.1 先缓存,后数据库 的数据不一致问题:

解决:

  1. 强一致性:

1) 对redis和数据库的操作必须是原子性的。

  1. 即需要加锁:性能下降,影响吞吐量

  2. 延迟双删操作:

  3. 数据库被更新之后,再对redis进行一次删除操作。

  4. 这样就保证了最终的数据一致性,只会出现最开始的一次数据不一致。

  5. 为什么延时:等查线程把脏数据写道缓存中再删,不然就是删了个寂寞

2.2.2 先数据库,后缓存 的数据不一致问题

在redis的数据被更新时,如果有读操作,就会读到老数据。

2.2.2.1 删除重试:

删除redis失败:

  1. 发一个异步消息(删除失败的key)到mq中,执行删除重试。

1) 缺点:代码变复杂,耦合度提高。

  1. 阿里巴巴的开源框架cannal可以监听数据库的数据变动(binlog),而后cannal客户端(例如一个spring应用)去执行删除重试。

Redis 做缓存的两种模式:

1、只读缓存

加强读请求性能。查询数据时,缓存缺失需要从DB加载。更新数据时到DB更新,Redis上的⽼数据直接删除。

优点:

所有最新的数据都在数据库中,数据不存在丢失的风险。

缺点:

每次修改数据,都会删除缓冲,之后的请求会发生一次缓存缺失。

2、读写缓存:

写操作分为同步直写和异步写回两种模式,根据实际的业务场景需求来进⾏选择:

  1. 同步直写模式

1) Redis和DB同时删改写回,等到缓存和数据库都写完数据,才给客户端返回。侧重于保证数据可靠性。

  1. 优点:

1) 即使缓存宕机或发生故障,最新的数据仍然保存在数据库中,这就提供了数据可靠性保证。

  1. 缺点:

1) 增加了缓存的响应延迟。

2) 异步写回模式

3) 所有写请求都先在缓存中处理。可以定时将缓存写入到内存中,然后等到这些增改的数据要被从缓存中淘汰出来时,再次将它们写回后端数据库。侧重于提供低延迟访问。

4) 优点:

5) 被修改的数据永远在缓存中,不会发生缓存缺失,下次可以直接访问,不在需要向数据库中进行一次查询。

6) 缺点:

7) 数据可能存在丢失的风险。

缓存更新策略

缓存更新是redis为了节约内存而设计出来的一个东西,主要是因为内存数据宝贵,当我们向redis插入太多数据,此时就可能会导致缓存中的数据过多,所以redis会对部分数据进行更新,或者把他叫为淘汰更合适。

  1. 内存淘汰:redis自动进行,当redis内存达到咱们设定的max-memery的时候,会自动触发淘汰机制,淘汰掉一些不重要的数据(可以自己设置策略方式)

  2. 超时剔除:当我们给redis设置了过期时间ttl之后,redis会将超时的数据进行删除

3) 主动更新:我们可以手动调用方法把缓存删掉,通常用于解决缓存和数据库不一致问题

缓存淘汰策略

Redis总共有8种淘汰策略:

1、不进⾏数据淘汰的策略

  1. noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。

2、会进⾏数据淘汰的策略:

(1)在设置了过期时间的数据中进⾏淘汰

4种:volatile-ttl、volatile-random、volatile-lru、volatile-lfu。

  1. volatile-lru:从已设置过期时间的数据集中,挑选最近最少使⽤的数据淘汰

  2. volatile-ttl:从已设置过期时间的数据集中,选择将要过期的数据进⾏淘汰

3) volatile-random:从已设置过期时间的数据集中,随机选择数据进⾏淘汰

(2)所有数据范围内进⾏淘汰

3种:allkeys-random、allkeys-lru、allkeys-lfu。

  1. allkeys-lru:从数据集中,挑选最近最少使⽤的数据淘汰 (LRU)

  2. allkeys-random:从数据集中,随机选择数据进⾏淘汰

缓存穿透:

一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

解决方法:

(1)对查询结果为空的情况也进行缓存。 优点:简单方便 缺点:额外的内存消耗。

(2)布隆过滤器:对一定不存在的key进行过滤。

(3)增加id的复杂度,避免被猜到id规律

缓存雪崩:

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方法:

  1. 多级缓存:比如A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。

  2. 随机TTL:给key的失效时间设置为随机时间,避免集体过期

3) 给缓存业务添加降级限流策略。

缓存击穿:

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

解决方法:

(1)互斥锁:给重建缓存数据这一操作加锁,只有一个线程可以进行热点数据的重构,其他都等待 优点:实现简单;保证一致性;没有额外的内存消耗; 缺点:现成需要等待,性能受到影响;可能有死锁的风险

(2)逻辑过期:不给该数据设置过期时间,而是在数据的value里添加一个expire属性 优点:无需等待,性能好。 缺点:不保证一致性;额外内存消耗;实现复杂

缓存预热

缓存预热并非一个问题,而是使用缓存手段时的一种优化方案。

缓存预热是指在系统启动的时候,先把查询结果预存到缓存当中,以便用户后面查询时可以直接从缓存中读取,以节约用户的等待时间。

缓存预热的优点:

  1. 解决上面的问题,可以让用户始终访问很快

缺点:

  1. 增加开发成本(你要额外的开发、设计)

  2. 预热的时机和时间如果错了,有可能你缓存的数据不对或者太老

3) 需要占用额外空间

Redis持久化

Redis持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失;

Redis 提供了两种持久化方式:RDB(默认) 和AOF

RDB:

每隔一定的时间将内存的数据以快照的形式保存到硬盘中。

优点:

  1. 只有一个文件 dump.rdb,方便持久化;

  2. 性能最大化,保证了Redis的高性能;

缺点:

  1. 数据安全性低:RDB 是间隔一段时间进行持久化,如果在持久化前Redis发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候;

在生成 RDB期间,Redis 可以同时处理写请求么?

  • 如果是save指令,会阻塞,因为是主线程执行的。

  • 如果是bgsave指令,是fork一个子进程来写入RDB文件的,快照持久化完全交给子进程来处理,父进程则可以继续处理客户端的请求。

AOF:

每次写命令记录到日志文件中

优点:

  1. 数据安全,AOF持久化可以配置 appendfsync 属性,有always属性,每进行一次命令操作就记录到AOF文件中一次(AOF 持久化的fsync策略为 no、everysec 时都会存在数据丢失的情况 。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。);

缺点:

  1. AOF文件比RDB文件大,且恢复速度慢;

  2. 数据集大的时候,比RDB启动效率低;

RDB对比AOF:

  • AOF文件比RDB更新频率高,优先使用AOF还原数据;

  • AOF比RDB更安全也更大;

  • RDB性能比AOF好;

  • 如果两个都配了优先加载AOF;

redis事务:

Redis之Redis事务_redis 有没有事物概念_封宇宸Potter的博客-CSDN博客

一、Redis事务的概念

Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。 总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

二、Redis不保证原子性,没有隔离级别的概念

  1. 单条命令是原子性执行的,但事务不保证原子性,

  2. 没有回滚事务中任意命令执行失败,其余的命令仍会被执行。

三、Redis事务的三个阶段

  1. 开始事务

  2. 命令入队

3) 执行事务

四、redis事务相关的命令

(1)WATCH可以为Redis事务提供 check-and-set (CAS)行为。被WATCH的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。

(2)MULTI

用于开启一个事务,它总是返回OK。MULTI执行之后,客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行,而是被放到一个队列中,当 EXEC命令被调用时, 所有队列中的命令才会被执行。

(3)UNWATCH

取消 WATCH 命令对所有 key 的监视,一般用于DISCARD和EXEC命令之前。如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行 UNWATCH 了。因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。

(4)DISCARD

当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空,并且客户端会从事务状态中退出。

(5)EXEC

负责触发并执行事务中的所有命令:

如果客户端成功开启事务后执行EXEC,那么事务中的所有命令都会被执行。

如果客户端在使用MULTI开启了事务后,却因为断线而没有成功执行EXEC,那么事务中的所有命令都不会被执行。需要特别注意的是:即使事务中有某条/某些命令执行失败了,事务队列中的其他命令仍然会继续执行,Redis不会停止执行事务中的命令,而不会像我们通常使用的关系型数据库一样进行回滚。

Redis模式:

Redis的三种模式——主从复制、哨兵、集群_redis集群三种方式_不回头的蛙的博客-CSDN博客

Redis有三种模式:分别是主从同步/复制、哨兵模式、Cluster

  1. 主从复制:主从复制是高可用Redis的基础,哨兵和群集都是在主从复制基础上实现高可用的。主从复制主要实现了数据的多机备份,以及对于读操作的负载均衡和简单故障恢复。

1) 缺陷:故障恢复无法自动化,写操作无法负载均衡,存储能力受到单机的限制。

2) 哨兵:在主从复制的基础上,哨兵实现了自动化的故障恢复。 缺陷:写操作无法负载均衡,存储能力受到单机的限制,哨兵无法对从节点进行自动故障转移;在读写分离场景下,从节点故障会导致读服务不可用,需要对从节点做额外的监控、切换操作。

  1. 集群:通过集群,Redis解决了写操作无法负载均衡,以及存储能力受到单机限制的问题,实现了较为完善的高可用方案。

一、Redis主从复制:

作用:

  1. 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

  2. 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

3) 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务 (即写 Redis 数据时应用连接主节点,读 Redis 数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

4) 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

主从复制的过程:

主从复制包括全量复制,增量复制两种。一般当slave第一次启动连接master,或者认为是第一次连接,就采用全量复制,全量复制流程如下:

redis2.8版本之后,已经使用psync来替代sync,因为sync命令非常消耗系统资源,psync的效率更高。

slave与master全量同步之后,master上的数据,如果再次发生更新,就会触发增量复制

当master节点发生数据增减时,就会触发replicationFeedSalves()函数,接下来在 Master节点上调用的每一个命令会使用replicationFeedSlaves()来同步到Slave节点。执行此函数之前呢,master节点会判断用户执行的命令是否有数据更新,如果有数据更新的话,并且slave节点不为空,就会执行此函数。这个函数作用就是:把用户执行的命令发送到所有的slave节点,让slave节点执行。

什么是增量同步?就是只更新slave与master存在差异的部分数据。如图:

那么master怎么知道slave与自己的数据差异在哪里呢?

2.2.3.repl_backlog原理

master怎么知道slave与自己的数据差异在哪里呢?

这就要说到全量同步时的repl_baklog文件了。

这个文件是一个固定大小的数组,只不过数组是环形,也就是说角标到达数组末尾后,会再次从0开始读写,这样数组头部的数据就会被覆盖。

repl_baklog中会记录Redis处理过的命令日志及offset,包括master当前的offset,和slave已经拷贝到的offset:

slave与master的offset之间的差异,就是salve需要增量拷贝的数据了。

随着不断有数据写入,master的offset逐渐变大,slave也不断的拷贝,追赶master的offset:

直到数组被填满:

此时,如果有新的数据写入,就会覆盖数组中的旧数据。不过,旧的数据只要是绿色的,说明是已经被同步到slave的数据,即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。

但是,如果slave出现网络阻塞,导致master的offset远远超过了slave的offset:

如果master继续写入新数据,其offset就会覆盖旧的数据,直到将slave现在的offset也覆盖:

棕色框中的红色部分,就是尚未同步,但是却已经被覆盖的数据。此时如果slave恢复,需要同步,却发现自己的offset都没有了,无法完成增量同步了。只能做全量同步。

2.3.主从同步优化

主从同步可以保证主从数据的一致性,非常重要。

可以从以下几个方面来优化Redis主从就集群:

  • 在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。

  • Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO

  • 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步

  • 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力

主从从架构图:

2.4.小结

简述全量同步和增量同步区别?

  • 全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。

  • 增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave

什么时候执行全量同步?

  • slave节点第一次连接master节点时

  • slave节点断开时间太久,repl_baklog中的offset已经被覆盖时

什么时候执行增量同步?

  • slave节点断开又恢复,并且在repl_baklog中能找到offset时

二、Redis哨兵模式

主从切换技术的方法是:当服务器宕机后,需要手动一台从机切换为主机,这需要人工干预,不仅费时费力,而且还会造成一段时间内服务不可用。为了解决主从复制的缺点,就有了哨兵模式。

3.1.1.集群结构和作用

哨兵的结构如图:

哨兵的作用如下:

  • 监控:Sentinel 会不断检查您的master和slave是否按预期工作

  • 自动故障恢复:如果master故障,Sentinel会投票选举一个slave提升为master。当故障实例恢复后也以新的master为主

  • 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

3.1.2.集群监控原理

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:

•主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线

•客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。

3.1.3.集群故障恢复原理

一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:

  • 首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点

  • 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举

  • 如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高

  • 最后是判断slave节点的运行id大小,越小优先级越高。

当选出一个新的master后,该如何实现切换呢?

流程如下:

  • sentinel给备选的slave1节点发送slaveof no one命令,让该节点成为master

  • sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令,让这些slave成为新master的从节点,开始从新的master上同步数据。

  • 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点

3.1.4.小结

Sentinel的三个作用是什么?

  • 监控

  • 故障转移

  • 通知

Sentinel如何判断一个redis实例是否健康?

  • 每隔1秒发送一次ping命令,如果超过一定时间没有相向则认为是主观下线

  • 如果大多数sentinel都认为实例主观下线,则判定服务下线

故障转移步骤有哪些?

  • 首先选定一个slave作为新的master,执行slaveof no one

  • 然后让所有节点都执行slaveof 新master

  • 修改故障节点配置,添加slaveof 新master

三、Redis分片集群

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:

  • 海量数据存储问题

  • 高并发写的问题

使用分片集群可以解决上述问题,如图:

分片集群特征:

  • 集群中有多个master,每个master保存不同数据

  • 每个master都可以有多个slave节点

  • master之间通过ping监测彼此健康状态

  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

集群中的节点分为主节点和从节点;只有主节点负责读写请求和集群信息的维护;从节点只进行主节点数据和状态信息的复制。

  1. 数据分区:数据分区(或称数据分片) 是集群最核心的功能。

1) 1)集群将数据分散到多个节点,一方面突破了 Redis 单机内存大小的限制,存储容量大大增加

  1. 2)另一方面每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。

1) Redis 单机内存大小受限问题,在介绍持久化和主从复制时都有提及;例如,如果单机内存太大,bgsave 和 bgrewriteaof的 fork 操作可能导致主进程阻塞,主从环境下主机切换时可能导致从节点长时间无法提供服务,全量复制阶段主节点的复制缓冲区可能溢出。

2) 高可用:集群支持 主从复制 和 主节点的自动故障转移(与哨兵类似) ;当任一节点发生故障时,集群仍然可以对外提供服务。

Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到:

以3个节点组成的集群为例:
1)节点A包含0到5461号哈希槽
2)节点B包含5462到10922号哈希槽
3)节点C包含10923到16383号哈希槽

数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:

  • key中包含"{}",且“{}”中至少包含1个字符,“{}”中的部分是有效部分

  • key中不包含“{}”,整个key都是有效部分

例如:key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作

分布式锁

Redis的分布式锁详解_redis分布式锁_张维鹏的博客-CSDN博客

分布式锁,就是解决了分布式系统中控制共享资源访问的问题。

基于set命令的分布式锁

SETNX 是 Redis 中的一条命令,用于设置指定键(key)的值,但仅当该键不存在时才会设置成功。

并返回 1 表示设置成功。如果 key 已经存在,SETNX 命令不会对 key 进行任何更改,并返回 0 表示设置失败。

1、加锁:使用setnx进行加锁,当该指令返回1时,说明成功获得锁

2、解锁:当得到锁的线程执行完任务之后,使用del命令释放锁,以便其他线程可以继续执行setnx命令来获得锁

(1)存在的问题:假设线程获取了锁之后,在执行任务的过程中挂掉,来不及显示地执行del命令释放锁,那么竞争该锁的线程都会执行不了,产生死锁的情况。

(2)解决方案:设置锁超时时间

3、设置锁超时时间:setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。可以使用expire命令设置锁超时时间

(1)存在问题:

setnx 和 expire 不是原子性的操作,假设某个线程执行setnx 命令,成功获得了锁,但是还没来得及执行expire 命令,服务器就挂掉了,这样一来,这把锁就没有设置过期时间了,变成了死锁,别的线程再也没有办法获得锁了。

(2)解决方案:

redis的set命令支持在获取锁的同时设置key的过期时间

4、使用set命令加锁并设置锁过期时间:

命令格式:set <lock.key> <lock.value> nx ex

(1)存在问题:释放掉别人的锁

1.假如线程A成功得到了锁,并且设置的超时时间是 30 秒。如果某些原因导致线程 A 执行的很慢,过了 30 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。

2.随后,线程A执行完任务,接着执行del指令来释放锁。但这时候线程 B 还没执行完,线程A实际上删除的是线程B加的锁。

(2)解决方案:

可以在 del 释放锁之前做一个判断,验证当前的锁是不是自己加的锁。在加锁的时候把当前的线程 ID 当做value,并在删除之前验证 key 对应的 value 是不是自己线程的 ID。

仍然存在的问题:
get操作、判断和释放锁是两个独立操作,不是原子性。对于非原子性的问题,我们可以使用Lua脚本来确保操作的原子性。
(Redis提供了Lua(撸啊)脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。)

具体代码如下:加锁

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
   // 获取线程标示
   String threadId = ID_PREFIX + Thread.currentThread().getId();
   // 获取锁
   Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
   return Boolean.TRUE.equals(success);
}

释放锁

public void unlock() {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁中的标示
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
    // 判断标示是否一致
    if(threadId.equals(id)) {
        // 释放锁
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

5、锁续期:(这种机制类似于redisson的看门狗机制,文章后面会详细说明)

虽然步骤4避免了线程A误删掉key的情况,但是同一时间有 A,B 两个线程在访问代码块,仍然是不完美的。怎么办呢?我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁“续期”。

1.假设线程A执行了29 秒后还没执行完,这时候守护线程会执行 expire 指令,为这把锁续期 20 秒。守护线程从第 29 秒开始执行,每 20 秒执行一次。

2.情况一:当线程A执行完任务,会显式关掉守护线程。

3.情况二:如果服务器忽然断电,由于线程 A 和守护线程在同一个进程,守护线程也会停下。这把锁到了超时的时候,没人给它续命,也就自动释放了。

6、 依旧可能存在的问题:

在主从模式或者哨兵模式下,正常情况下,如果加锁成功了,那么master节点会异步复制给对应的slave节点。但是如果在这个过程中发生master节点宕机,主备切换,slave节点从变为了 master节点,而锁还没从旧master节点同步过来,这就发生了锁丢失,会导致多个客户端可以同时持有同一把锁的问题。

Redission

Redisson分布式锁的主要原理非常简单,利用了lua脚本的原子性

看门狗:

锁快过期的时候自动续期。

若是机器宕机,看门狗线程运行不了,锁到期后自动解开。

Redis线程安全问题

超卖问题:

超卖现象本质上就是买到了比仓库中数量更多的宝贝。

超卖问题(Over-selling)是在高并发环境下,当多个用户同时购买某个商品或服务时,可能导致系统错误地售卖超过库存数量的商品。解决Redis的超卖问题通常可以使用以下几种方式:

  1. 乐观锁(Optimistic Locking)

    • 使用乐观锁机制,例如在Redis中使用版本号(version number)或者时间戳(timestamp)来实现乐观锁。在每次购买操作前,先获取当前商品的版本号或者时间戳,然后在购买时比较版本号或时间戳是否仍然相等,如果相等,则执行购买操作,否则拒绝购买。

    • 弊端:成功率太低,100个券,200个线程并发,可能最终只卖出去20份。

  1. Lua脚本

    • 使用Redis的Lua脚本(Redis的原子性操作)来实现购买操作。在Lua脚本中,可以通过EVAL命令执行一段原子性的Lua脚本,确保购买操作的原子性,避免了并发问题。

  2. 分布式锁

    • 使用分布式锁来保证购买操作的原子性。例如,可以使用Redis的SET命令来设置一个带有过期时间的锁,保证同一时刻只有一个线程可以执行购买操作。

4) 队列(Queue)

  • 使用队列来串行化购买请求。将购买请求放入一个队列中,然后使用单个线程处理队列中的请求,确保购买操作的顺序执行,避免了并发问题。

5) 库存检查

  • 在购买前,先检查库存是否足够。在Redis中,可以使用GET命令获取当前库存数量,然后比较库存和购买数量,如果库存不足,则拒绝购买。

缓存实操

缓存短信验证码:

(存在HttpSession中的话有效期为30min)

缓存菜品数据:

每次点击相应菜品分类时,都会查询数据库,频繁查询数据库会导致系统性能下降,服务端响应时间变长。

所以需要缓存一下。

Spring Data Redis

Spring Data Redis中提供了一个高度封装的类: RedisTemplate,针对jedis客户端中大量api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:

  1. ValueOperations:简单K-V操作

  2. SetOperations:set类型数据操作

3) ZSetOperations: zset类型数据操作

4) HashOperations:针对map类型的数据操作

  1. ListOperations:针对list类型的数据操作

使用redis存储推荐的用户:

当使用 RedisTemplate<String, Object> 存储 MyBatisPlus 的 Page 对象时,它在 Redis 中实际上是以字符串的形式存储的。RedisTemplate 将对象序列化为字符串后存储到 Redis 中。

在 Redis 中,虽然数据类型通常是字符串、哈希、列表、集合、有序集合等,但是对于 RedisTemplate 存储的对象,它们在 Redis 中都会以字符串的形式存储。这是因为 RedisTemplate 默认使用的序列化器将对象序列化为字节流,并将字节流转换为字符串后存储到 Redis 中。因此,无论存储的是什么类型的对象,都会以字符串的形式呈现在 Redis 中。

所以,虽然 Page 对象在 Java 中是一个复杂的对象,但当它被存储到 Redis 中时,实际上就是以字符串的形式存储的。当从 Redis 中获取这个键值对时,RedisTemplate 会自动进行反序列化操作,将字符串转换为原始的 Page 对象类型。

具体的实现思路如下:

1、改造DishController的list方法,先从Redis中获取菜品数据,如果有则直接返回 ,无需查询数据库;

如果没有则查询数据库,并将查询到的菜品数据放入Redis。

根据菜品的分类id(dish.getCategoryId())来查询redis,动态构造一个key:

//动态构造key
String key = "dishs:" + dish.getCategoryId() + "_" + dish.getStatus();//dish_1397844391040167938_1

查询结果应该为一个list,包含了多个菜品。

按照添加顺序存储商品列表,可以使用列表数据结构。每个商品可以表示为一个JSON对象,然后将它们依次添加到列表中。

LPUSH key '{"id": "1001", "price": 29.99, "kind": "电子产品"}'
LPUSH key '{"id": "1002", "price": 49.99, "kind": "服装"}'
LPUSH key '{"id": "1003", "price": 39.99, "kind": "书籍"}'

购物车的话也可以参照上面的形式,key为:cart:user_id, value为list类型

2、改造DishController的save和update方法,加入清理缓存的逻辑。(当更新update数据时,删除缓存的数据)

redis工具:

可视化工具:Redis Desktop Manager

redis操作:spring data redis:RedisTemplate

RedisTemplate常用方法(超详细)_Yan Yang的博客-CSDN博客