Redis的其他知识点

缓存的集中读写模式

Cache Aside Pattern

​ Cache Aside Pattern(旁路缓存:常用):是最经典的缓存+数据库读写模式:读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
在这里插入图片描述

更新的时候,先更新数据库,然后再删除缓存。
在这里插入图片描述

为什么是删除缓存,而不是更新缓存?

  1. 一般来说缓存的值是一个结构,hash,list等。如果这个时候去更新数据,需要去遍历找到对应的值在去修改比较耗时。
  2. 使用懒加载.使用的时候才更新缓存。使用的时候才从DB中加载,也可以采用异步的方式填充缓存,比如开启一个线程定时将DB的数据刷到缓存中。

高并发下的坑

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

​ 如果同时有两个写请求需要更新数据,每个写请求都先更新数据库再更新缓存,在并发场景可能会出现数据不一致的情况。在这里插入图片描述

  1. 写请求1更新数据库,将 age 字段更新为18;
  2. 写请求2更新数据库,将 age 字段更新为20;
  3. 写请求2更新缓存,缓存 age 设置为20;
  4. 写请求1更新缓存,缓存 age 设置为18;

​ 执行完预期结果是数据库 age 为20,缓存 age 为20,结果缓存 age为18,这就造成了缓存数据不是最新的,出现了脏数据。

先删缓存,再更新数据库

在一个读请求和一个写请求并发场景下可能会出现数据不一致情况。

在这里插入图片描述

  1. 写请求删除缓存数据;
  2. 读请求查询缓存未击中(Hit Miss),紧接着查询数据库,将返回的数据回写到缓存中;
  3. 写请求更新数据库。
先更新数据库,再删除缓存

在实际的系统中针对写请求还是推荐先更新数据库再删除缓存,但是在理论上还是存在问题。
在这里插入图片描述

  1. 读请求先查询缓存,缓存未击中,查询数据库返回数据;
  2. 写请求更新数据库,删除缓存;
  3. 读请求回写缓存;

​ 整个流程操作下来发现数据库age为20,缓存age为18,即数据库与缓存不一致,导致应用程序从缓存中读到的数据都为旧数据。
​ 但我们仔细想一下,上述问题发生的概率其实非常低,因为通常数据库更新操作比内存操作耗时多出几个数量级,上图中最后一步回写缓存(set age 18)速度非常快,通常会在更新数据库之前完成。
​ 如果这种极端场景出现了怎么办?我们得想一个兜底的办法:缓存数据设置过期时间。通常在系统中是可以允许少量的数据短时间不一致的场景出现。

延时双删
基础延时双删

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间

  1. 先删除缓存
  2. 再写数据库
  3. 休眠500毫秒(根据具体的业务时间来定)
  4. 再次删除缓存。

那么,这个500毫秒怎么确定的,具体该休眠多久呢?

​ 需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。

设置缓存的过期时间

​ 给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

如果写入数据成功,但是删除缓存失败?

​ 操作完数据库后,由于种种原因删除缓存失败,这时,可能就会出现数据不一致的情况。这里,我们需要提供一个保障重试的方案。

方案一

  1. 更新数据库数据;
  2. 缓存因为种种问题删除失败;
  3. 将需要删除的key发送至消息队列;
  4. 自己消费消息,获得需要删除的key;
  5. 继续重试删除操作,直到成功。

​ 该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。

方案二

  1. 更新数据库数据;
  2. 数据库会将操作信息写入binlog日志当中;
  3. 订阅程序提取出所需要的数据以及key;
  4. 另起一段非业务代码,获得该信息;
  5. 尝试删除缓存操作,发现删除失败;
  6. 将这些信息发送至消息队列;
  7. 重新从消息队列中获得该数据,重试操作。

Read/Write Through Pattern

应用程序只操作缓存,缓存操作数据库。

  1. Read-Through(穿透读模式/直读模式):应用程序读缓存,缓存没有,由缓存回源到数据库,并写入缓存。(guavacache)
  2. Write-Through(穿透写模式/直写模式):应用程序写缓存,缓存写数据库。该种模式需要提供数据库的

Write Behind Caching Pattern

​ 应用程序只更新缓存,缓存通过异步的方式将数据批量或合并后更新到DB中。不能实时同步,并且可能会丢失数据。

缓存所带来的的问题

