Redis学习 - 了解Redis(三)

1. 什么是缓存击穿、缓存穿透、缓存雪崩?

1.1 缓存穿透问题

先来看一个常见的缓存使用方式:读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。

缓存穿透 :是指查询一个不存在的数据,由于 Redis 中没有该数据,因此会直接查询数据库,导致数据库压力增大,甚至可能引发恶意攻击。

缓存穿透一般都是这几种情况产生的:

  • 业务不合理的设计,比如大多数用户都没开守护,但是你的每个请求都去缓存,查询某个userid查询有没有守护。
  • 业务/运维/开发失误的操作,比如缓存和数据库的数据都被误删除了。
  • 黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在的业务数据。

如何避免缓存穿透呢? 一般有二种方法。

方法一:缓存空数据

查询返回的数据为空,仍把这个空结果进行缓存;比如一个get请求:gugu/shop/getById/1,可以将{key:1,value:null}存入redis中。

如果查询数据库为空,我们可以给redis缓存设置个空值,或者默认值,当查询一个不存在的数据时,Redis 中有该键,但是该键对应的值是一个空对象,因此不会查询数据库。 注意:有写请求进来的话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。(业务上比较常用,简单有效)。

优点 :实现简单。
缺点:①如果有大量查询的数据都不存在,则redis中会缓存大量空数据,这会消耗内存(这里可以给缓存添加一个TTL,减少内存消耗);②如果原先查的数据不存在,但是后来数据库中又添加上了,可能存在数据不一致的问题。

方法二:布隆过滤器

使用布隆过滤器快速判断数据是否存在。即一个查询请求过来时,先通过布隆过滤器判断值是否存在,存在才继续往下查。

优点 :内存占用较少,没有多余key。
缺点:①实现复杂;②存在误判。

1.3 小结

缓存穿透的解决方案有哪些?

①缓存nul值

②布隆过滤

【额外补充几点】

③增强id的复杂度,避免被猜测id规律

④做好数据的基础格式校验

⑤加强用户权限校验

⑥做好热点参数的限流

1.2 缓存雪奔问题

缓存雪奔 : 是指在同一时段大量的缓存key过期或者Redis服务宕机,导致大量请求都直接访问数据库,引起数据库压力过大甚至down机。

方法一:大量的缓存key失效

给不同的Key的TTL添加随机值,比如将缓存失效时间分散开,可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

方法二:Redis服务宕机

①利用Redis集群 提高服务的可用性,比如哨兵模式、集群模式;

②给缓存业务添加降级限流策略,比如可以在ngxin或spring cloud gateway中处理;(注:降级可做为系统的保底策略,适用于穿透、击穿、雪崩)

③给业务添加多级缓存,比如使用Guava或Caffeine作为一级缓存,redis作为二级缓存等;

1.3 缓存击穿问题

缓存击穿 : 给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮。

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

方法一: 互斥锁

**使用互斥锁方案。**缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。

方法二: 逻辑过期

①在设置key的时候,设置一个过期时间字段,一起存入缓存中,注意不给当前key设置TTL过期时间;

②当查询的时候,从redis取出数据后判断时间是否过期;

③如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,但这个数据不是最新的

"永不过期",是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。

2.Redis缓存之双写一致性

redis做为缓存,mysql的数据如何与redis进行同步呢?(本质上问的就是双写一致性)

2.1 双写一致性定义

双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致。

在这里插入图片描述

读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间;

写操作:延迟双删

2.2 问题分析

这里就会产生一个问题,上图步骤①②可以调换顺序吗?即先删除缓存,还是先修改数据库?

解答:不管先删除缓存,还是先修改数据库,都会产生脏数据 ,所以需要删除两次缓存;由于数据库主从同步的问题,还需要延时删除

但是延迟双删这个延迟时间不好判断;而且在延时的过程中可能会出现脏数据,并不能保证强一致性;

2.3 解决方案

2.3.1 针对一致性要求高

