Redis面试题
Redis到底是多线程还是单线程
Redis 6.0版本之前的单线程指的是其网络I/O和键值对的读写是由一个线程完成的。
多线程在Redis 6.0中的引入是为了改善一些特定场景下的性能问题,特别是在大型多核系统上。Redis 6.0引入了多个I/O线程,这些线程负责处理网络事件的监听和接收。主线程仍然是单线程的,负责命令的执行和响应的返回
举个例子,假设有多个客户端同时向Redis发送请求,这些请求在网络上到达Redis服务器。主线程会监听这些网络事件,并将请求分发给空闲的I/O线程进行处理。每个I/O线程负责接收请求、解析命令,并将命令发送给主线程执行。主线程执行完命令后,将响应发送给对应的I/O线程,然后I/O线程将响应返回给客户端。而键值对读写命令仍然是单线程处理,所以Redis依然是并发安全的。
只有网络请求模块和数据操作模块式是单线程的,而其他的持久化、集群数据同步等,其实是由额外的线程执行的。
服务端的读写命令是单线程的。
Redis单线程为什么还能那么快
- 命令执行基于内存操作,一条命令在内存里操作的时间是几十纳秒
- 命令执行时单线程操作,没有线程切换的开销
- 基于IO多路复用机制提升Redis的IO利用率
- 高效的数据存储结构:全局hash表以及多种高效数据结构,比如跳表,压缩列表,链表等等。
Redis底层数据如何用跳表存储的
有序集合放到链表中,跳表就是根据链表查找较慢这点而去做优化
在Redis中,跳表(Skip List)是用于实现有序集合(Sorted Set)的数据结构。跳表通过引入多层索引来加速有序集合的查找操作。
跳表的底层原理是由多层链表组成,其中最底层是原始链表,包含所有的元素节点。上层的索引节点通过指针连接了下层的节点,形成了一种跳跃的结构。
跳表的层数是根据元素数量和概率计算得出的,通常由一个随机算法决定。每一层的索引节点数量是根据元素数量和层数计算得出的。
O(logN)
把有序链表改造为支持近似“折半查找”算法,可以进行更快速的插入、输出、查找操作
Redis删除过期key的策略
Redis对于过期key的删除策略主要有三种:
- 定时删除:在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。这种方式可以保证内存被尽快释放,但如果过期key很多,删除这些key会占用很多的CPU时间¹。
- 惰性删除:key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null。这种方式对CPU时间的占用是比较少的,但如果大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露。
- 定期删除:每隔一段时间执行一次删除过期key操作。通过限制删除操作的时长和频率,来减少删除操作对CPU时间的占用¹。
定时删除和定期删除为主动删除:Redis会定期主动淘汰一批已过去的key。惰性删除为被动删除:用到的时候才会去检验key是不是已过期,过期就删除¹。
惰性删除为redis服务器内置策略。定期删除可以通过配置redis.conf 的hz选项,默认为10 (即1秒执行10次,100ms一次),以及配置redis.conf的maxmemory最大值,当已用内存超过maxmemory限定时,就会触发主动清理策略¹。
Redis Key过期了为什么内存没释放
你在使用Redis时,肯定经常使用SET
命令
SET
除了可以设置key-value 之外,还可以设置key的过期时间,就像下面这样:
127.0.0.1:6379> SET tuling zhuge EX 120
oK
127.0.0.1:6379> TTL tuling4 (integer)117
此时如果你想修改key的值,但只是单纯地使用SET命令,而没有加上过期时间的参数,那这个key的过期时间将会被擦除
127.0.0.1:6379> SET tuling zhuge6662oK
127.0.0.1:6379> TTL tuling // key永远不过期了!4 (integer) -1
导致这个问题的原因在于:SET
命令如果不设置过期时间,那么Redis 会自动擦除这个key的过期时间
如果你发现Redis的内存持续增长,而且很多key原来设置了过期时间,后来发现过期时间丢失了,很有可能是因为这个原因导致的。
这时你的Redis中就会存在大量不过期的 key,消耗过多的内存资源
所以,你在使用SET
命令时,如果刚开始就设置了过期时间,那么之后修改这个key,也务必要加上过期时间的参数,避免过期时间丢失问题。
Redis对于过期key的处理一般有惰性删除和定时删除两种策略
1、惰性删除:当读/写一个已经过期的key时,会触发惰性删除策略,判断key是否过期,如果过期了直接删除掉这个key。
2、定时删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期(默认每100ms)主动淘汰一批已过期的key ,这里的一批只是部分过期key,所以可能会出现部分key已经过期但还没有被清理掉的情况,导致内存并没有被释放。
Redis Key没设置过期时间为什么被Redis主动删除了
当redis已用内存超过maxmemory
限定时,触发主动清理策略
主动清理策略在Redis 4.0之前一共实现了6种内存淘汰策略,在4.0之后,又增加2种策略,总共8种:
a)淘汰策略只对设置了TTL的key生效:
- volatile-ttl:是基于生存时间(Time-To-Live,TTL)的策略。这种策略会从已设置过期时间的数据集中挑选将要过期的数据进行淘汰。淘汰的策略不是LRU,而是基于key的剩余寿命TTL的值,TTL越小的key越优先被淘汰。
- volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
- volatile-lru:会使用LRU 算法筛选设置了过期时间的键值对删除。
- volatile-lfu:会使用LFU 算法筛选设置了过期时间的键值对删除。
b)淘汰策略对所有key生效:
- allkeys-random:从所有键值对中随机选择并删除数据。
- allkeys-lru:使用LRU (Least Recently Used)算法在所有数据中进行筛选删除。
- allkeys-lfu:使用LFU(Least Frequently Used)算法在所有数据中进行筛选删除。
c) 不处理:
- noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed whenused memory",此时Redis只响应读操作。(默认的内存淘汰策略)
LRU (Least Recently Used)算法:在淘汰时应该优先选择最近最少使用的数据项。LRU算法维护一个使用顺序队列,最近访问的数据项被移动到队列的末尾(redis默认)。
LFU(Least Frequently Used)算法:在淘汰时应该优先选择最不经常使用的数据项。LFU算法维护一个使用频率计数器,记录每个数据项被访问的次数。当存在大量的热点缓存数据时,LFU可能更好。
删除Key的命令会阻塞Redis吗
会!
当你试图删除一个非常大的key时。例如,如果你有一个包含数百万个元素的列表或集合或者很大的String,并且你试图用一个DEL命令来删除它,那么这个操作可能会花费一些时间,并在此期间阻塞其他操作。
为什么当Key非常大的时候redis会阻塞?
Redis是单线程的,这意味着它一次只能执行一个操作。当你要求Redis删除一个大的key时,这个操作可能会花费一些时间。在这个时间内,Redis不能处理其他任何操作,因此会出现阻塞。
这是因为在Redis中,删除一个key需要遍历并释放key所关联的所有内存。如果key关联的数据结构非常大(例如,一个包含数百万个元素的列表或集合),那么遍历和释放内存的过程就会耗费更多的时间。在这个过程中,Redis不能处理其他操作,因此会出现阻塞。
为了避免这种阻塞,Redis 4.0引入了UNLINK
命令。当你使用UNLINK
命令删除一个key时,Redis会立即返回,并在后台异步删除key。这样,即使你正在删除一个大的key,其他的Redis操作也不会被阻塞。但是,UNLINK命令不能保证立即删除key,如果你需要立即删除key,你仍然需要使用DEL命令。
Redis 主从、哨兵、集群架构的优缺点
- 监控:哨兵会不断的检查master和slave是否按照预期工作
- 自动故障恢复:如果master故障了,就选取一个slave来“当老大”,即使故障修复了也不会换回来。
- 通知:在选出新的master后,需要让其他的slave和客户端与新的master通信,那么就需要哨兵在执行故障转移的时候,通知其他的slave和客户端。
哨兵节点(sentinel)也是redis的实例,对每一个主从redis节点监听
客户端直接访问哨兵节点。
如果主节点挂了
哨兵会监视到,然后从从节点中选取一个作为主节点。并且把这个新的主节点告诉客户端。
主从切换的时候不需要运维介入,全程自动化。
切换的过程虽然自动化,但是没有那么快,如果在这个过程中访问可能会瞬断/报错。
单节点实际支持的并发只有10万。大公司满足不了并发。
单节点不宜设置过大<10G,太大会影响数据恢复或主从同步的效率
Redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片的特性。
假设存储100G的数据,总的缓存数据分片存储(一个主节点30G,30G,40G),可以现行扩容到上万个节点,但是官方推荐不超过1000个。
瞬断的问题:假设一个主节点挂了,那么会选取一个从节点作为主节点,访问当前主节点的数据会瞬断,但是如果访问的数据在其他主从节点就不会瞬断,因此还存在瞬断,但是概率降低了。
redis集群中一个master挂了,怎么选举新的master
在Redis集群中,当一个master节点宕机或者不可达时,会从它的slave节点中选出一个新的master节点接替它,具体的选举过程如下:
- 发现故障
集群中的每个节点会定期使用ping消息检测相邻节点是否可达。如果一个节点检测到master节点失败,会将这个master标记为PFAIL状态。
- 选举投票
检测到故障的slave节点会向集群内的所有master节点发送投票消息,请求将自己设置为新的master。
- 处理投票
收到投票请求的master节点会比较所有候选slave,根据slave优先级、偏移量(offset)、运行时长等来选出最合适的那个slave。
- 响应投票
集群中的所有master节点向该slave发送确认消息,同意其成为新的master。
- 配置更新
该slave在收到超过半数(包括自己的主节点 )master的确认后,会将自己的配置切换为master,包括生成新的节点ID。(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)
- 主从切换
原来的slave会向新的master发送SYNC命令,成为其slave。新的master会更新原来master的slot信息。
至此,选举完成,这个slave已切换成新的master,原有的master节点也从集群中移除。这保证了集群的高可用性。
整个重新选举的时间通常在1-2秒,对客户端无感知。Redis集群能够自动快速完成故障转移和新master的选举。
Redis集群数据hash分片算法
Redis集群将所有数据划分为16384个槽位(slot)
Redis执行命令有死循环阻塞bug
RANDOMKEY
从当前数据库中返回(不删除)一个key。
#数据库不为空
redis> MSET fruit "apple" drink "beer" food "cookies" #设置多个key
OK
redis> RANDOMKEY
"fruit"
redis> RANDOMKEY
"food"
redis> KEYS * #查看数据库内所有key,证明RANDOMKEY 并不删除key
1) "food"
2) "drink"
3) "fruit"
Redis对于过期Key的处理策略是惰性删除的方式,RANDOMKEY在随机拿到一个key时,首先会检查key是否过期,如果过期,就会删除这个key,因为这个key被删除了,RANDOMKEY无法把当前key作为正确结果输出,因此会再次查找下一个随机key。如果有大量key已经过期还未来得及被清理调,那么会一直去寻找没有过期的key,这个过程可能会持续很久,因此会影响redis性能。
Redis 在复制过程中使用的是异步复制机制,主服务器将命令发送给从服务器,但从服务器并不会中断正在执行的查询命令来执行主服务器发送的删除命令。相反,从服务器会继续处理当前的查询命令,保持与主服务器的同步,并且只有在查询命令执行完毕后,才会开始执行主服务器发送的删除命令。
如果在从节点(slave)上执行RANDOMKEY,那么问题会更严重,
slave自己是不会清理过期key,当一个key要过期时,master会先清理删除它,之后master向slave发送一个DEL命令,告诉slave也删除key以此达到主从库的数据一致性。
因此在redis中存在大量过期key的情况下,在slave身上执行RANDOMKEY取到随机但是过期的key不会删除它,而是继续寻找不过期的key,由于大量key都已过期,因此大概率陷入死循环状态。
这其实是Redis的一个Bug,这个bug一直持续到5.0才被修复。
修复的解决方案就是在slave中的查找次数做出限制。
一次线上事故,Redis主从切换导致了缓存雪崩
假设slave的机器时钟比master走得快很多,比如主服务器12点,此时从服务器已经1点了。
此时master中一些没有过期的key,在slave的视角就是过期的,如果此时进行主从切换操作,把时间快的从服务器换到主服务器上,新主服务器就会开始大量清理过期key,引发缓存雪崩。
一定要保证主从服务器时钟的一致性。
Redis持久化RDB、AOF、混合持久化是怎么回事
RDB快照(snapshot)
在默认情况下,Redis将内存数据库快照保存在名字为dump.rdb文件中。
需要注意的是,RDB 持久化是一种全量持久化方式,它将整个 Redis 数据集写入到磁盘。因此,在每次持久化操作期间,Redis 会将内存中的所有数据保存到 dump.rdb 中,而不是增量地向文件中添加数据。
rdb的启动方式:
save 900 1
save 300 10
save 60 10000 #在60秒的实践中10000个改动
在redis中的save命令可以手动持久化数据。
bgsave
(backgroundsave)后台save,其是通过操作系统提供的写时复制技术(Copy-On-Write,COW)实现的,在生成快照的同时,依然可以正常处理命令。
在Redis中,bgsave
命令用于在后台异步地创建当前数据库的快照。这个命令实际上会调用fork()系统调用来创建一个子进程,子进程会将数据写入磁盘。 在创建子进程时,父进程的内存空间会被复制到子进程。这是一个昂贵的操作,因为如果你有一个占用了10GB内存的Redis实例,那么执行fork()就需要额外的10GB内存。但是,这就是Copy-On-Write技术发挥作用的地方。
Copy-On-Write (COW) 是一种可以延迟或避免复制数据的技术。当父进程创建子进程时,操作系统并不立即复制所有的内存页,而是让父进程和子进程共享同样的内存页。只有当其中一个进程尝试修改某个内存页时,操作系统才会创建那个内存页的副本,让修改的进程(主进程)使用这个副本,而另一个进程(bgsave子进程)继续使用原来的内存页。这就是所谓的"写时复制"。
在Redis的BGSAVE命令中,COW技术可以帮助节省内存。当BGSAVE命令执行时,子进程开始写快照,而父进程继续处理命令。如果在此过程中,父进程需要修改某个内存页,那么操作系统会为父进程创建一个新的内存页,而子进程仍然可以看到原来的内存页,因此可以正确地将数据写入快照。
总的来说,COW技术允许Redis在创建快照时最大限度地节省内存,并且确保了快照的一致性,因为子进程看到的始终是fork()时的数据。
两者的区别如下。
缺点:
- 生成快照占用内存巨大。
- 按照快照的保存策略进行持久化,例如save 60 1000,如果在下一个60秒内没有满足save条件但是redis宕机了,那么就有数据丢失的问题。
AOF(Append-Only file)持久化
可以通过修改配置文件来打开AOF功能
# appendonly yes
将修改的每一条指令记录进文件appendonly.aof中(先写入os cache每隔一段时间fsync到磁盘),逐条的把所有命令执行。
可以配置redis多久fsync到磁盘一次:
appendfsync always #每次有新命令追加到AOF文件时就执行一次fsync
appendfsync everysec #每秒fsync一次,足够快,并且故障时只会丢失1秒钟的数据
appendfsync no #从不fsync,将数据交给操作系统来处理,足够快但是不安全。
推荐并且默认的就是每秒fsync一次,appendfsync everysec具体来说:
- Redis会用一个1秒计时器来实现这个时间间隔的控制。
- 在一个1秒时间段内,所有发生的写命令会存在内存缓冲区。
- 到了每个1秒时间点,Redis会把这1秒内缓冲起来的所有写命令一次性异步写入AOF文件。
- 然后计时器重置,开始计时下一个1秒时间段内的写命令。
缺点:redis运行时间很长,AOF文件会很大,恢复的速度会很慢。
RDB和AOF的优劣:
生产环境可以都启动
AOF重写:
incr readcount #多次执行,会在AOF文件中多次记录
set readcount n #不如直接set为最后一次的readcount的值
多条命令可以直接重写为一条命令。
#aof文件至少要达到64m才会自动重写,文件太小恢复速度本来就很快,重写意义不大
auto-aof-rewrite-percentage 100
#aof文件自上次重写后文件大小增长了100%会触发重写
auto-aof-rewrite-min-size 64mb
aof重写是执行bgrewriteaof命令重写aof,类似bgsave也是有些消耗性能。
Redis4.0 混合持久化
必须先开启AOF
aof-use-rdb-preamble yes
AOF在重写时,直接写成RDB的二进制格式数据,.aof文件中追加 快照的形式。
混合持久化的文件结构。
需要注意的是,这个RDB格式的快照是直接写入到AOF文件中的,而不是作为一个单独的文件存在。也就是说,一个AOF文件可能既包含RDB格式的数据快照,又包含普通的AOF命令。实际上,我们无法直接读取或者理解RDB格式的数据快照,因为它是二进制的。我们只能知道,当Redis加载这个AOF文件时,它会首先加载RDB格式的数据快照,然后再执行后面的命令,从而恢复所有的数据。
此时就不需要rdb持久化方式了。
优势:
- 数据格式更加紧凑,
- 数据启动恢复效率更高,
- 又兼顾安全性。
线上Redis持久化策略一般如何设置
如果对性能要求较高,在Master最好不要做持久化,可以在某个Slave开启AOF备份数据,策略设置为每秒同步一次即可。
一次线上事故,Redis主节点宕机导致数据全部丢失
如果你的Redis 采用如下模式部署,就会发生数据丢失的问题:
- master-slave+哨兵部署实例。
- master 没有开启数据持久化功能。
- Redis进程使用supervisor管理,并配置为进程宕机,自动重启。
如果此时master宕机,就会导致下面的问题:
- master宕机,哨兵还未发起切换,此时 master进程立即被supervisor自动拉起。
- 但master没有开启任何数据持久化,启动后是一个空实例。
- 此时 slave为了与master保持一致,它会自动清空实例中的所有数据,slave也变成了一个空实例。在这个场景下,master / slave 的数据就全部丢失了。
这时,业务应用在访问Redis时,发现缓存中没有任何数据,就会把请求全部打到后端数据库上,这还会进一步引发缓存雪崩,对业务影响非常大。
这种情况下我们一般不应孩给Redis主节点配置进程宕机马上自动重启策略,而应该等哨兵把某个Redis从节点切换为主节点后再重启之前宕机的Redis主节点让其变为slave节点。
Supervisor是用Python开发的一个client/server服务,是Linux/Unix系统下的一个进程管理工具,不支持Windows系统。它可以很方便的监听、启动、停止、重启一个或多个进程。用用Supervisor管理的进程,当一个进程意外被杀死,supervisort监听到进程死后,会自动将它重新拉起,很方便的做到进程自动恢复的功能,不再需要自己写shell脚本来控制。
Redis线上数据如何备份
- 写crontab定时调度脚本,每小时都copy一份rdb或aof文件到另一台机器中,保留最近48小时的备份。
- 每天都保留一份当日的数据备份到一个目录中去,可以保留最近一个月的的备份
- 每次copy备份的时候,都把太旧的备份删除。
Redis主从复制风暴是怎么回事
大规模数据同步
如果Redis主节点有很多从节点,在某一时刻如果所有从节点都同时连接柱节点,那么主节点会同时把内存快照RDB发送给多个从节点,这样导致Redis主节点压力非常大,此外还有可能因为数据量非常大而导致主从复制风暴。这就是所谓的Redis主从复制风暴问题。
这种问题可以对Redis主从架构做一些优化得以避免。
网络延迟或拥塞
主节点和从节点之间的网络延迟或拥塞也可能导致主从复制风暴。当网络延迟较高时,主节点可能会频繁地重新发送数据同步请求,从节点收到大量的重复数据,导致不断重复的同步过程。如果网络带宽不足或网络拥塞,数据同步的速度将受到影响,可能会导致复制风暴。
解决方案:
优化主从节点之间的网络连接,确保带宽充足,减少网络延迟和拥塞的影响。可以使用高速网络连接、合理配置网络参数等方式来改善网络性能。
批量操作或大量写入
如果在主节点上进行了大批量的写入操作,或者有大量的写入请求同时涌入主节点,主节点需要将这些写入操作同步到从节点,可能引发主从复制风暴。大规模的数据写入会导致复制缓冲区堆积,从节点无法及时处理所有的写入请求,造成复制延迟和复制风暴。
解决方案:
控制写入压力:限制写入操作的频率和并发量,避免批量写入操作和大量写入请求同时涌入主节点,以减轻主从复制的压力。
定期监控主从复制的延迟和系统负载情况,及时发现问题并采取相应的调优措施,如增加从节点、调整复制缓冲区大小等。
Redis集群网络抖动导致频繁主从切换怎么处理
真实世界机房网络往往不是风平浪静的。
为解决这种问题Redis Cluster提供一种选项cluster-node-timeout,其作用是当某个节点连续timeout的时间才失联,才可以认定该节点出现故障需要主从切换,如果这个选项设置的timeout时间较低或者为0,网络抖动会导致频繁切换(数据也需要重复复制)。
Redis集群支持批量操作命令吗
可以,mset、mget等命令支持多个key的原生批量操作命令,但是redis集群只支持所操作的key都落在同一个slot的情况。
如果多个key不在同一个slot,则命令会报错。
如果有多个key一定要用mset命令在集群中操作,这可以在key的前面加上相同的{XXX}(hash tag),这样进行hash分片算法时只会使用大括号里的值进行计算,可以保证不同的key能落到同一个slot中。
mset {user1}:1:name zhuge {user1}:1:age 18
Lua脚本能在Redis集群里执行吗
Redis官方规定Lua脚本如果想在Redis集群里执行,需要Lua脚本里操作的所有Redis Key落在集群的同一个节点上,这种的话我们可以给Lua脚本的Key前面加一个相同的hash tag,就是{XXX},这样就能保证Lua脚本里所有Key落在相同的节点上了。
分布式锁
redisson分布式锁实现原理
Redissson中的分布式锁主要是基于Redlock算法实现的,其具体实现原理可以概括为:
- 获取锁
在获取分布式锁时,Redisson会使用Redis的SETNX
命令去设置一个锁的key(注意要添加过期时间,否则当setnx锁住的线程宕机那么其他线程永远无法得到这个锁)。如果设置成功,表示获取锁,设置失败则未获取锁。
SETNX
命令实际上就就是为了解决java的synchronized锁只能锁本地,对于分布式的服务无法上锁。
- 随机等待
设置失败时,会根据超时时间生成一个随机等待时间,等待后再次尝试获取锁。这样可以避免多个节点在同一时刻重复请求锁。
- 检查占有
成功获取到锁后,会启动一个定时续期线程,周期性地续期锁的超时时间(添加子线程,每十秒确认线程是否在线,如果在线则重设过期时间),以避免锁被自动释放。
这是由于当业务处理时间大于锁的过期时间,需要延长锁的周期,防止在业务处理的过程中锁被释放。
- 释放锁
释放锁时,会发送一条Lua脚本,这段脚本可以保证只有锁真正的占有者才能够释放锁。释放时需要判断是否是某个id的值,且占有锁的时间内才可以。(给锁加唯一的ID(UUID))
这是防止一个线程处理完业务,释放锁时释放的是别的线程持有的锁。
- 失败重试
获取或释放锁失败时,会重试一定次数,以便在并发情况下能够重试成功。
- 解决死锁
如果一个节点在指定时间内一直失败,会通过让当前线程睡眠一段时间来避免死锁。
以上加锁、占有、释放、重试的流程,可以保证分布式环境下Redisson锁的安全性和可靠性。
RLock lock = redisson.getLock(LOCK_KEY);
lock.lock() ;
这种方式有一个问题,就是当加锁是给一组主从节点加锁时,是直接存储在主节点的。由于redis的AP特性,只支持高性能,高可用,不支持高一致性,因此如果此时主节点宕机,从节点没来得及同步主节点的信息,那么会导致锁的加锁失败,因此就有了红锁(Redlock),redlock会将锁强制记录给所有主从节点。
Redlock 红锁
至少三个redis节点,setnx命令,给所有节点加锁,半数以上的节点加锁成功才会加锁成功。
缺点:
- 如果每个节点添加一个从节点,在加锁key的过程中其中一个节点挂了,换成其从节点顶替该节点,那么别的进程就会得不到这个节点的锁。如果要redlock高可用,那么就需要更多的单点服务器。
- redis每秒钟持久化一次,某个节点在持久化的1秒钟时间还没到的情况下,有可能还没加锁成功就挂了,运维重启这个节点,恢复的数据是没有加锁的,这样会导致并发问题。
- 解决方案:每执行一条命令就持久化一次。
大厂线上大规模数据的缓存
只有1%的商品是经常被访问的,因此对数据设置过期时间,比如一天的时间。商品数据被访问就延长过期时间。
冷热数据分离,热点数据放在缓存,冷数据走数据库。
缓存雪崩
什么是缓存雪崩?
当某一个时刻出现大规模的缓存失效的情况,那么就会导致大量的请求直接打在数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。这就是缓存雪崩。
分析
造成缓存雪崩的关键在于在同一时间大规模的key失效。为什么会出现这个问题呢,有几种可能,第一种可能是Redis宕机,第二种可能是采用了相同的过期时间。搞清楚原因之后,那么有什么解决方案呢?
解决方案
1、在原有的失效时间上加上一个随机值,比如1-5分钟随机。这样就避免了因为采用相同的过期时间导致的缓存雪崩。
如果真的发生了缓存雪崩,有没有什么兜底的措施?
2、使用熔断机制。当流量到达一定的阈值时,就直接返回“系统拥挤”之类的提示,防止过多的请求打在数据库上。至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。
3、提高数据库的容灾能力,可以使用分库分表,读写分离的策略。
4、为了防止Redis宕机导致缓存雪崩的问题,可以搭建Redis集群,提高Redis的容灾性。
缓存击穿问题
什么是缓存击穿
热点数据失效,大量请求请求到数据库。
其实跟缓存雪崩有点类似,缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。这种现象就叫做缓存击穿。
解决方案
1、上面说过了,如果业务允许的话,对于热点的key可以设置永不过期的key。
2、使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。当然这样会导致系统的性能变差。
缓存穿透
什么是缓存穿透?
我们使用Redis大部分情况都是通过Key查询对应的值,假如发送的请求传进来的key是不存在Redis中的,那么就查不到缓存,查不到缓存就会去数据库查询。假如有大量这样的请求,这些请求像“穿透”了缓存一样直接打在数据库上,这种现象就叫做缓存穿透。
分析
关键在于在Redis查不到key值,这和缓存击穿有根本的区别,区别在于缓存穿透的情况是传进来的key在Redis中是不存在的。假如有黑客传进大量的不存在的key,那么大量的请求打在数据库上是很致命的问题,所以在日常开发中要对参数做好校验,一些非法的参数,不可能存在的key就直接返回错误提示,要对调用方保持这种“不信任”的心态。
解决方案
1、把无效的Key存进Redis中。如果Redis查不到数据,数据库也查不到,我们把这个Key值保存进Redis,设置value="null",当下次再通过这个Key查询时就不需要再查询数据库。这种处理方式肯定是有问题的,假如传进来的这个不存在的Key值每次都是随机的,那存进Redis也没有意义。
2、使用布隆过滤器。布隆过滤器的作用是某个 key 不存在,那么就一定不存在,它说某个 key 存在,那么很大可能是存在(存在一定的误判率)。于是我们可以在缓存之前再加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回。
突发性热点缓存重建导致系统压力暴增问题
大V带货,一般是冷门数据,热点商品一般不需要大v带货,由于冷门数据不在缓存,因此会导致大量访问直接请求到数据库。
加锁:
第一个请求进同步代码块会新建key,后面的请求排队过来就不会一直请求数据库,而是直接从缓存中查找了。写成单例设计模式中的DCL的形式。
public Product get(Long productId) {
Product product = null;
string productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
product = getProductFromcache(productCacheKey);
if (product !=null ){
return product;
}
RLock hotCacheCreateLock = redisson.getLock( LOCK_HOT_CACHE_CREATE_PREFIX + productTd);
hotCacheCreateLock.lock();
try {
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
product = productDao. get(productId);
if (product != null) {
redisutil.set(productCacheKey,JSON.toJSoNstring(product),
genProductCacheTimeout(0),TimeUnit.SECONDS);
}else {
redisUtil.set(productCacheKey,
EMPTY_CACHE,
genEmptyCacheTimeout(),
TimeUnit.SECONDS);
}finally {
hotCacheCreateLock. unlockO);
}
return product;
}
缺点:
- synchronized锁是本地jvm的锁。
- 如果有两个直播间:A和B,直播间A的商品上锁,B的商品也需要等待。即synchronized(对象){}
还要维护一个商品id对象,不同的商品用不同(商品id对象)的锁方式。或者使用分布锁,使用分布锁,所有问题都解决了
public static final String Lock_HOT_CREATE_PREFIX="lock:hot_cache_create";
redisson.getLock(Lock_HOT_CREATE_PREFIX+productId);
hotCacheCreateLock.lock();
try{
// 逻辑代码 从缓存中获取key,如果获取不到就查数据库。
// ......
}final{
hotCacheCreateLock.unlock(); // del(lockKey)
}
分布式锁解决数据库和缓存双写不一致问题
线程3在更新缓存前,线程2执行了写数据库和更新缓存操作,结果缓存和数据库不一致
删除缓存的方法也不行,因为本来线程3就是缓存为空,就是要查数据库给key赋值。
解决方案
分布式锁。问题是由于并发读写同一条数据导致的。
读缓存和查找缓存都使用同一把锁,一个线程拿到锁另一个线程不能修改数据
public Product get(Long productId) {
Product product = null;
string productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
product = getProductFromcache(productCacheKey);
if (product !=null ){
return product;
}
RLock hotCacheCreateLock = redisson.getLock( LOCK_HOT_CACHE_CREATE_PREFIX + productTd);
hotCacheCreateLock.lock();
try {
product = getProductFromCache(productCacheKey);
if (product != null) {
return product;
}
RLock updateProductLock =
redisson.getLock( LOCK_PRODUCT_UPDATE_PREFIX + productId);
updateProductLock.lock();
try {
product = productDao. get(productId);
if (product != null) {
redisutil.set(productCacheKey,JSON.toJSoNstring(product),
genProductCacheTimeout(0),TimeUnit.SECONDS);
}else {
redisUtil.set(productCacheKey,
EMPTY_CACHE,
genEmptyCacheTimeout(),
TimeUnit.SECONDS);
} finally {
updateProductLock.unlock();
}finally {
hotCacheCreateLock. unlockO);
}
return product;
}
public Product update(Product product) {
Product productResult = null;
RLock updateProductLock = redisson.getLock( name LOCK_PROOUCT_UPOATE_PREFIX + product.getId())updateProductLock.lock();
try {
productResult =productDao. update(product);
redisUtil.set( key:.RedisKeyPrefixConst.PROOUCT_CACHE + productResult.getTd(),JSOM . to3SONString(productResult)
genProductCacheTimeout(),TimeUnit.SECONDS);
}finally {
updateProductLock.unloek(;
}
return productResult;
}
优化性能
大部分场景读多写少
- 基于读写锁做优化。
对同一个key加两个锁,一个读锁一个写锁,都是读的时候可以并行执行,有写操作的时候加写锁,和读锁互斥,读的线程都要等待
RReadWriteLock readWIiteLock =
nedisson.getReadlWIniteLock(LOCK_PRODUCT_UPDATE_PREFIX + productIo);
RLock writeLock = readWriteLock.writeLock();
- tryLock(time,unit);
- time是最大等待锁的时间,超过这个时间就不等了
- unit是时间单位
time秒之后就不等了
利用多级缓存架构解决缓存雪崩问题
可以用jvm缓存,比如hashmap
public static HashMap<String,Product> productMap=new HashMap<>();
private Product getProductFromCache(String productCacheKey) {
Product product = null;
product = productMap.get(productCacheKey);if (product != null) {
return product;
}
// 逻辑代码:使用redis缓存再查数据
}
mysql和redis怎么保持数据一致性
一般情况下,Redis用来实现应用和数据库之间读操作的缓存层,主要目的是减少数据库IO,还可以提升数据的IO性能。
当应用程序需要去读取某个数据的时候,首先会先尝试去 Redis里面加载,如果命中就直接返回。如果没有命中,就从数据库查询,查询到数据后再把这个数据缓存到Redis里面。
在这样一个架构中,会出现一个问题,就是一份数据,同时保存在数据库和 Redis里面,当数据发生变化的时候,需要同时更新Redis和 Mysql,由于更新是有先后顺序的,并且它不像Mysql中的多表事务操作,可以满足ACID特性。所以就会出现数据一致性问题。
在这种情况下,能够选择的方法只有几种。
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
如果先更新数据库,再更新缓存,如果缓存更新失败,就会导致数据库和 Redis中的数据不一致。
如果是先删除缓存,再更新数据库,理想情况是应用下次访问Redis 的时候,发现Redis里面的数据是空的,就从数据库加载保存到Redis里面,那么数据是一致的。但是在极端情况下,由于删除Redis和更新数据库这两个操作并不是原子的,所以这个过程如果有其他线程来访问,还是会存在数据不一致问题。
所以,如果需要在极端情况下仍然保证Redis和 Mysql的数据一致性,就只能采用最终一致性方案。
比如基于RocketMQ的可靠性消息通信,来实现最终一致性。
还可以直接通过Canal组件,监控Mysql中 binlog 的日志,把更新后的数据同步到Redis 里面。
因为这里是基于最终一致性来实现的,如果业务场景不能接受数据的短期不一致性,那就不能使用这个方案来做。
以上就是我对这个问题的理解。
RocketMQ是怎么实现数据一致性的
在面过的几家大厂中,几乎每轮的面试官(没写错,几乎是每轮面试官)都问了同样一个问题:你们的系统是分布式的系统吗?
答:是。
面试官:那么你们分布式的系统是如何解决分布式事务这个问题的呢?也就是如何保证数据的一致性。
答:我们的系统中通过 RocketMQ 的事务消息来保证数据的最终一致性。
面试官:那你说说它是如何来保证数据的最终一致性的?
答:分两部分来回答,第一部分先回答事务消息的实现流程,第二部分解释为什么它能保证数据的最终一致性。
事务消息的实现流程
- 首先服务 A 发送一个半事务消息(也称 **half **消息)至 MQ 中。
- 为什么要先发送一个 half 消息呢?这是为了保证服务 A 和 MQ 之间的通信正常,如果无法正常通信,则服务 A 可以直接返回一个异常,也就不用处理后面的逻辑的了。
-
如果 half 消息发送成功,MQ 收到这个 half 消息后,会返回一个 success 响应给服务 A。
-
服务 A 接收到 MQ 返回的 success 响应后,开始处理本地的业务逻辑。
-
提交/commit本地事务
- 如果服务 A 本地事务提交成功,则会向 MQ 中发送 commit,表示将 half 消息提交,MQ 就会执行第 5 步操作;
- 如果服务 A 本地事务提交失败,则直接回滚本地事务,并向 MQ 中发送 rollback,表示将之前的 half 消息进行回滚,MQ 接收到 rollback 消息后,就会将 half 消息删除。
- MQ 如果 commit,则将 half 消息写入到磁盘。.
- 如果 MQ 长时间没有接收到 commit 或者 rollback 消息,例如:服务 A 在处理本地业务时宕机了,或者发送的 commit、rollback 因为在弱网环境,数据丢失了。那么 MQ 就会在一定时间后尝试调用服务 A 提供的一个接口,通过这个接口来判断 half 消息的状态。所以服务 A 提供的接口,需要实现的业务逻辑是:通过数据库中对应数据的状态来判断,之前的 half 消息对应的业务是否执行成功。如果 MQ 从这个接口中得知 half 消息执行成功了,那么 MQ 就会将 half 消息持久化到本地磁盘,如果得知没有执行成功,那么就会将 half 消息删除。
- 服务 B 从 MQ 中消费到对应的消息。
- 服务 B 处理本地业务逻辑,然后提交本地事务。
如何保证数据的最终一致性
实现流程说完了,可能你现在有各种各样的疑惑?
Q: half 消息是个啥?
A: 它和我们正常发送的普通消息是一样的,都是存储在 MQ 中,唯一不同的是 half 在 MQ 中不会立马被消费者消费到,除非这个 half 消息被 commit 了。(至于为什么未 commit 的 half 消息无法被消费者读取到,这是因为在 MQ 内部,对于事务消息而言,在 commit 之前,会先放在一个内部队列中,只有 commit 了,才会真正将消息放在消费者能读取到的 topic 队列中)
Q: 为什么要先发送 half 消息?
A: 前面已经解释过了,主要是为了保证服务 A 和 MQ 之间是否能正常通信,如果两者之间都不能正常通信,后面还玩个锤子,直接返回异常就可以了。
Q: 如果 MQ 接收到了 half 消息,但是在返回 success 响应的时候,因为网络原因,导致服务 A 没有接收到 success 响应,这个时候是什么现象?
A: 当服务 A 发送 half 消息后,它会等待 MQ 给自己返回 success 响应,如果没有接收到,那么服务 A 也会直接结束,返回异常,不再执行后续逻辑。不执行后续逻辑,这样服务 A 也就不会提交 commit 消息给 MQ,MQ 长时间没接收到 commit 消息,那么它就会主动回调服务 A 的一个接口,服务 A 通过接口,查询本地数据后,发现这条消息对应的业务并没有正常执行,那么就告诉 MQ,这个 half 消息不能 commit,需要 rollback,MQ 知道后,就将 half 消息进行删除。
Q: 如果服务 A 本地事务执行失败了,怎么办?
A: 服务 A 本地事务执行失败后,先对自己本地事务进行回滚,然后再向 MQ 发送 rollback 操作。
Q: 服务 A 本地事务提交成功或失败后,向 MQ 发送的 commit 或者 rollback 消息,因为网络问题丢失了,又该怎么处理?
A: 和上一个问题一样,MQ 长时间没有接收到 half 消息的 commit 或者 rollback 消息,MQ 会主动回调服务 A 的接口,通过这个接口来判断自己该对这个 half 消息如何处理。
Q: 前面说的全是事务消息的实现流程,这和事务消息如何保证数据的最终一致性有什么关系呢?
A: 有关系。首先,服务 A 执行本地事务并提交和向 MQ 中发送消息这是两个写操作,然后通过 RocketMQ 的事务消息,我们保证了这两个写操作要么都执行成功,要么都执行失败。然后让其他系统,如服务 B 通过消费 MQ 中的消息,然后再去执行自己本地的事务,这样到最后,服务 A 和服务 B 这两个系统的数据状态是不是达到了一致?这就是最终一致性的含义。
而RocketMQ作为一种消息队列,其本身特点是异步、解耦,无法保证服务A和服务B在同一时刻的数据强一致性。它只能保证最终一致性。
目前通过可靠消息来保证数据的最终一致性是很多大厂都采用的方案,基本都是通过 MQ 和补偿机制来保证数据的一致性。(所谓的可靠消息,就是消息不丢失,如何保证 MQ 的消息不丢失,下篇文章会写,这也是面试常考题)
Q: 服务 B 本地事务提交失败了,怎么办?
A: 如果服务 B 本地事务提交失败了,可以进行多次重试,直到成功。如果重试多次后,还是提交失败,例如此时服务 B 对应的 DB 宕机了,这个时候只要服务 B 不向 MQ 提交本次消息的 offset 即可。如果不提交 offset,那么 MQ 会在一定时间后,继续将这条消息推送给服务 B,服务 B 就可以继续执行本地事务并提交了,直到成功。这样,依旧是保证了服务 A 和服务 B 数据的最终一致性。
————————————————
版权声明:本文为CSDN博主「qq_34436819」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34436819/article/details/114444204