缓存穿透

  • 产生原因
    缓存穿透是指查询一个一定不存在的数据。由于缓存不命中,并且出于容错考虑,如果从数据库查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。请求的数据在缓存大量不命中,导致请求走数据库。
    缓存穿透如果发生了,也可能把我们的数据库搞垮,导致整个服务瘫痪!
  • 解决方案
  1. 验证拦截:接口层面进行校验,如鉴定用户权限,对ID之类的字段做基础校验。如ID<=0等
  2. 缓存空数据,当数据库也查询不到数据的时候,将这条数据也进行缓存。但是如果采取这种措施的话。应该将该缓存的有效性设置短一点。防止影响正常数据的缓存,也可以节省缓存占用的空间。
  3. 其次就是使用布隆过滤器进行拦截,但是存在一定的误差。当它自定一个数据存在的时候,它不一定存在。但是当它指定一个数据不存在的时候,那么它一定不存在。
    1. 布隆过滤是一种比较特殊的结构,类似HashMap。布隆过滤器是用多个bit位代替HashMap里面的数组。这样存储空间就下来了。其次就是会对key进行多次hash。将key的hash值所对应的bit位为1。
    2. 当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。
    3. 当判断一个元素是否存在的时候,就去判断这个值的hash值的bit位是否都为1,如果都为1,那么可能存在。如果有一个bit位不为1,就肯定不存在。
    4. 布隆过滤器并不支持删除操作,只支持添加操作。这一点很容易理解,因为你如果要删除数据,就得将对应的bit位置为0,但是你这个Key对应的bit位可能其他的Key也对应着。 因为hash会存在hash冲突

缓存雪崩

  • 产生原因

我们都知道Redis不可能把所有的数据都缓存起来,所以Redis需要对数据设置过期时间,并采用的是惰性删除(放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,那就返回该键)+定期删除两种策略对过期键删除。如果缓存数据设置的过期时间是相同的,并且Redis恰好将这部分数据全部删光了。这就会导致在这段时间内,这些缓存同时失效,全部请求到数据库中。这就是缓存雪崩:Redis挂掉了,请求全部走数据库。缓存雪崩如果发生了,很可能就把我们的数据库搞垮,导致整个服务瘫痪!

  • 解决方案
    • 在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期。首先强调的是缓存雪崩对底层系统的冲击非常可怕。
    • 有一个简单处理方案,就是将缓存失效时间分散开,比如我们在原有失效时间上增加一个随机值,如1~5分钟随机,尽量让缓存不要同时失效,从而尽量避免缓存雪崩。
    • 实现高可用架构,尽量避免Redis挂掉的情况。
    • Redis挂掉后采用本地缓存和限流策略,避免DB直接被干掉。
    • Redis持久化,Redis挂掉后,重启后可以自动从磁盘中加载数据,能快速回复数据。

缓存击穿(失效)

  • 产生原因
    对于一些设置了过期时间的Key,当这些Key在被某些时间点大量高并发访问时,这个时候就需要考虑缓存被“击穿”的问题,这个问题和雪崩区别在于只针对某个Key的缓存,而缓存雪崩是针对多个Key的缓存。简单来说,就是当某个时间点某个Key被高并发访问,此时恰好缓存过期,那么所有请求都落到DB上了,这是瞬时的大并发就有可能导致将DB压垮,这种现象就叫缓存击穿。
  • 解决方案
    • 一劳永逸的方法,就是设置数据永不过期。
    • 后台刷新:后台定义一个job(定时任务)专门主动更新缓存数据.比如,一个缓存中的数据过期时间是30分钟,那么job每隔29分钟定时刷新数据(将从数据库中查到的数据更新到缓存中)。这种方案比较容易理解,但会增加系统复杂度。比较适合那些 key 相对固定,cache 粒度较大的业务,key 比较分散的则不太适合,实现起来也比较复杂。
    • 检查更新:将缓存key的过期时间(绝对时间)一起保存到缓存中(可以拼接,可以添加新字段,可以采用单独的key保存…不管用什么方式,只要两者建立好关联关系就行).在每次执行get操作后,都将get出来的缓存过期时间与当前系统时间做一个对比,如果缓存过期时间-当前系统时间<=1分钟(自定义的一个值),则主动更新缓存.这样就能保证缓存中的数据始终是最新的(和方案一一样,让数据不过期.)。 这种方案在特殊情况下也会有问题。假设缓存过期时间是12:00,而 11:59到 12:00这 1 分钟时间里恰好没有 get 请求过来,又恰好请求都在 11:30 分的时候高并发过来,那就悲剧了。这种情况比较极端,但并不是没有可能。因为“高并发”也可能是阶段性在某个时间点爆发。
    • 分级缓存:采用 L1 (一级缓存)和 L2(二级缓存) 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。请求优先从 L1 缓存获取数据,如果 L1缓存未命中则加锁,只有 1 个线程获取到锁,这个线程再从数据库中读取数据并将数据再更新到到 L1 缓存和 L2 缓存中,而其他线程依旧从 L2 缓存获取数据并返回。 这种方式,主要是通过避免缓存同时失效并结合锁机制实现。所以,当数据更新时,只能淘汰 L1 缓存,不能同时将 L1 和 L2 中的缓存同时淘汰。L2 缓存中可能会存在脏数据,需要业务能够容忍这种短时间的不一致。而且,这种方案可能会造成额外的缓存空间浪费。
    • 互斥锁:这种方案是通过异步方式 去获取缓存过程中,其他key 处于等待现象,必须等待第一个构建完缓存之后,释放锁,其他人才能通过该key才能访问数据; 第一个key1 在查询db,获取数据 到放入缓存过程中,都有把锁 先锁住,其他人就必须等待,等待这个人把缓存设置成功,才去释放锁,那其他人就直接从缓存里面取数据;不会造成数据库的读写性能的缺陷;

