Redis

Redis

一、Redis的使用场景

① 缓存  ② 分布式锁  ③限流  ④购物车  ⑤Token存储  ⑥点赞关注  ⑦短信验证码存储  ⑧分布式Session  ⑨发布订阅  ⑩排行榜

1、缓存

  热点数据(经常查询,但不修改和删除)首选redis,性能高。

 2、分布式锁

注:锁,即在多线程环境下,对共享资源的访问造成线程安全问题,通过锁的机制来实现资源访问互斥;比如Java语言有线程锁:synchronize / Lock 等。

 分布式锁:在分布式环境下,所有应集群部署,锁(互斥)的范围发生了改变;

分布式锁是一种用于分布式系统中实现同步访问共享资源的机制。在分布式系统中,多个节点需要协调合作完成某项任务时,为避免资源竞争和数据不一致问题,可以使用分布式锁来确保在某一时刻只有一个节点可以访问共享资源。

 redis分布式锁实现:① Jedis   ② Lttuce   ③ RedisTemplete   ④ Redisson   

一。命令实现:setnx(获取锁) ,expire(设置锁的过期时间),del(删除锁,释放锁)

1、获取锁:setnx  key value

2、设置锁的过期时间:expire  key  30

3、执行业务代码

4、删除锁:del  key

二、Redisson实现

1、获取锁:redisson.getLock("lock");

       lock.lock();

公平锁

 联锁

 读写锁

2、执行业务代码

3、释放锁:lock.unlock();

Redisson实现的分布式锁是可重入的吗?  

注:可重入?   即可重复获取,它是指线程T获取到锁A之后,线程T再次去获取锁A是可以获取到的,Java中的synchronized、ReentrantLock都是可重入锁。

Redisson实现的分布式锁是可重入锁

redis实现分布式锁需要注意的问题?

1、不是原子操作

因为redis获取锁和设置锁的过期时间不是原子操作,在极端情况下就可以会出现问题,如果在获取锁后,redis宕机了,这个时候锁是没有过期时间的,会导致这个锁一直无法释放的情况。所以需要将获取锁和设置锁的过期时间在一个命令下执行,保证原子性。

2、没有释放锁

如果在执行业务代码后,服务突然宕机了,这个时候锁是还没有释放的,所以在获取锁的时候一定要设置过期时间,这样就算服务宕机了,我们没有手动释放锁,也会因为我们设置了锁的过期时间而自动释放锁,如果没有设置过期时间,会导致其他线程一直获取不到锁。

3、释放了锁。但业务还没有执行完

因为redis实现的分布式锁默认过期时间是30秒,但如果业务代码执行完要35秒,但是锁已经释放了,剩下的5秒内如果有线程进来获取锁并执行业务代码,这时就会有两个线程来执行业务代码,这样就会导致数据不一致,比如在减库存的时候会出现超卖的情况。所以锁的过期时间要让续期,redis已经提供了,是锁的过期时间的1/3。

4、释放了别人的锁

当前有两个线程,线程A和线程B,如果设置锁的过期时间是30s,但执行业务代码要35s,并且没有设置锁的过期时间的续期,此时线程A在执行,但锁已经释放了,然后线程B进来执行,因为线程A的锁已经过期了,此时线程A释放的锁是线程B的,这样就产生了释放了别人的锁的情况。在实现分布式锁时,要求自己的锁要自己释放,不允许释放别人的锁。线程可以在获取锁的时候,生成一个唯一的id,然后把这个id设置到key的值里面去,当要释放锁的时候,去判断当前锁是不是当前线程设置的。

5、大量请求竞争锁失败

① 重试,让锁自旋,重试3~5次,每一次获取失败间隔50ms,如果超过5次,就返回获取锁失败。

② 让业务执行尽可能短

③ 限流处理

6、多节点Redis主从复制的问题

 假设当前有两个线程,线程A和线程B,并且当前redis是哨兵模式的主从复制,一主二从,当线程A去主服务器去获取key,并且将key同步到两个从节点,但是还没有复制,这个主服务器就宕机了,然后从服务器升级成主服务器,这个时候线程B就开始去这个新的主服务器去获取key,这样两个线程都获取到了锁,都会区执行业务代码,这样数据会有安全问题。可以使用redis提供的红锁来解决。

解决:红锁会向所有的redis节点加锁,但这种方法性能很低,也很复杂,如果要在redis集群环境下使用分布式锁,建议使用zookeeper来实现分布式锁。redis是保证AP,zookeeper是保证CP。

7、锁的性能问题

分段锁。

8、锁的可重入性

Redisson实现的分布式锁就具有可重入性。

3、Token存储

在现在的前后端分离的开发模式下,redis存储token很常见。

用户在前端登录成功后会生成token,并将token写入redis,并且前端也会将token进行保存,后续用户需要进行访问其他接口时,会携带token请求,在后端中经过过滤器,拦截器等校验,最后会判断当前token是否和登录成功时存入redis的token进行比较,一致则将结果返回到前端。