使用分布式锁

  • 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作;
  • 排他锁:也叫独占锁writeLock,加锁之后,阻塞其他线程读写操作;

读数据的时候,添加共享锁,写数据的时候,添加排他锁

虽然使用读写锁,可以保证强一致性,但性能会降低。

2.3.2 针对允许延迟一致

基于MQ的异步通知:使用MQ中间件,更新数据之后,通知缓存删除;

基于Canal的异步通知:利用canal中间件,不需要修改业务代码,伪装为mysql的一个从节点,canal通过读取binlog数据更新缓存;

备注:二进制日志(BINLOG)记录了所有的DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句;

3.说说Redis的常用应用场景

  • 缓存
  • 排行榜
  • 计数器应用
  • 共享Session
  • 分布式锁
  • 社交网络
  • 消息队列
  • 位操作

2.1 缓存

我们一提到redis,自然而然就想到缓存,国内外中大型的网站都离不开缓存。合理的利用缓存,比如缓存热点数据,不仅可以提升网站的访问速度,还可以降低数据库DB的压力。并且,Redis相比于memcached,还提供了丰富的数据结构,并且提供RDB和AOF等持久化机制,强的一批。

2.2 排行榜

当今互联网应用,有各种各样的排行榜,如电商网站的月度销量排行榜、社交APP的礼物排行榜、小程序的投票排行榜等等。Redis提供的zset数据类型能够实现这些复杂的排行榜。

比如,用户每天上传视频,获得点赞的排行榜可以这样设计:

1.用户Jay上传一个视频,获得6个赞,可以酱紫:

zadd user:ranking:2021-03-03 Jay 3

2.过了一段时间,再获得一个赞,可以这样:

zincrby user:ranking:2021-03-03 Jay 1

3.如果某个用户John作弊,需要删除该用户:

zrem user:ranking:2021-03-03 John

4.展示获取赞数最多的3个用户

zrevrangebyrank user:ranking:2021-03-03 0 2

2.3 计数器应用

各大网站、APP应用经常需要计数器的功能,如短视频的播放数、电商网站的浏览数。这些播放数、浏览数一般要求实时的,每一次播放和浏览都要做加1的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。

2.4 共享Session

如果一个分布式Web服务将用户的Session信息保存在各自服务器,用户刷新一次可能就需要重新登录了,这样显然有问题。实际上,可以使用Redis将用户的Session进行集中管理,每次用户更新或者查询登录信息都直接从Redis中集中获取。

2.5 分布式锁

几乎每个互联网公司中都使用了分布式部署,分布式服务下,就会遇到对同一个资源的并发访问的技术难题,如秒杀、下单减库存等场景。

用synchronize或者reentrantlock本地锁肯定是不行的。

如果是并发量不大话,使用数据库的悲观锁、乐观锁来实现没啥问题。

但是在并发量高的场合中,利用数据库锁来控制资源的并发访问,会影响数据库的性能。

实际上,可以用Redis的setnx来实现分布式的锁。

使用过Redis分布式锁嘛?有哪些注意点呢?

分布式锁,是控制分布式系统不同进程共同访问共享资源的一种锁的实现。秒杀下单、抢红包等等业务场景,都需要用到分布式锁,我们项目中经常使用Redis作为分布式锁。

选了Redis分布式锁的几种实现方法,大家来讨论下,看有没有啥问题哈。

  • 命令setnx + expire分开写
  • setnx + value值是过期时间
  • set的扩展命令(set ex px nx)
  • set ex px nx + 校验唯一随机值,再删除
2.5.1 命令setnx + expire分开写