big key

大key指的是存储的值(value)非常大

主要影响范围:

  • 大key会大量占用内存,在集群中无法均衡
  • Redis的性能下降,主从复制异常
  • 在主动删除或者过期删除的时候会因为操作时间过长而引起服务阻塞。

redis-cli --bigkeys命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key。

Redis的性能监控指标

connected_clients:68 #连接的客户端数量 
used_memory_rss_human:847.62M #系统给redis分配的内存 
used_memory_peak_human:794.42M #内存使用的峰值大小 
total_connections_received:619104 #服务器已接受的连接请求数量 
instantaneous_ops_per_sec:1159 #服务器每秒钟执行的命令数量 qps 
instantaneous_input_kbps:55.85 #redis网络入口kps 
instantaneous_output_kbps:3553.89 #redis网络出口kps 
rejected_connections:0 #因为最大客户端数量限制而被拒绝的连接请求数量 
expired_keys:0 #因为过期而被自动删除的数据库键数量 
evicted_keys:0 #因为最大内存容量限制而被驱逐(evict)的键数量 
keyspace_hits:0 #查找数据库键成功的次数 
keyspace_misses:0 #查找数据库键失败的次数

说说Redis哈希槽的概念?

Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽, 每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。

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

使用keys指令可以扫出指定模式的key列表。但是如果这个redis正在给线上的业务提供服务,使用keys会导致线程阻塞一段时间,线上服务会停顿,直到指令全部结束,原因时因为Redis时单线程的。这个时候可以使用scan指令,可以无阻塞的去除置顶模式的key列表,但是会有一定的重复率,只需要再客户端做一次去重就可以了,总体花费的时间要比keys时间短。

scan 渐进式遍历key scan 0 match key* count 1000

怎么理解Redis事务?

事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

Redis如何做内存优化?

尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面.

Redis为什么是单线程的,优点

  • Redis采用单线程多进程集群方案
  • Redis是基于内存的操作,CPU不是Redis的瓶颈
  • 瓶颈最有可能是机器内存的大小或者网络带宽
  • 单线程的设计是最简单的
  • 但是对多核CPU利用率不够,所以Redis6采用多线程。
  • 代码更清晰,处理逻辑更简单 不用去考虑各种锁的问题,
  • 不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
  • 不存在多进程或者多线程导致的切换而消耗CPU

单线程的redis为什么这么快

  • redis在内存中操作,持久化只是数据的备份,正常情况下内存和硬盘不会频繁swap
  • 多机主从,集群数据扩展
  • maxmemory的设置+淘汰策略
  • 数据结构简单,有压缩处理,是专门设计的
  • 单线程没有锁,没有多线程的切换和调度,不会死锁,没有性能消耗
  • 使用I/O多路复用模型,非阻塞IO;
  • 构建了多种通信模式,进一步提升性能
  • 进行持久化的时候会以子进程的方式执行,主进程不阻塞

Redis做异步队列

使用list做异步队列的时候,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当的sleep一会再试。或者使用blpop,在没有消息的时候回阻塞,直到消息的到来。

或者使用pub/sub 发布订阅模式,可以实现1:N的消息队列

如果做延时队列

使用zset,那时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指定获取N秒之前的数据,轮询进行处理。

bigkey的危害

导致redis阻塞,网络阻塞。删除的时候不要直接使用del删除,使用hscan、zscan 渐进式删除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值