4、短信验证码存储

用户在前端点击发送验证码,在后台生成验证码之后,将验证码存入redis,并且将验证码发送给用户,用户输入验证码后发起请求,将用户输入的验证码和在redis里的验证码进行比对,一致则成功。

5、计数器

 6、全局唯一id

 

 7、排行榜

使用redis的Zset数据结构

放入一些数据

 

 将这些数据按score从高到低排序

 增加分数

 8、限流

redis可以配合lua脚本来对数据进行限流

 9、购物车

可以使用redis的hash结构来存储用户的购物车数据

 10、点赞关注

点赞可以使用redis的hash结构,关注可以使用redis的set结构。

 

二、缓存穿透,缓存击穿,缓存雪崩

1、缓存穿透

缓存穿透是由于请求一个不存在的数据而导致的;如果数据库或者redis被一直这样恶意请求一个不存在的数据,会造成数据库的访问压力过大,导致数据库响应过慢,严重甚至会导致数据库宕机。

 解决:

① 缓存空值。在缓存中,之所以会发生穿透,就是因为没有将那些不存在的值的key缓存下来,从而导致每次查询请求都要请求到数据库。所以就可以为这样key对应的值设置为null并放到缓存当中,这样再出现查询这个key的时候,直接返回null即可

② 布隆过滤器(BloomFilter)。布隆过滤器用于检索一个元素是否在一个集合中。它是采用一个很长的二进制数组,通过一系列的hash函数来确定该数据是否存在。

布隆过滤器是一种比较巧妙的概率性数据结构,它可以告诉你数据一定不存在或者可能存在,相比Map,Set,List等传统的数据结构它占用内存少、结构更高效。

布隆过滤器添加元素时,会使用多个hash函数对元素进行哈希,然后用数组的长度取余数,算出一个索引位置值,再把数组的位置值设置为1,这样就完成了元素添加

向布隆过滤器查询元素时,和添加元素一样,也会对元素进行多次hash计算出数组的位置,然后查看数组的位置值是否都为1,只要有一个位置值为0,就表示布隆过滤器不存在该元素。如果这几个位置都为1,则表示元素可能存在(并不是一定存在)

③布隆过滤器为什么存在误判?

 具体实现:Guava,Hutool,Redisson,手动实现

redisson

 2、缓存击穿

缓存击穿是指当某一个key的缓存过期时,大并发量的请求同时访问此key,瞬间击穿缓存服务器直接访问数据库,让数据库处于负载的情况。

解决:

① 异步定时更新

比如某一个热点数据的过期时间是1小时,那么每59分钟,通过定时任务去更新这个热点key,并重新设置其过期时间

② 互斥锁

当redis中根据key获得的value值为空时,先锁上,然后从数据库加载,加载完毕,释放锁。若其他线程也在请求该key时,发现获取锁失败,则先阻塞。

3、缓存雪崩

缓存雪崩是指当大量缓存同时过期或者缓存服务宕机,所有的请求都直接访问数据库,造成数据库高负载,影响性能,甚至数据库宕机。

解决:

① 不同的过期时间

为了避免大量的缓存在同一时间过期,可以把不同的key过期时间设置成不同的,并且通过定时刷新的方式更新过期时间。

② 集群

在缓存雪崩问防治上,一个比较典型的技术就是采用集群方式部署,使用集群可以避免服务单点故障

 

三、Redis是AP还是CP的?

AP

Redis的设计目标是高性能、高可扩展性和高可用性,Redis的一致性模型是最终一致性,即在某个时间点读取的数据可能并不是最新的,但最终会达到一致的状态。

Redis没办法保证强一致性的主要原因是,因为它的分布式设计采用的是异步复制,这导致在节点之间存在数据同步延迟和不一致的可能性。

也就是说,当某个节点上的数据发生改变时,redis会将这个修改操作发送给其他节点进行同步,但是由于网络传输的延迟等原因,这些操作不一定会立即被其他节点收到和执行,这就可能导致节点之间存在数据不一致的情况。

除此之外,redis的一致性还受到节点故障的影响。当一个节点宕机时,这个节点上的数据可能无法同步到其他节点,这就可能导致数据在节点之间的不一致。虽然redis通过主从复制和哨兵等机制可以提高系统的可用性和容错性,但是这些机制并不能完全解决数据一致性问题。

四、Redis的集群模式?

redis有三种主要的集群模式,用于分布式环境中实现高可用和数据复制。

1、主从复制

主从复制是Redis最简单的集群模式。这个模式主要是为了解决单点故障的问题,所以将数据复制到多个副本中,这样即便有一台服务器出现故障,其他服务器依然可以继续提供服务。