if(jedis.setnx(key,lock_value) == 1){ //加锁 expire(key,100); //设置过期时间 try { do something //业务请求 }catch(){ } finally { jedis.del(key); //释放锁 } }

如果执行完setnx加锁,正要执行expire设置过期时间时,进程crash掉或者要重启维护了,那这个锁就"长生不老"了,别的线程永远获取不到锁啦,所以分布式锁不能这么实现。

2.5.2 setnx + value值是过期时间

long expires = System.currentTimeMillis() + expireTime; //系统时间+设置的过期时间 String expiresStr = String.valueOf(expires); // 如果当前锁不存在,返回加锁成功 if (jedis.setnx(key, expiresStr) == 1) { return true; } // 如果锁已经存在,获取锁的过期时间 String currentValueStr = jedis.get(key); // 如果获取到的过期时间,小于系统当前时间,表示已经过期 if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间(不了解redis的getSet命令的小伙伴,可以去官网看下哈) String oldValueStr = jedis.getSet(key_resource_id, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才可以加锁 return true; } } //其他情况,均返回加锁失败 return false; }

笔者看过有开发小伙伴是这么实现分布式锁的,但是这种方案也有这些缺点

  • 过期时间是客户端自己生成的,分布式环境下,每个客户端的时间必须同步。
  • 没有保存持有者的唯一标识,可能被别的客户端释放/解锁。
  • 锁过期的时候,并发多个客户端同时请求过来,都执行了jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。
2.5.3:set的扩展命令(set ex px nx)(注意可能存在的问题)

if(jedis.set(key, lock_value, "NX", "EX", 100s) == 1){ //加锁 try { do something //业务处理 }catch(){ } finally { jedis.del(key); //释放锁 } }

这个方案可能存在这样的问题:

锁过期释放了,业务还没执行完。

锁被别的线程误删。

2.5.4 set ex px nx + 校验唯一随机值,再删除

if(jedis.set(key, uni_request_id, "NX", "EX", 100s) == 1){ //加锁 try { do something //业务处理 }catch(){ } finally { //判断是不是当前线程加的锁,是才释放 if (uni_request_id.equals(jedis.get(key))) { jedis.del(key); //释放锁 } } }

在这里,判断当前线程加的锁和释放锁是不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁

一般也是用lua脚本代替。lua脚本如下:

if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end;

这种方式比较不错了,一般情况下,已经可以使用这种实现方式。但是存在锁过期释放了,业务还没执行完的问题 (实际上,估算个业务处理的时间,一般没啥问题了)。

2.6 社交网络

赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适保存 这种类型的数据,Redis提供的数据结构可以相对比较容易地实现这些功能。

2.7 消息队列

消息队列是大型网站必用中间件,如ActiveMQRabbitMQKafka 等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。

2.8 位操作

用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。腾讯10亿用户,要几个毫秒内查询到某个用户是否在线,能怎么做?千万别说给每个用户建立一个key,然后挨个记(你可以算一下需要的内存会很恐怖,而且这种类似的需求很多。这里要用到位操作------使用setbit、getbit、bitcount命令。原理是:redis内构建一个足够长的数组,每个数组元素只能是0和1两个值,然后这个数组的下标index用来表示用户id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标和元素值(0和1)来构建一个记忆系统。

相关推荐
张铁铁是个小胖子44 分钟前
微服务学习
java·学习·微服务
AITIME论道2 小时前
论文解读 | EMNLP2024 一种用于大语言模型版本更新的学习率路径切换训练范式
人工智能·深度学习·学习·机器学习·语言模型
青春男大4 小时前
java栈--数据结构
java·开发语言·数据结构·学习·eclipse
mashagua5 小时前
RPA系列-uipath 学习笔记3
笔记·学习·rpa
沐泽Mu6 小时前
嵌入式学习-QT-Day05
开发语言·c++·qt·学习
锦亦之22336 小时前
cesium入门学习二
学习·html
04Koi.6 小时前
Redis--常用数据结构和编码方式
数据库·redis·缓存
m0_748256146 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
IT古董7 小时前
【机器学习】机器学习的基本分类-半监督学习(Semi-supervised Learning)
学习·机器学习·分类·半监督学习
jbjhzstsl7 小时前
lv_ffmpeg学习及播放rtsp
学习·ffmpeg