主从模式中,包括一个主节点和一个或者多个从节点。主节点负责处理所有的写操作和读操作,而从节点则负责主节点的数据,并且只能处理读操作。当主节点发生故障时,可以将一个从节点升级为主节点,实现故障转移。

 优点:简单易用,适用于读多写少的场景。它提供了数据备份的功能,并且可以有很好的扩展性,只要增加更多的节点,就能让整个集群的读的能力不断提升。

缺点:不具备故障自动转移的能力,没有办法做容错和恢复。主节点和从节点的宕机都会导致客户端部分读写请求失败,需要人工介入让节点恢复或者手动切换一台从节点变成主节点服务器才可以。并且在主节点宕机时,如果数据没有及时复制到从节点,也会导致数据不一致。

 2、哨兵模式

哨兵模式是为了解决主从模式无法自动容错及恢复的问题。

哨兵模式在主从复制的基础上加入了哨兵节点。哨兵节点是一种特殊的redis节点,用于监控主节点和从节点的状态。当主节点发生故障时,哨兵节点可以自动进行故障转移,选择一个合适的从节点升级为主节点,并通知其他从节点和应用程序进行更新。每个redis实例都可以作为哨兵节点,通常需要部署多个哨兵节点,以确保故障转移的可靠性。

 哨兵节点会定期向所有主节点和从节点发送PING命令,如果在指定的时间内未收到PONG响应,哨兵节点会将该节点标记为主观下线。如果一个主节点被多数哨兵节点标记为主观下线,那么它将被标记为客观下线

当主节点被标记客观下线时,哨兵节点会触发故障转移过程。他会从所有的健康的从节点中选举一个新的主节点,并将所有从节点切换到新的主节点,实现自动故障转移。同时,哨兵节点会更新所有客户端的配置,指向新的主节点。

哨兵节点通过发布订阅功能来通知客户端有关主节点状态变化的消息。客户端收到消息后,会更新配置,将新的主节点信息应用于连接池,从而使客户端可以继续与新的主节点进行交互。

优点:为整个集群系统提供了一种故障转移和恢复的能力。

3、Cluster模式

Redis  Cluster是Redis中推荐的分布式集群解决方案。它将数据自动分片到多个节点上,每个节点负责一部分数据。

 Redis  Cluster 采用主从复制来提高可用性。每个分片都有一个主节点和多个从节点。主节点负责写操作,而从节点复制主节点的数据并处理读请求。

Redis  Cluster能够自动检测节点的故障。当一个节点失去连接或者不可达时,Redis  Cluster会尝试将该节点标记为不可用,并从可用的从节点中提升一个新的主节点。

优点:

① 适用于大规模应用的解决方案,它提供了更好的横向扩展和容错能力。它自动管理数据分片和故障转移,减少了运维的负担。

② 数据分片存储在不同的节点上,每个节点都可以单独对外提供读写服务。不存在单点故障的问题。

 

五、Redis的数据分片

Redis的数据分片是一种将一个reids数据集分割成多个部分,分别存储在不同的redis节点上的技术。它可用用于将一个单独的redis数据库扩展到多个物理机器上,从而提高redis集群的性能和可扩展性。

实现方式(Cluster):

在Redis的Cluster集群模式中,使用哈希槽(hash  slot)的方式来进行数据分片,将整个数据集划分为多个槽,每个槽分配给一个节点。客户端访问数据时,先计算出数据对应的槽,然后直接连接到该槽所在的节点进行操作。

 Redis  Cluster中的数据分片具有以下特点:

提升性能和吞吐量

通过在多个节点上分散数据,可以并行处理更多的操作,从而提升整体的性能和吞吐量。这在高流量场景下尤其重要,因为单个节点可能无法处理所有请求。

提高可扩展性

分片使得redis可以水平扩展。可以通过添加更多节点扩展数据库的容量和处理能力。

更好的资源利用

分片允许更有效地利用服务器资源。每个节点只处理数据地一部分,这降低了单个节点地内存和计算需求。

避免单点故障

在没有分片的情况下,如果唯一的redis服务器发生故障,整个服务可能会停止。在分片的环境中,即使一个节点出现问题,其他节点仍然可以继续运行。

数据冗余和高可用性

在某些分片策略中,如redis集群,每个分片的数据都可以在集群内的其他节点上进行复制。这意味着即使一个节点失败,数据也不会丢失,从而提高了系统的可用性。

 

六、Redis使用什么协议进行通信?

Redis使用的是一种文本协议进行客户端与服务端之间的通信------RESP,这种协议简单、高效、易于解析,被广泛使用。

RESP协议基于TCP协议,采用请求/响应模式,每条请求由多个参数组成,以命令名称作为第一个参数。请求和响应都以行结束符(\r\n)作为分隔符。

除了基本的GET、SET操作,Redis还支持事务、lua脚本、管道等高级功能,这些都是通过Redis协议来实现的。

 

七、Redis为什么这么快?

① 基于内存

Redis是基于内存的数据库,数据存储在内存中,数据的读写速度非常快,因为内存访问速度比硬盘访问速度快得多

② 单线程模型

Redis使用单线程模型,这意味着它的所有操作都是在一个线程内完成的,不需要进行线程切换和上下文切换。着大大提高了Redis的运行效率和响应速度。

③ 多路复用 I/O 模型

Redis在单线程的基础上,采用了 I/O 多路复用技术,实现了单个线程同时处理多个客户端连接的能力,从而提高了Redis的并发性能。

④ 高效的数据结构

Redis提供了多种高效的数据结构,如哈希表、有序表、列表等,这些数据结构都被实现的非常高效,能够在O(1) 的时间复杂度内完成数据读写操作,这也是Redis能够快速处理数据请求的重要因素之一。
⑤ 多线程的引入

在Redis 6.0中,为了进一步提升IO的性能,引入了多线程的机制。采用多线程,使得网络处理的请求并发进行,就可以大大的提升性能。多线程除了可用减少由于网络IO等待造成的影响,还可以充分利用CPU的多核优势。

 

八、Redis支持哪几种数据结构?

① 字符串 (String)

② 哈希(Hash)

③ 列表(List)

④ 集合(Set)

⑤ 有序集合(ZSet)

另外,redis还支持一些高级的数据结构,如:Streams、Bitmap、Geospatial以及HyperLogLog

 

九、Redis中的ZSet是怎么实现的?

ZSet是Redis中的一种特殊的数据结构,它内部维护了一个有序的字典,这个字典的元素中既包括了一个成员(member),也包括了一个double类型的分值(score)。这个结构可用帮助用户实现记分类型的排行榜数据,比如游戏分数排行榜,网站流行度排行榜等。

Redis中的ZSet在实现中,有多种数据结构,大类的话有两种,分别是ziplist(压缩列表)和skiplist(跳跃表),但这只是以前,在redis5.0中新增了一个listpack(紧凑列表)的数据结构,这种数据结构就是为了替代ziplist的,而之后redis 7.0 的发布中,在Zset的实现中,已经彻底不用ziplist了。

当ZSet的元素比较少时,Redis会采用ZipList(ListPack)来存储ZSet的数据。ZipList(ListPack)是一种紧凑的列表结构,它通过连续存储元素来节约内存空间。当ZSet的元素数量增多时,Redis会自动将ZipList(ListPack)转换为SkipList,以保持元素的有序性和支持范围查询操作。

在这个过程中,Redis会遍历ZipList(ListPack)中的所有元素,按照元素的分数值依次将它们插入到SkipList,这样就可以保证元素的有序性。

 

十、Redis为什么被设计成是单线程的?

redis的单线程其实指的是"其网络IO和键值对读写是由一个线程完成的",也就是说,Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块,集群支撑模块等是多线程的。

其实redis在 4.0的时候就针对部分命令做了多线程化。

一个计算机程序在执行的过程中,主要需要进行两种操作分别是读写操作和计算操作。

其中读写操作主要涉及到的是I/O操作,其中包括网络I/O和磁盘I/O。计算操作主要涉及到CPU。

而多线程的目的,就是通过并发的方式来提升I/O的利用率和CPU的使用率。

之所以Redis没有使用多线程处理IO操作,主要是因为,Redis的操作基本都是基于内存的,CPU资源根本就不是Redis的性能瓶颈。

 

十一、Redis中的setnx命令为什么是原子性的?

Redis中的setnx命令是一个原子性操作,因为它利用了Redis单线程的特点。

在Redis中,所有的命令都是在主线程中顺序执行的,这意味着每个命令在执行时不会被其他命令打断。当执行setnx命令时,Redis会在内存中检查给定的key是否存在,如果不存在,则设置key的值,并返回1。如果该key已经存在,则不做任何操作,直接返回0。

由于Redis是单线程的,所以当一个客户端执行setnx命令时,其他客户端无法执行任何命令,直到该命令执行完毕。因此,setnx是一个原子性操作,它可以保证任何时候只有一个客户端可用执行该命令,这可以防止并发访问造成的数据竞争和不一致性问题。

 

十二、Redis的持久化机制是怎么样的?

RDB

RDB是将Redis的内存中的数据定期的保持到磁盘上,以防止数据在Redis进程异常退出或者服务器断电等情况下丢失。

优点:快照文件小,恢复速度块,适合做备份和灾难恢复。

缺点:定期更新可能会丢失数据

 

AOF

AOF是将Redis的所有写操作追加到AOF文件的末尾,从而记录了Redis服务器运行期间所有修改操作的详细记录。当Redis重新启动时,可以通过执行AOF文件中保持的写操作来恢复数据。

优点:可以实现更高的数据可靠性、支持更细粒度的数据恢复,适合做数据存档和数据备份。

缺点:文件大占用空间更多,每次写操作都需要写磁盘导致负载较高。

 

RDB和AOF比较

 混合持久化

在开启混合持久化的情况下,AOF重写时会把Redis的持久化数据,以RDB的格式写入到AOF的开头,之后的数据再以AOF格式化追加到文件的末尾。

aof-use-rdb-preamble 是开启混合模式的参数。

混合持久化结合了RDB和AOF持久化的优点,开头为RDB的格式,使得Redis可以更快的启动,同时结合AOF的优点,又减低了大量数据丢失的风险。

 

十二、Redis的事务机制?

Redis是支持事务的,它的事务主要的目的是保证多个命令执行的原子性,即要在一个原子操作中执行,不会被打断。

需要注意的是,Redis的事务时不支持回滚的。从Redis 2.6.5 开始,服务器将会在累计命令的过程中检测到错误。然后,在执行EXEC 期间会拒绝执行事务,并返回一个错误,同时丢弃该事务。如果事务执行过程中发生错误,Redis会继续执行剩余的命令而不是回滚事务。

 

十三、Redis的过期策略是怎么样的?

Redis通过过期时间来控制键值对的生命周期。过期时间可以通过EXPIRE、EXPIREAT、PERSIST等命令设置,也可以在插入数据时直接设置过期时间。

定期删除:

Redis默认每隔100ms 就随机抽取一些设置了过期时间的key,并检查是否过期,如果过期就删除。定期删除是Redis的主动删除策略,它可以确保过期的key能够被及时删除,但是会占用CPU 资源去扫描key,可能会影响Redis的性能。

惰性删除:

当一个key过期时,不会理解从内存中删除,而是在访问这个key的时候才会触发删除操作。惰性删除时Redis的被动删除策略,它可以节省CPU资源,但是会导致过期的key始终都保存在内存中,占用内存空间。

Redis默认同时开启定期删除和惰性删除两种过期策略。

定期删除会在Redis设置的过期键值对的过期时间达到一定阈值时进行一次扫描,将过期的key删除,但不会立即释放内存,而是把这些key标记为 “已过期”,并放入一个专门的链表中。然后,在Redis的内存使用率达到一定的阈值时,Redis会对这些 “已过期” 的key进行一次内存回收操作,释放被这些key占用的内存空间。

而惰性删除则是在key被访问时进行过期检查,如果过期了则删除key并释放内存。

 

十四、Redis的内存淘汰策略是怎么样的?

Redis的内存淘汰策略用于在内存满了之后,决定哪些key要被删除。Redis支持多种内存淘汰策略,可以通过配置文件中的 maxmemory-policy参数来指定。

以下是Redis支持的内存淘汰策略:

① noeviction:不会淘汰任何键值对,而是直接返回错误信息。

② allkeys-lru:从所有的key中选择最近最少使用的那个key并删除。

③ volatile-lru:从设置了过期时间的key中选择最近最少使用的那个key并删除。

④ allkeys-random:从所有的key中随机选择一个key并删除。

⑤ volatile-random:从设置了过期时间的key中随机选择一个key并删除。

⑥ volatile-ttl:从设置了过期时间的key中选择剩余时间最短的key并删除。

⑦ volatile-lfu:淘汰的对象是带有过期时间的键值对中,访问频率最低的那个。

⑧ allkeys-lfu:淘汰的对象则是所有的键值对中,访问频率最低的那个。

 

十五、什么情况下会出现数据库和缓存不一致的问题?

在非并发的场景中,出现不一致的问题都比较容易理解,因为缓存的操作和数据库的操作是存在一定时间差的。而且这两个操作是没办法保证原子性的,也就是说,是有可能一个操作成功,一个操作失败。所有就必然会存在数据不一致的问题。

如果在并发场景中,如果两个线程,同时进行先写数据库,后更新缓存的操作,就可能出现不一致的

 如果在并发场景中,如果两个线程,同时进行先更新缓存,后写数据库的操作,同理,也可能出现不一致的

 在并发场景中,还有一种容易忽略的并发场景,那就是读写并发。

 加入一个读线程,在读缓存的时候没有查到值,它就会去数据库中查询,但是如果自查询到结构之后,更新缓存之前,数据库就被更新了,但是这个读线程是完全不知道的,那么就会导致最终缓存会被重新用一个 "旧值" 覆盖掉。这就导致了缓存和数据库不一致的现象。

 

十六、如何解决Redis和数据库的一致性问题?

为了保证Redis和数据库的数据一致性,肯定是要缓存和数据库双写了。

问题:是先操作缓存还是数据库?  是删除缓存还是更新缓存?

我的建议是优先考虑删除缓存而不是更新缓存,因为删除缓存更加简单,而且带来的一致性问题特更少一些。

在具体操作过程中,考虑延迟双删的策略,即:

1、删除缓存

2、更新数据库

3、再次删除缓存

也就是先删除缓存,再更新数据库,然后过个几秒再删一把缓存,避免因为并发出现脏数据。

 

先写数据还是先删缓存?

① 先写数据库

因为数据库和缓存的操作是两步的,没办法做到保证原子性,所以就有可能第一步成功而第二步失败。

而一般情况下,如果把缓存的删除动作放到第二步,有一个好处,那就是 缓存删除失败的概率还是比较低的,除非是网络问题或者缓存服务器宕机的问题,否则大部分情况都是可以成功的。

而且,先写数据库,后删除缓存,如果第二步失败了,会导致数据库中的数据已经更新,但是缓存还是旧数据,导致数据不一致

② 先删缓存

如果选择先删除缓存后写数据库这种方案,那么第二步失败是可以接收的,因为这样不会有脏数据,也没有什么影响,只需要重试就好了。

但是,先删除缓存后写数据库的这种方式,会无形中放大 “读写并发” 导致数据不一致问题。我们知道,当使用了缓存之后,一个读的线程在查数据的过程是这样的:

1、查询缓存,如果缓存中有值,则直接返回

2、查询数据库

3、把数据库的查询结构更新到缓存

所以,对于一个读线程来说,虽然不会写数据库,但是是会更新缓存的,所以,在一些特殊的并发场景中,就会导致数据不一致的情况。

假如一个读线程,在读缓存的时候没有查到值,它就会去数据库中查询,但是如果查询到结果之后,更新缓存之前,数据库被更新了,但是这个读线程是完全不知道的,那么就会导致最终缓存会被重新用一个 “旧值” 覆盖掉。

这也就导致了缓存和数据库的不一致现象。

解决:延迟双删

 

如何选择?

如果业务量不大,并发不高的情况,可以选择先更新数据库,后删除缓存的方式,因为这种方案更加简单。

如果业务量比较大,并发量很高的话,那么建议选择先删除缓存,因为这种方式在引入延迟双删,分布式锁等机制,会使得整个方案会更加趋势近于完美,带来的并发问题更少。当然,也会更复杂。

 

十七、Redis如何实现延迟消息?

Redis过期消息实现延迟消息 (不建议使用)

在redis.conf 中,加入一条配置notify-keyspace-events Ex 开启过期监听,然后再代码中实现一个 KeyExpirationEventMessageListener,就可以监听key 的过期消息了。

这样就可以在接收到消息的时候,进行订单的关单操作。

 

Redis的zset实现延迟消息

zset是一个有序集合,每一个元素(member)都关联了一个score,可以通过score 排序来取集合中的值。

我们将订单超时时间的时间戳(下单时间 + 超时时间)与订单号分别设置为 score 和 member 。这样redis会对zset 按照 score 延迟时间进行排序。然后我们再开启redis 扫描任务,获取 “ 当前时间 > score ” 的延时任务,扫描到之后取出订单号,然后查询到订单进行关单操作即可。 

使用redis  zset 来实现订单关闭的功能的优点是可以借助redis的持久化、高可用机制。避免数据丢失。但是这个方案也有缺点,那就是再高并发场景中,有可能有多个消费者同时获取到同一个订单号,一般采用分布式锁解决,但是这样做会降低吞吐性。

 

Redisson实现延迟消息

Redisson中定义了分布式延迟队列 RDelayedQueue,这是一种基于ZSet结构实现的延迟队列,它允许以知道的延迟时长将元素放到目标队列中。

其实就是在zset的基础上增加了一个基于内存的延迟队列。当我们要添加一个数据到延迟队列的时候,redission会把数据 + 超时时间放到zset中,并且起一个延时任务,当任务到期的时候,再去zset中把数据取出来,返回给客户端使用。

 

十八、什么是RedLock,它解决了什么问题?

RedLock是Redis的作者提出的一个多节点分布式锁算法,旨在解决使用单节点Redis分布式锁可能存在的单点故障问题。

Redis的单点故障问题:

① 在使用单节点Redis实现分布式锁时,如果这个Redis实例挂掉,那么所有使用这个实例的客户端都会出现无法获取锁的情况。

② 当使用集群部署的时候,如果master 一个客户端在master节点加锁成功了,然后没来得及同步数据到其他节点上,它就挂了,那么这个时候如果选出一个新的节点,再有客户端来加锁的时候,就也能加锁成功,因为数据没来得及同步,新的master会认为这个key是不存在的。

 

RedLock通过使用多个Redis节点,来提供一个更加健壮的分布式锁解决方案,能够在某些Redis节点故障的情况ia,仍然能够保证分布式锁的可用性。

RedLock是通过引入多个redis节点来解决单点故障的问题。

 

十九、Redisson的watch dog 机制是怎么样的?

为了避免Redis实现的分布式锁超时,Redisson引入了一个watch dog的机制,它可以帮助我们在Redisson实例被关闭之前,不断地延长锁地有效期。

① 自动续租:当一个Redisson客户端实例获取到一个分布式锁时,如果没有指定锁的超时时间,Watch dog 会基于Netty的时间轮启动一个后台任务,定期向Redis发送命令,重新设置锁的过期时间,通常是锁的租期时间的1/3.这确保了即使客户端处理时间较长,所持有的锁也不会过期。

② 续期时长:默认情况下,每10s 钟做一次续期,续期时长是30s。

③ 当锁被释放或者客户端实例被关闭时,watch dog会自动停止对应锁的续租任务。

 

二十、介绍下Redis集群的脑裂问题?

脑裂问题一般来说就是指一个分布式系统中有两个子集,然后每个子集都有一个自己的大脑(Leader / Master)。那么整个分布式系统中就会存在多个大脑了,而且每个大脑都认为自己是正常的,从而导致数据不一致或重复写入的问题。

脑裂的发生

Redis的脑裂问题可能发生在网络分区或者主节点出现的问题的时候:

① 网络分区:网络故障或分区导致了不同子集之间的通信中断。

Master 节点,哨兵和Slave 节点被分割成了两个网络,Master处在一个网络中,Slave 库和哨兵在另外一个网络中,此时哨兵发现和Master连不上了,就会发起主从切换,选一个新的Master,这时候就会出现两个主节点的情况。

② 主节点问题:集群中的主节点之间出现问题,导致不同的子集认为它们是正常的主节点。

Master节点有问题,哨兵就会开始选举新的主节点,但是这个过程中,原来的那个Master节点又恢复了,这时候就可能会导致一部分Slave 节点认为它是Master节点,而另一部分Slave新选出了一个Master。

脑裂的危害

① 数据不一致:不同子集之间可能对同一数据进行不同的写入,导致数据不一致

② 重复写入:在脑裂解决后,不同子集可能尝试将相同的写入操作应用到主节点上,导致数据重复。

③ 数据丢失:新选出来的Master会向所有的实例发送slave  of 命令,让所有实例程序进行全量同步,而全量同步首先就会将实例上的数据先清空,所以在主从同步期间在原来那个Master 上执行的命令将会被清空。

如何避免脑裂

Redis已经提供了两个配置项可以帮我们做这个事儿:

① min-slaves-to-write : 主库能进行数据同步的最少从库数量

② min-slaves-max-lag : 主从库之间进行数据复制时,从库给主库发送ACK 消息的最大延迟秒数。

尽管Redis脑裂可以采用 min-slaves-to-write 和 min-slaves-max-lag 合理配置尽量规避,但无法彻底解决

 

二十一、Redis中key过期了一定会立即删除吗

Redis中key的过期方式有两种:被动过期和主动过期

被动过期是指当某个客户端尝试访问一个key,发现该key已经超时,那么它会被从Redis中删除。

当然,仅仅靠被动过期还不够,因为有些过期的key可能永远不会再被访问,这些key应该被及时删除,因此Redis会定期随机检查一些带有过期时间的key,所有邮件过期的key都会从key空间中删除。

具体来说Redis每秒会执行以下操作10次:

① 从带有过期时间的key集合中随机选择20个key。

② 删除所有已经过期的key

③ 如果已经过期的key占比超过25%,则重新从步骤①开始。

直到过期key的比例下降到 25% 或者这次任务的执行耗时超过了25 毫秒,才会退出循环。

所以,Redis其实并不保证key在过期的时候就能被立即删除的,因为一方面惰性删除中需要下次访问才会删除,即便是主动删除,也是通过轮询的方式来实现的。如果要过期的key很多的话,就会带来延迟的情况。

 

二十二、Redis的事务和Lua脚本之间有哪些区别?

Redis中,事务和lua都是保证原子性的手段,当有多个命令要执行,希望他们以原子性方式执行的时候,就会考虑使用事务或者lua脚本。

1、原子性保证

事务和lua都是可以保证原子性操作的,但是,这里说的原子性指的是不可拆分,不可中断的原子性操作。所以,需要注意的是,不管是redis的事务还是Lua,都没办法回滚,一旦执行过程中有命令失败了,都是不支持回滚的。

但是,Redis的事务在执行过程中,如果有某一个命令失败了,是不影响后续命令执行的,而lua脚本中,如果执行过程中某个命令失败了,是会影响后续命令执行的

2、交互次数

在Redis的事务执行时,每一条命令都需要和Redis服务器去进行一次交互,我们可以在Redis事务过程中,MULTI 和 EXEC 之间发送多个Redis命令到Redis服务器,这些命令会被服务器缓存起来,但并不会立即执行。但是每一条命令的提交都需要进行一次网络交互

而lua脚本则不需要,只需要一次性的把整个脚本提交给Redis即可。网络交互比事务要少。

3、前后依赖

在Redis的事务中,事务内的命令都是立即执行的,并且在没有执行EXEC 命令之前,命令是没有被真正执行的,所以后续命令是不会也不能依赖与前一个命令的结果的。

而在lua脚本中是可以依赖前一个命令的结果的,lua脚本中的多个命令是依次执行的,我们可以利用前一个命令的结果进行后续的处理。

4、流程编排

借助lua脚本,我们可以实现非常丰富的各种分支流程控制,以及各种运算相关操作。而Redis的事务是不支持这些操作的。

 

二十三、Redisson的lock和tryLock有什么区别

当使用Redisson实现分布式锁的时候,经常用到 lock 和tryLock两个方法,它们有什么区别么?

tryLock是尝试获取锁,如果能获取到锁直接返回ture,如果无法获取到锁,它会按照我们指定的waitTime 进行阻塞,在这个时间段内它还会尝试获取到锁。如果超过这个时间段还没有获取到则返回false。如果我们没有指定 waitTime ,那么它在未获取到锁的时候,就直接返回false了。

lock的原理是以阻塞的方式去获取锁,如果获取锁失败就会一直等待,直到获取成功。

所以,可以认为,lock实现的是一个阻塞锁,而tryLock实现的是一个非阻塞锁(在没有指定waitTime的情况下)

 

二十四、为什么Redis不支持回滚?

我们都知道,Redis是不支持回滚的,即使是Redis的事务和Lua脚本,在执行过程中,如果出现了错误,也是无法回滚的,可是为什么呢?

不支持回滚的主要原因是支持回滚将对Redis的简洁性和性能产生重大影响。

① 使用场景:Redis 通常用作缓存,而不是作为需要复杂事务处理的关系型数据库。因此,它的目标用户通常不需要太复杂的事务支持。如果需要的话,直接用数据库就行了。

② 性能优先:Redis是一个高性能的 K-V存储系统,它优化了速度和效率。引入回滚机制会增加复杂度和开销,从而影响性能。

③ 简化设计:Redis 的设计哲学倾向于简单和高效。回滚机制会使得系统变得复杂,增加了错误处理和状态关联的难度。

④ 数据类型和操作:Redis支持的数据类型和操作不需要太复杂的事务支持。大多数命令都是原子性的。

⑤ 单线程模型:Redis事务是提交后一次性在单线程中执行的,而关系型数据库如MySQL是交互式的多线程模型执行的,所以MySQL需要事务的回滚来确保并发更新结果不出现异常。而Redis不太需要。

⑥ 出错情况:在Redis中,命令失败的原因比较有限:语法错误、操作的数据的类型不一致、Redis资源不足等,而这几种问题,是应该在编码阶段就发现的,而不应该在Redis执行过程中出现。

 

总结:Redis的设计就是简单、高效等,所以引入事务的回滚机制会让系统更加的复杂,并且影响性能。从使用场景上来说,Redis一般都是被用做缓存的,不太需要很复杂的事务支持,当人们需要复杂的事务时会考虑持久化的关系型数据库。相比于关系型数据库,Redis是通过单线程执行的,在执行过程中,出现错误的概率比较低,并且这些问题一般在编译阶段都应该被发现,所以就不太需要引入回滚机制。

 

二十五、如何用Redis实现乐观锁?

所谓乐观锁,其实就是基于CAS的机制,CAS本质是 Compare And Swap,就是需要知道一个key在修改前的值,去进行比较。

在Redis中,想要实现这个功能,可以依赖 WATCH 命令。这个命令一旦运行,它会确保只有在 WATCH 监视的key在调用 EXEC 之前没有改变,后续的事务才会执行。

1、WATCH:使用 WATCH 命令监视一个或多个key。这个命令会监视给定key,直到事务开始。

2、GET:在事务开始之前,查询你需要的数据。

3、MULTI:使用 MULTI 命令开始事务。

4、SET:在事务中添加所有需要执行的命令。

5、EXEC:使用 EXEC 命令执行事务。如果自从事务就是以来监视的key被修改过,EXEC 将返回 nil ,这表示事务中的命令没有被执行。

通过这种方式,Redis保证了只有在监视数据自事务开始以来没有改变的情况下,事务才会执行,从而实现了乐观锁定。

 

二十六、Redis如何高效安全的遍历所有key

在Redis中遍历所有的key,可以使用KEYS命令和 SCAN 命令

KEYS:KEYS命令用于查找所有符合给定模式的key,例如 KEYS * 会返回所有key。它在小数据库中使用时非常快,但在包含大量key的数据库中使用可能会阻塞服务器,因为它以此性检查并返回所有匹配的key。

使用Jedis的实现方式:

 SCAN:SCAN 命令提供了一种更安全的遍历key的方式,它以游标为基础分批次迭代key集合,每次调用返回一部分匹配的key。SCAN 命令不会一次性加载所有匹配的key,因此不会像 KEYS命令那样阻塞服务器,更适合用于生产环境中遍历key集合。

使用Jedis的实现方式:

 当遍历结束时,cursor 的值会变为0,我们可以通过判断cursor的值来终止迭代。

 

posted on 2024-03-02 17:55  respectxx  阅读(32)  评论(0编辑  收藏  举报