黑马点评Redis学习笔记

笔记按照教学视频讲解的顺序,并附上内容所在的视频分p位置

P1 课程介绍

在这里插入图片描述
项目是前后端分离,不是微服务

P3 短信登录-基于session实现短信登录的流程

在这里插入图片描述
在这里插入图片描述
登录校验功能
在这里插入图片描述
在ThreadLocal中存储用户信息
在这里插入图片描述

MyBatisPlus支持对单表的CRUD
例如不用自己去写一条查询语句
在这里插入图片描述
原理是在结果的类中使用@TableName注解已经配置好CRUD的表名称了
在这里插入图片描述

P6 短信登录-实现登录校验拦截器

在这里插入图片描述
主要讲在controller之前加一个拦截器用于一个预先的处理,通过session获取用户信息

P7 短信登录-隐藏用户敏感信息

在这里插入图片描述
这节主要讲User和UserDTO的区别,就是说不要把不用的信息(或敏感的信息)返回给前端网页,这里要进行一个类型转换
这两个对象的属性信息拷贝可以用hutool工具包的BeanUtil.copyProperties()方法

P8 短信登录-session共享的问题分析

在这里插入图片描述
在这里插入图片描述

P9 短信登录-Redis代替session的业务流程

在这里插入图片描述
使用redis的时候这里的key不能像使用session一样都使用code,因为每个session都有一个sessionID可以保证不冲突,
但这里是redis集群,它的key必须唯一,才能保证获取到正确的值
在这里插入图片描述
在这里插入图片描述
所以可以使用手机号作为key(手机号不安全有泄露风险),但推荐使用一个随机字符串作为key
在这里插入图片描述
在redis中存储一个对象可以使用hash结构(相比string结构更好),
hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,而如果使用string的话只能整体删掉重新赋值
在这里插入图片描述

P10 短信登录-基于Redis实现短信登录

在这里插入图片描述
在redis中存储用户的信息的时候(通过token作为key)会设置一个过期时间(比如30分钟),需求是用户在不操作30分钟后过期,而不是一直操作也会过期
方法是,可以在拦截器中刷新key的时间

注意StringRedisTemplate和RedisTemplate的区别,
前者要求存的数据只能是字符串,并且这两个存的数据不能相互查到
在这里插入图片描述
在这里插入图片描述
通过stringRedisTemplate存储String字符串数据、获取数据
stringRedisTemplate.opsForValue().set()
stringRedisTemplate.opsForValue().get()
通过stringRedisTemplate存储Hash数据、获取数据、设置过期时间
stringRedisTemplate.opsForHash().putAll()
tringRedisTemplate.opsForHash().entries()
stringRedisTemplate.expire()
使用hutool的BeanUtil.beanToMap方法将Bean转为Map

P11 短信登录-解决状态登录刷新的问题

在这里插入图片描述
问题:在拦截器中刷新用户的key时间只能针对需要登录的网页(因为只拦截需要登录的网页),我们希望用户在访问不需要的登录的网页的时候也起到一个刷新用户key的效果
解决:在拦截器前再设置一个拦截器,第一个拦截器拦截一切路径并刷新key,第二个拦截器才做真正的拦截
在这里插入图片描述

P12 什么是缓存

在这里插入图片描述
在这里插入图片描述
缓存可以减少后端负载,减少相应时间,但会带来数据一致性成本,也会引入缓存穿透击穿等问题

P13 添加商户缓存

在这里插入图片描述
mybatisplus也提供业务层的接口,可以直接查询

在这里插入图片描述
在这里插入图片描述
在客户端与数据库之间添加缓存
在这里插入图片描述

P14 缓存练习题分析

在这里插入图片描述
用上述类似的方法给商户类型列表添加缓存,其中的逻辑是相同的,不同点在于是对一个List进行Redis的String的存储和读取
利用hutool的JSONUtil工具包调用toList方法进行字符串转对象数组

在这里插入图片描述

P15 缓存更新策略

在这里插入图片描述
在这里插入图片描述
通常来说更常用的是第一种策略,由自己在更新数据库时更新缓存
第二种策略整合的服务非常复杂,第三种策略需要考虑缓存宕机数据丢失的问题
在这里插入图片描述
在这里插入图片描述
先操作缓存还是先操作数据库,都可能导致程安全问题,避免多线程下缓存数据库数据不一致
但先操作数据库再删除缓存的方式更好,发生线程安全问题概率较低
在这里插入图片描述
在这里插入图片描述

P16 实现商铺缓存与数据库的双写一致

在这里插入图片描述
当要更新数据库的时候,先更新数据库,再删除缓存(为了防止删除失败数据库又更新了,加一个Transactional注解来回滚)

P17 缓存穿透的解决思路

在这里插入图片描述
缓存穿透是指用户请求一个并不存在的数据,故意不命中缓存来消耗数据库的资源
第一种解决方法:可以将这些空对象缓存起来,避免相同的空请求重复地打到数据库
缓存空的对象只能解决重复请求同一个空对象的情况,如果每次请求的都不一样会占用大量的缓存空间(比如随机生成一个请求大概率是不存在的),
此时可以用第二种方法,先用过滤器判断再去查redis缓存

第二种方法:在缓存之前加一个布隆过滤器,这个过滤器可以判断请求的东西在数据库中是否存在(利用bit数组实现)
由于布隆过滤器的误判性,它只能确定请求东西在数据库中一定不存在(此时直接拒绝),或者在数据库中大概率存在(先放行但仍小概率不存在),
原理是一个东西如果加入了过滤器中,那么它一定会被过滤到,也有可能没加入过滤器的东西也被过滤了
在这里插入图片描述

P18 编码解决商铺查询的缓存穿透问题

在这里插入图片描述
采用缓存空对象的方法解决的逻辑流程
在这里插入图片描述
除了以上两种解决方法之外,也有一些其他的手段来解决穿透
比如增加id的复杂的避免有规律的自然数,可以设置id的长度等
然后根据自己制定的id规则来做格式校验,可以提前阻止一部分请求
或者要求查询的用户具有某些权限,比如要先登录等,不同权限的用户查询的频率有不同限制等
在这里插入图片描述

P19 缓存雪崩问题及解决思路

在这里插入图片描述
给TTL过期时间添加一个随机值,避免同时失效
采用集群来提高可用性
降级限流意为给某些业务降低服务质量,返回拒绝服务
给请求过程的每个环节添加缓存,可以是nginx、redis、JVM、数据库等
在这里插入图片描述

P20 缓存击穿问题及解决方案

在这里插入图片描述
在这里插入图片描述
两种解决方案的时序图
在这里插入图片描述
互斥锁在重建缓存时其他线程只能等待,此时服务不可用,如果重建缓存的业务中涉及到的缓存也正在发生缓存重建,可能就会发生死锁。
逻辑过期在缓存中加了一个自定义字段表示数据的过期时间(不是真正的过期),实际上设置它永不过期,如果查询到数据逻辑过期了,那么就进行重建,重建期间返回旧的数据,保证了服务的可用性。

这两种方法如何选择,其实是在一致性和可用性直接选择,
分布式系统中也面临一致性和可用性之间的抉择问题,在这里互斥锁选择了一致性,逻辑过期选择了可用性
在这里插入图片描述

P21 利用互斥锁解决缓存击穿问题

在这里插入图片描述
在这里插入图片描述
在某个缓存失效的情况下,有多个线程(比如1000个)发起查询请求,只有第1个线程去重建缓存(去查数据库,只查1次),其它线程等待
互斥锁可以用redis中的setnx锁来实现,这个锁的数据只能在创建的时候赋值(赋给它1),值不能被修改,只能被删除key
在这里插入图片描述
线程等待使用Thread.sleep(),这个方法需要解决异常,往上抛

利用jmeter压力测试工具,发起1000个http请求,
在这里插入图片描述
可以在结果中看到所有请求都成功得到了是数据,也可以在也可以在汇总报告中看到吞吐量

在这里插入图片描述

P22 利用逻辑过期解决缓存击穿问题

在这里插入图片描述
在这里插入图片描述
首先考虑逻辑过期这个字段设置在哪里的问题,因为存储的对象本身是不带这个字段的,可以在对象中添加一个字段,但这会修改原来的代码,不推荐
还有一种做法是使用继承,新定义一个带逻辑过期字段的类,它继承原来的那个类。
推荐的做法是定义一个对象,有两个属性,一个属性存过期时间,另一个属性存Object字段,这样所有的对象都可以进行存储。

通过java.time.LocalDateTime获取时间
LocalDateTime.now()得到当前时间,LocalDateTime.now().plusSeconds(秒数)得到当前时间之后一刻时间

测试步骤:
1、往缓存里面存数据(可以运行单元测试方法),缓存里面一定要有一个数据,不管是新的还是旧的,否则被认为要查询的东西不存在
2、在数据库中修改数据,此时数据库的数据是最新的,缓存里面的数据是旧的
3、通过jmeter在1秒内发起100个请求,可以看到前面部分的请求获取的是旧数据(缓存里面旧的),后面部分的请求得到的是新数据(缓存重建之后的新的)
4、这个过程中只发生了1次缓存重建(比如数据库查询),表明它是线程安全的

P23 封装Redis工具类

在这里插入图片描述
在这里插入图片描述
将这些方法编写到一个工具类中,难点在于:1,类型不确定用泛型,2,方法不确定用函数式编程。这两者都由调用者来确定。
返回的类型不确定,id的类型不确定,利用泛型在类型不确定的情况下指定数据类型,
由使用这个方法的调用者告诉我们类型是什么,例如这里的商品类型Shop
在这里插入图片描述
由于查询数据库的方式是不确定的,让方法的调用者传一个参数告诉方法如何查询数据库,需要一个参数并带有返回值,所以进行函数式编程,
用java里的Function来实现,这里传入了一个函数,命名为dbFallback,参数为ID和返回类型R都是泛型,调用apply来执行传入的函数
在这里插入图片描述
这是调用方法的传入参数,它说明了返回一个Shop商铺类型,查询数据库的方式为getById(),
id2 -> getById(id2) 由于id2相同可写为 this::getById
在这里插入图片描述

P24 缓存总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

-----分割线----以上是基础课程以下是高级实战课程--------

在这里插入图片描述

P25 优惠券秒杀-全局唯一ID

在这里插入图片描述
redis在电商秒杀中扮演重要角色
在某些情况下使用自增ID不好,需要使用全局ID生成器生成唯一ID

在这里插入图片描述
好处有:1,避免ID被猜测到,2,方便分布式分多个表扩充数据(否则ID会重复发生冲突)

在这里插入图片描述
全局ID需要满足这些特性,而Redis正好都符合这些要求,所以可以用Redis来做
对于分布式系统来说,或许有多个数据库,但Redis作为中间件只有1个(或集群),可认为它是唯一的

在这里插入图片描述
采用Java中的Long类型来作为ID,其中拼接了一些其它信息

P26 优惠券秒杀-Redis实现全局唯一ID

在这里插入图片描述
通过redis的自增策略来生成唯一id,使用increment()方法
stringRedisTemplate.opsForValue().increment()
单元测试id生成的结果

在这里插入图片描述
在redis中看到生成了30000个id

在这里插入图片描述
可以看到key就是一天的日期,这里增长到了30000,也表明这一天生成了30000次id,方便做统计

总结唯一id的策略,除了redis自增还有一些其它方法
在这里插入图片描述
但是这里的数据库自增不是指单表的id自增,这种方法不好,
这里的自增是单独建一个表,这个表是自增的,其它多张表在这个表里面去取,这种方法也可行,但比Redis自增的性能略差

P27 优惠券秒杀-添加优惠券

在这里插入图片描述
在黑马点评里的商品就是优惠券
优惠券分为两种:普通优惠券、秒杀优惠券,普通优惠券作为一张表存储,其中秒杀优惠券属于普通优惠券,但有一些额外的信息
这种情况,再建一个表作为扩展字段专用于存储秒杀优惠券,添加秒杀优惠券的时候,两张表都要存储

P28 优惠券秒杀-实现秒杀下单

在这里插入图片描述
在这里插入图片描述
下单的逻辑是2部分,先修改库存,再创建订单

通过mybatisplus修改一条数据,在继承了mybatisplus的实体类对象的接口上使用update方法

在这里插入图片描述
通过mybatisplus来插入一条数据,首先创建一个带有mybatis注解的实体类对象,set好对应的字段,用save方法提交
在这里插入图片描述

P29 优惠券秒杀-库存超卖问题分析

在这里插入图片描述
在上面的逻辑中,用多线程并发进行测试,出现了超卖,库存为负数,订单也多出了相应的数量

在这里插入图片描述
在这里插入图片描述
出售的逻辑过程是先查询值,然后修改值

在这里插入图片描述
乐观锁不会真正的去加锁,只是做一个判断是否修改了数据,由于不加锁因此效率比悲观锁要高

在这里插入图片描述
乐观锁的版本号法,每次修改数据都要把版本号加1,通过

在这里插入图片描述
乐观锁的CAS法(compare and swap),其实和版本号法是一样的,这里用库存来代替了版本号,用库存来判断
但可能有ABA问题,虽然库存值和之前相等,但期间的值可能发生了变化又修改回来了

P30 优惠券秒杀-乐观锁解决超卖

在这里插入图片描述
这里用CAS方法来实现乐观锁,在修改数据的时候where的条件是数据要和之前查到的一样,

在这里插入图片描述
经过一次并发测试,虽然没有发生超卖现象,但出现了剩余库存(200个线程去买100个库存),
引起的原因是乐观锁的成功率低的问题,就算是库存充足只要数字和之前查的不一样它就认为会导致线程安全问题,因此不执行

在这里插入图片描述
因此,对于库存来说,不需要要求前后相等,只需要值大于0即可

在这里插入图片描述
对于乐观锁成功率低的问题,可以用分段加锁的方式解决(每次锁定的资源少),比如这里的100个库存分为10张表,抢的时候同时去抢10张表,可大大提高成功率(减少了触发乐观锁修改了数据就不执行的概率)

P31 优惠券秒杀-实现一人一单功能

在这里插入图片描述
需求是一个用户只能下一单,不允许购买多单

在这里插入图片描述
用悲观锁synchronized来解决
在这里插入图片描述
整体逻辑是,用户先获取锁,然后提交事务,最后释放锁,才能确保线程安全
确保锁释放的时候,数据库一定存在订单,然后同一个用户再去查的时候一定有订单存在
在这里插入图片描述
经过jmeter并发测试,一个用户发起100个线程,最终只有一个订单下单成功

P32 优惠券秒杀-集群下的线程并发安全问题

在这里插入图片描述
在这里插入图片描述
拷贝项目为两份
在这里插入图片描述
设置第二份的端口为8081(第一份为8081)
在这里插入图片描述
前端发起的请求在2个后台之间轮询访问
在这里插入图片描述
在这种情况下2个相同用户id的请求都进入到synchronized语句里面了(应该只能进1个,因为用户id是一样的),
表明出现了后端集群模式的线程安全问题
在这里插入图片描述
在这里插入图片描述
因此后面在查询库存的时候,2台都查到count为0,都会认为用户没有购买过,最终得到一个用户下了2单的错误结果

在这里插入图片描述
在单体后台的时候,锁的原理是JVM内部维护的锁监视器对象,监视器用的是常量池里面的用户id,id相同它们就是同一个锁,监视器也是同一个
在2个后台的时候,它们都有自己的JVM,获取锁的时候分别走各自的监视器

解决办法是,在有多个JVM的时候,需要它们获取的是同一把锁(需要锁能够跨JVM,或者跨进程)

P33 分布式锁-基本原理和不同实现方式对比

在这里插入图片描述
分布式锁是JVM之外的锁,也就是之外的锁监视器,多个进程都可以看见它
在这里插入图片描述
主要特点是多进程可见和互斥
在这里插入图片描述
分布式锁常见的有三种

在这里插入图片描述
Redis可以设置锁超时自动释放,在宕机的情况下也能释放锁

P34 分布式锁-Redis的分布式锁实现思路

在这里插入图片描述
设置锁和设置超时时间这两个操作要保证原子性(防止在设置锁之后就宕机了,导致永远不能释放锁)
在这里插入图片描述
这里设置key为业务名称,value为线程标识

P35 分布式锁-实现Redis分布式锁版本1

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过postman同一个用户来发起测试,由于nginx轮询将请求分别打到2台后端上,可以发现只有一个请求获取到了锁,成功实现
因为2个后台都是从同一个redis获取的锁

在这里插入图片描述
在这里插入图片描述

P36 分布式锁-Redis分布式锁误删问题

在这里插入图片描述
上一节的方案在极端环境下依然存在问题

在这里插入图片描述
线程在获取锁之后业务阻塞的时间比锁超时的时间还长,当线程恢复后就会把期间别人获取的锁释放掉(以此类推),导致线程安全问题
因此在释放锁的时候要判断锁标识(线程id)是否一致
在这里插入图片描述

P37 分布式锁-解决Redis分布式锁误删问题

在这里插入图片描述
在这里插入图片描述
在JVM集群场景下,只靠线程id来区分是不够的(因为不同的JVM可能会产生相同的线程id),
采用UUID和线程id结合的方法(用UUID来区分JVM,用线程ID来区分不同的线程)
在这里插入图片描述
用UUID拼接上线程id来作为线程标识,删除锁时通过判断线程标识解决锁的误删问题
经过postman发起的两个请求,在调试到断点之后,删除redis的锁,线程不会去删除不属于自己的锁

P38 分布式锁-分布式锁的原子性问题

在这里插入图片描述
在这里插入图片描述
在上一节的例子中,还有可能出现极端情况,也会导致线程安全问题,
虽然它在删除锁之前判断了锁是不是自己的(用于不删除别人的锁),但是由于判断锁和删除锁是两个操作,
期间如果线程阻塞(或许是Full GC导致的)并且锁超时释放了,此时若有其它线程进来,会获得锁,然后原有的线程释放了不属于自己的锁

引起的原因是,这两个动作不是原子性的,因此,必须确保判断锁和删除锁是原子操作

P39 分布式锁-Lua脚本解决多条命令原子性问题

在这里插入图片描述
由于判断锁和删除锁是两个操作,两条命令,
通过redis提供的Lua脚本(撸啊),将多条redis命令编写在脚本中,实现多条命令的原子性

在这里插入图片描述
在这里插入图片描述
使用redis的EVAL命令执行脚本(格式为字符串),末尾0表示0个参数

在这里插入图片描述
如果有多个参数会放入KEYS数组或者ARGV数组
redis的参数分为两种:key类型和其它类型,这里的参数个数是指key类型的参数个数
在这里插入图片描述

P40 分布式锁-Java调用Lua脚本改造分布式锁

在这里插入图片描述
在这里插入图片描述在resource中创建lua脚本
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
通过lua脚本的方式编写好代码之后进行测试,测试步骤:
1、用postman发起两次请求,分别打到2台不同的后台上
2、在第一个线程获取到锁之后,去redis里面把锁删掉(模拟超时释放)
3、由于没有锁了因此第二个线程也获取到了锁,在redis里面可以看到锁的值变了,此时是第二个线程拿到了锁
4、让第一个线程继续运行执行unlock方法,可以看到锁并没由被删除,因为它通过执行lua脚本发现锁不是自己的,不执行删除命令
5、让第二个线程继续运行执行unlock方法,锁被删除了,执行lua脚本发现锁是自己的,删除锁
结论:通过StringRedisTamplate执行lua脚本,将判断锁和删除锁两个操作放在一起,保证了原子性,保证线程只会删除自己的锁

在这里插入图片描述
以上的方案已经是一种相对完善的锁了,在生产中也有使用,但仍有改进空间

P41 分布式锁-Redisson功能介绍

在这里插入图片描述
在上一节的实现的锁,方法中还存在一些问题:
1、锁不可重入。一个线程在一个方法内获取了锁之后,这个线程就不能再去获取锁了,或者还有,这个方法调用的方法都不可以再调用这个加锁的方法了(死锁了),造成的原因是同一个线程名称来标识锁的,此时锁还没有释放。
2、锁不可重试。在获取不到锁时应该再等一等,而不是直接返回失败

在这里插入图片描述
可使用现成的开源框架Redisson(读作“瑞迪森”)来实现各种分布式锁
在这里插入图片描述

P42 分布式锁-Redisson快速入门

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
使用Redisson分布式锁来代替redisd的锁,经过测试,功能正常,无并发安全问题,订单创建正常

P43 分布式锁-Redisson的可重入锁原理

在这里插入图片描述
重入锁的原理是,创建锁的时候,不仅会记录线程标识,还会记录锁的重入次数(这两个参数用hashmap来存储,不再用以前的String结构)
在这里插入图片描述
获取锁的时候,判断如果线程标识相同,此时重入次数加1,释放锁的时候,重入次数减1(并且判断如果为0,删除锁)

由于其中的逻辑复杂,判断锁和释放锁等操作要保证原子性,因此锁的逻辑写在lua脚本中

在这里插入图片描述
在这里插入图片描述
单元测试锁的重入性
在这里插入图片描述
当第二个线程获取到锁的时候,可以看到值增加了

在这里插入图片描述

P44 分布式锁-Redisson的锁重试和WatchDog机制

在这里插入图片描述
通过源码了解Redisson锁的原理
在这里插入图片描述
首先执行lua脚本,它返回一个锁的剩余有效期(ttl)或者null,返回了null表示获取锁成功了。
然后判断leaseTime(默认为-1,作为开启看门狗的条件,看门狗会不停地更新有效期),如果用户提供了其它的数值,则不会开启看门狗机制。

也就是总的流程就是,首先获取到锁(这个锁30s后自动释放),然后对锁设置一个延迟任务(10s后执行),延迟任务给锁的释放时间刷新为30s,并且还为锁再设置一个相同的延迟任务(10s后执行),这样就达到了如果一直不释放锁(程序没有执行完)的话,看门狗机制会每10s将锁的自动释放时间刷新为30s。
而当程序出现异常,那么看门狗机制就不会继续递归调用renewExpiration,这样锁会在30s后自动释放。

leaseTime(锁自动释放时间)
在这里插入图片描述
通过hash结构记录重入次数实现锁的可重入性,
通过发布和订阅的方式在获取锁失败时可再次尝试获取,实现锁的可重试性
通过看门狗机制重置超时时间

P45 分布式锁-Redisson的multiLock原理

在这里插入图片描述
这节分析redisson是如何解决第4个问题,主从一致性问题的
redis主节点(master)进行写操作,从节点(slave)进行读操作
主节点会把数据同步给从节点

如果数据写入主节点后,还未来得及写入从节点,此时主节点宕机了,去从节点读数据发现没有锁,会产生锁失效问题
主从关系是导致一致性问题发生的原因

Redisson解决的方法是不采用这种主从关系,每个节点都可以去做读写操作,
Redisson在获取锁的时候会去向每个节点发送获取锁,只有每个节点都拿到锁才算成功(只要有一个没拿到算不成功),
因此,当宕机发生时(某个节点宕机),不会获取到锁,不会产生锁失效的问题
多个节点也保证了可用性,并且每个节点后面也可以做主从同步

在这里插入图片描述
这个锁的名称叫联锁(multiLock,意为把多个独立的锁合在一起变成联锁)这个方案的好处:
1、保留了主从一致机制,高可用性
2、避免了主从一致引发的锁失效问题

联锁(RedissonMultiLock)对象可以将多个RLock对象关联为一个联锁,实现加锁和解锁功能

测试multiLock联锁
在这里插入图片描述
在测试锁的重入次数的时候,3个节点的会同时创建一个锁,并且value值会一起增加和减少

在这里插入图片描述
1、简单的利用setnx实现的锁,由于没有记录重入次数,无法实现重入性
2、利用hash结构记录实现了锁的重入性,但在主从模式下,redis宕机会导致锁失效(一个节点拿不到锁就认为没有锁了)
3、多个节点组成联锁,避免主从一致性问题(必须所有节点拿到锁,才算拿到锁)

思考:
联锁是如何解决宕机引起的锁失效问题的?

P46 秒杀优化-异步秒杀思路

在这里插入图片描述
回顾一下秒杀的业务,必须做到:
1,库存不能出现超卖
2,一个用户只能下一单
业务的流程具体为:
1,库存是否充足,用户是否买过(用户能不能买,购买资格判断)
2,扣减库存,创建订单

对之前的这个业务进行测试:
准备1000个token(已经登录了,存在redis里面),设置库存为200个,通过jmeter发起请求
在http信心头管理器中设置authorization的值为${token}

在这里插入图片描述
并指定保存token的文件,以作为请求发起的token
在这里插入图片描述
在这里插入图片描述
测试结束查看聚合报告
在这里插入图片描述
结果显示1000个请求的平均相应时间为148毫秒(教学视频中为500毫秒),吞吐量为987个请求每秒,异常值应该为80%(可能是jmeter配置问题,确实有200个请求成功了,但仍显示异常)
测试的结论是,响应时间较长,接口还可以进行优化

在这里插入图片描述
通过分析业务流程,造成慢的原因是:
业务中的逻辑是串行执行的,并且写在了事务中,再加上mysql的并发能力较弱,多种原因的结果影响了性能

解决方法是把这部分逻辑分成2个线程执行,
主线程用于判断用户购买资格,然后开启一个独立线程进行减库存和下单操作,
其中秒杀资格(购买资格)的判断可以在redis里面去做(因为比mysql性能要好),资格判断包括判断库存和一人一单,
而后面的下单操作,它的时效性要求并不高,可以在之后再写入数据库,对于性能来说它不是整个秒杀的关键核心步骤

判断用户具有购买资格之后,生成订单id,并放入阻塞队列里面去,此时订单并没有真正创建,并保证之后一定会创建

在这里插入图片描述
然后就是redis如何实现购买资格判断的问题,
1,对于判断库存,首先是库存充足,然后要进行库存减1,
2,对于一人一单,需要存储当前商品被哪些用户购买过,用于进行判断
一人一单可以用redis中的set集合,它的特性是元素不重复,符合业务的需求
在这里插入图片描述

P47 秒杀优化-基于Redis完成秒杀资格判断

在这里插入图片描述
因为要在redis里面做秒杀资格判断,首先要把库存信息存入redis中,用String结构
然后运行lua脚本,扣减库存,并保存下单用户信息(用于之后写入)
在这里插入图片描述
在这里插入图片描述
jmeter并发测试的结果,接口平均相应时间大大缩短了(上次148,本次58),但吞吐量不知道什么原因导致没有提升
(本次第一次测试的结果1400毫秒可能不准确,可能是由于这些服务部署在本机一台机器上造成的)
在这里插入图片描述

P48 秒杀优化-基于阻塞队列实现秒杀异步下单

在这里插入图片描述
阻塞队列:如果队列里面没有元素这个线程会被阻塞,直到队列里有元素时才会被唤醒
将下单信息存入阻塞队列里面之后,接下来就是从队列里面取出订单完成下单,
此时需要准备,线程池和线程任务
在这里插入图片描述
之前的内容中通过代理对象来执行方法的事务功能,关于代理对象的获取方式,这里要对之前的代码进行修改:
由于这里是异步的,即主线程通过执行lua脚本判断资格,把下单信息放入队列,而子线程从队列中取出信息,通过代理对象调用事务方法完成下单
问题是,子线程通过之前的方法AopContext.currentProxy()是获取不到代理对象的,因为它不是同一个线程(它不是父线程)
这里的解决方法是,定义一个类成员变量,在执行主线程的时候初始化赋值,那么在执行子线程的时候就可以直接使用了

功能结果:
用户下单完成后,redis和mysql的值都完成了更新,其中mysql的更新是异步的
redis记录了库存的剩余数量,以及下单的用户id,
mysql更新了库存表和订单表

Jmeter多用户发起请求测试结果(1000个线程每个线程一个用户,200个库存)
第一次测的结果
在这里插入图片描述
第二次测的结果(与教学视频中的值相近,为什么第二次测的与第一次差距很大?)
在这里插入图片描述
存在的问题是使用了阻塞队列,这个队列会占用jvm的空间,可能由于内存溢出导致问题
阻塞队列在jvm内存中,服务宕机(或者服务重启)会导致数据丢失,发生与数据库的数据不一致的情况
在这里插入图片描述

P49 Redis消息队列-认识消息队列

在这里插入图片描述
由于之前的阻塞队列在使用中存在问题,最好使用消息队列,因为它独立于jvm之外,
更重要的是消息队列负责管理消息,比如用持久化保证数据安全等
在这里插入图片描述
redis也提供了实现消息队列的方式

在这里插入图片描述

P50 Redis消息队列-基于List实现消息队列

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
缺点是消息丢失问题,取走消息后,还没来得及处理,此时消费者宕机,但消息取出就已经找不到了
支持单消费者意味者,消息被一个消费者取走后,其它消费者就拿不到了

P51 Redis消息队列-PubSub实现消息队列

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
消费者退出后,消息就收不到了,返回收到的消费者数量为0
在这里插入图片描述
PubSub的消息队列不支持持久化,它不像List实现的消息队列一样属于redis的数据存储格式,而redis的存储格式是可以做持久化的
消息发送之后如果没有人接收消息会丢失
消息会缓存在消费者中,如果消费者的缓存达到上限会造成数据丢失

P52 Redis消息队列-Stream的单消费模式

在这里插入图片描述
Stream是一种数据类型,因此它支持持久化
在这里插入图片描述
读取消息为空时,如果设置了阻塞就会等待,如果没设置阻塞就会返回空
在这里插入图片描述
在开发时将阻塞队列写在死循环内,实现持续监听的结果
在这里插入图片描述
0表示接收从头开始的消息(从0这个id之后的消息)
$美元符号为接收最新消息
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

P53 Redis消息队列-Stream的消费者组模式

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
接下来进行测试
在这里插入图片描述
现在队列里面有6条信息
在这里插入图片描述
首先,为s1消息队列创建消费者组g1
然后,在g1消费者组中通过消费者c1阻塞等待2000毫秒查询1条消息在s1队列里面去取,
使用大于符号(>)取出未消费的消息,同一个消费者组是一个争抢的关系(别人消费了就没有了,加快消费速度),
消息消费后放入pending-list数组,之后消息被确认后,从数组中移除,
使用符号0取出数组中取出消息(已消费但未确认),可以使用xpending命令查看数组中未确认的消息

java代码中使用消费者组监听消息
在这里插入图片描述
取出的消息如果在处理消费时发生异常,意味着消息没有被确认(正常处理消息成功后的逻辑里面,会进行确认)
catch捕捉异常之后,从pending列表里面取出这条异常的消息,再次进行处理,如果又发生了异常,重复进行直到能够正常处理为止
在这里插入图片描述
在这里插入图片描述
Stream比前两者方法更好,但在业务庞大时可以使用更专业的消息队列,比如RocketMQ、RabbitMQ
Stream消息队列的持久化依赖于Redis的持久化,而它的持久化也存在一定风险
Stream只支持消费者的确认机制,不支持生产者的确认机制(生产者发送消息丢失会引起问题)
Stream不支持事务(多消费者下的消息有序性)

P54 Redis消息队列-基于Stream消息队列实现异步秒杀

在这里插入图片描述
在这里插入图片描述
lua脚本判断秒杀资格,然后往消息队列里面添加消息,
在java中设置一个线程任务,通过两个while死循环,分别不断地处理未处理的消息和pending列表里的消息

在这里插入图片描述
创建Stream消息队列

测试结果
在这里插入图片描述
第二次测试
在这里插入图片描述
教学视频中的平均值为110

P55 达人探店-发布探店笔记

在这里插入图片描述
在这里插入图片描述
探店笔记包括图片和文字等,图片上传到nginx的imgs文件夹里

在这里插入图片描述
在这里插入图片描述
图片上传成功后返回图片的保存地址到前端,文字填写完毕后最后点击发布

P56 达人探店-查看探店笔记

在这里插入图片描述
实现查看探店笔记的功能
在这里插入图片描述
点击探店笔记后显示笔记详情

P55 达人探店-点赞功能

在这里插入图片描述
一个用户对一篇探店笔记只能点一次赞,点过了就高亮,再次点击取消点赞
在这里插入图片描述
由于Blog类中的islike字段并不是数据库表中的一个字段,因此这里要加上一个注解(因为使用了mybatisplus)
在这里插入图片描述
实现类中的逻辑,利用了redis中的set集合,用于记录当前blog被哪些用户点了赞
在这里插入图片描述

P58 达人探店-点赞排行榜

在这里插入图片描述
展示给当前笔记点赞的用户信息(只展示前几名)

在这里插入图片描述这里不能用set了,有三个需求(能存多个元素、能排序、要求唯一)
在这里插入图片描述
Sorted_set又叫zset
在这里插入图片描述
在这里插入图片描述
实现点赞排行榜按时间排序,就把zset的score填入一个时间戳,按照score排序就是按照时间排序

在这里插入图片描述取出前5条(0到4)key的值(按score序,就是时间序),用户id集合

在这里插入图片描述
拿到ids到数据库查询,但是查询的用户顺序并没有按照给定的ids顺序来输出
在这里插入图片描述在sql脚本中,where in中的in不会按照给定的顺序来输出结果(比如这里的id顺序),需要加上order by field来指定输出结果的id顺序

修改后的代码,加上in,并使用mybatis提供的last手写一段代码实现按顺序输出查询结果
在这里插入图片描述

结果测试,点赞的先后顺序正常显示
在这里插入图片描述

P59 好友关注-关注和取关

在这里插入图片描述
在这里插入图片描述
用户的关注关系是多对多的关系(一个被关注的用户对多个用户,又有多个被关注的用户),因此需要一张中间表来记录两个用户的关系

在这里插入图片描述
数据库tb_follow表一条数据记录用户id和关注用户id

在这里插入图片描述
在这里插入图片描述
使用mybatisplus删除表中的一行记录

P60 好友关注-共同关注

在这里插入图片描述
在这里插入图片描述
点击头像进入用户页面,显示用户信息和用户的博客信息
在这里插入图片描述
共同关注,当前登录用户和浏览的用户,显示他们的共同关注的好友,
即找到这两个用户的关注列表的交集,可以用redis中的set结构来实现,它支持找两个set的交集

在这里插入图片描述
要获取当前用户关注的用户列表,从redis中获取,
因此需要修改之前的关注接口,在关注的时候把关注的用户存入redis

在这里插入图片描述
redis里面存好信息后,就可以通过求交集的方式取出来了

测试结果,成功显示两个用户的共同关注
在这里插入图片描述

P61 好友关注-Feed流实现方案分析

在这里插入图片描述
关注推送(也叫Feed流),用户发送博客时推送给所有关注了此用户的人
在这里插入图片描述
Feed流的2种模式:
第一种是时间线排序(Timeline),第二种是智能模式,根据用户喜好排序
在这里插入图片描述
在这里插入图片描述
拉模式(读扩散),粉丝去关注人的发件箱拉去消息,每个用户都对应一个发件箱,拉取后并按时间戳排序,缺点是拉取耗时
在这里插入图片描述
推模式(写扩散),博主直接将消息发送到每个粉丝的收件箱,每个粉丝都对应一个收件箱,优点是可以直接读取省时间,缺点是消息保存了多个副本

在这里插入图片描述
推拉结合的模式(读写混合),由于单独的推模式或者拉模式都存在缺点,这种模式根据用户特点进行了区分,
对于普通博主来说,由于他们粉丝较少,可以直接把消息推送给粉丝,不会保存大量相同的内容造成空间浪费,采用推模式,
对于人气博主来说,他们的活跃粉丝会频繁浏览消息,采用推模式以节省时间,而普通粉丝采用拉模式,牺牲一定拉取时间以节省空间
在这里插入图片描述
总体来说推模式较好

P62 好友关注-推送到粉丝收件箱

在这里插入图片描述
在这里插入图片描述
redis能实现排序的数据结构有2个:list(链表有插入顺序)和sorted_list(按score排序)
实现分页查询要对数据结构指定一个范围值参数,list具有脚标便于找到一个范围内数据,
sorted_list没有脚标,但有排名,也可以实现找一个范围的数据,
在这里插入图片描述
对Feed流来说,由于数据是不断变换的,传统的分页会造成数据读取重复(比如上面的6号,已经在第一页读取过了)

在这里插入图片描述
实现滚动分页,需要记录每次分页的最后一个数据(第一次分页设置为无穷大),避免数据读取重复问题
而list不支持这种脚标的变化,因此选择sorted_list来实现

测试,用户发一条博客并查看粉丝收件箱的情况
在这里插入图片描述
在这里插入图片描述
比如id为1的用户关注了id为5的用户,用户(id为5)发布了一条新的博客,粉丝用户(id为1)的收件箱会存入这个博客的id,score为时间戳

P63 好友关注-滚动分页查询收件箱的思路

在这里插入图片描述
先测试一下redis的命令

在这里插入图片描述
有一个名为z1的zset

在这里插入图片描述
按照score降序(反转REV)查询ZSET(Z)中一个0到2(也就是前3个)范围(RANGE)的元素并带上分数(WITHSCORES)输出结果,
在插入一条数据m7之后,如果继续查第二页(3到5),新插入的数据会导致查询重复

在这里插入图片描述
使用ZREVRANGEBYSCORE命令传入4个参数来实现滚动分页效果,参数分别是MAX, MIN, LIMIT, COUNT
其中min(最小值)和count(查询条数)参数不变,而max(最大值应设为上一次查询的最小值,第一次查设为最大值)
和offset(表示从第几个开始查起)根据需要变化
在这里插入图片描述
假设现在z1有两个值的score是相同的

在这里插入图片描述
score相同的情况(时间戳可能相同),如果有元素score相同offset的值就不能为1,否则会查询重复,
offset的值应该是上一次查询的与最小元素相同的个数(2个相同,偏移量就跳过2个),避免重复查询,实现滚动分页

在这里插入图片描述
接口参数和返回值

P64 好友关注-实现滚动分页查询

在这里插入图片描述
在这里插入图片描述
功能测试
在这里插入图片描述
id为1的用户的feed流,value为博客的id,score为博客发布的时间戳
在这里插入图片描述
设置分页大小为2,滚动条往上会请求查询第一页,滚动条往下超出第一页后会请求查询第二页
在这里插入图片描述
可以看到第一次请求除了返回查到的博客,还给前端返回了minTime(最小时间)和offset(偏移量)参数,用于记录当前分页信息

在这里插入图片描述
前端通过保存的当前分页信息,在第二次请求时带上这些信息,得以获知第二页从哪里开始

P65 附近商铺-GEO数据结构的基本用法

在这里插入图片描述
实现查找附近商铺可以用redis里面的GEO数据结构,它专用于表示地理坐标

在这里插入图片描述
在这里插入图片描述练习GEO的用法
首先添加数据
在这里插入图片描述
在这里插入图片描述

在redis里面可以看到g1实际上是zset(sorted_set),value是坐标的名称,经纬度被转成一个数字保存在score中
在这里插入图片描述
计算北京西站到北京站的距离
在这里插入图片描述
搜索天安门附近的火车站
在这里插入图片描述
查找北京站的经纬度坐标,查看北京站坐标的hash字符串

P66 附近商铺-导入店铺数据到GEO

在这里插入图片描述
接口信息
在这里插入图片描述
要实现的功能是,比如用户点击美食,显示与美食相关的店铺,查询接口需要传入的参数有:店铺类型,页码(这里是传统的分页查询),用户位置的经纬度(在此教学中这里是固定的)
在这里插入图片描述
查看商铺表,里面存储了店铺类型和xy经纬度坐标,
需要把这些店铺信息存入到redis中,方便之后利用geo数据结构来计算位置

为了便于以后查找,在存入redis的时候就做好店铺的分类,类型相同的店铺设置为相同的key(把店铺类型作为key)

在这里插入图片描述
在单元测试中写一个方法,将数据存入redis
1、首先,查到数据后,使用stream流根据店铺id的类型来进行分类,并放入不同的map中保存(map中Long为类型id,List为一条结果)
2、遍历map中的若干个类型(遍历次数为商铺的类型数量),对于同一类型中的数据,使用相同的key存入到redis中
3、由于redis的geo表示的经纬度是score中的一串数字(不是直观的经纬度值),每条数据库中的经纬度需要逐个转成Point类型来存入(因为geo的add方法接收一个RedisGeoCommands. GeoLocation的类型)

在redis中查看保存的店铺信息,类型相同的店铺为同一个key
在这里插入图片描述

P67 附近商铺-实现附近商户功能

在这里插入图片描述
在这里插入图片描述
由于GEOSERCH命令需要redis 6.2版本,而老版本的springboot版本不支持,把它提供的redis排除掉,自己重新引入需要用到的依赖

在这里插入图片描述
1,首先是计算分页参数,由于这里不是滚动分页,计算比较简单(以每页5条为例,第1页是0到5,第2页是5到10)
2,去redis中查询得到第1条到end条的数据(由于这个limit只接收1个参数,不能传一个范围值,之后需要将前面不需要的数据截取掉)
3,解析id以得到真正的集合,用于之后截取
4,用stream流来跳过前面from条数据(截取数据),然后用2个变量来收集,ids收集店铺的id并保存顺序,再用一个Map收集id和距离的对应关系
5,用变量ids来得到查询出具有指定顺序的结果,用变量Map中的数据来设置shop中的字段(Shop中有一个非数据库字段表示距离,方便设置好距离后返回给前端)
5,返回对象集合作为结果

结果测试
在这里插入图片描述
可以看到查询出的店铺按距离从近到远排序,每次请求查询5条数据

P68 用户签到-BitMap功能演示

在这里插入图片描述
在这里插入图片描述
如果设置表的字段来保存签到的时间会造成极大的空间浪费

在这里插入图片描述
如果用一个比特数组(位图)来存储,可以提高空间的利用率,减少空间占用
在这里插入图片描述
在这里插入图片描述
设置一个名为bm1的位图,向指定位置存入信息

在这里插入图片描述
此时用二进制格式查看键的内容

在这里插入图片描述
在这里插入图片描述
在8位置上(第9个)设置值为1,此时长度变为2,可以知道它1个长度存储8位
在这里插入图片描述
1、getbit获取2位置上的值
2、bitcount统计1的个数
3、用bitfield获取多个值,比如无符号(u)从0位置开始,获取之后2个数字,返回成十进制格式,即找到数字11的十进制为3
3、用bitpos找到第1个出现0的位置为3位置

P69 用户签到-实现签到功能

在这里插入图片描述
在这里插入图片描述
签到功能的接口不需要从外部接收参数,它需要的用户信息和当天日期信息可以在后台获取

在redis中的bitmap,将用户id和年月设置为key,而value就是长度为32的比特数组(用于记录一个月的签到情况)

通过postman测试接口
在这里插入图片描述
返回成功后,在redis里面可以看到字符串中写入了一个1,当前是第几天就会在第几个位置上写入值1
在这里插入图片描述

P70 用户签到-统计连续签到

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
统计连续签到次数就是从今天开始向之前每一天遍历,直到遇到没签到的日期为止,通过位运算实现,
由于bitfield拿到的是一个范围日期的签到情况并且用十进制表示,把这个数字不断的右移并和1做与运算,就可以遍历每一天的值
在这里插入图片描述
功能测试结果正确

P71 UV统计-HyperLogLog的用法

在这里插入图片描述
在这里插入图片描述
HyperLogLog用于统计网页的访问量,如果用常规的方式保存所有数据会导致空间占用非常大

在这里插入图片描述

它属于一种概率算法,以较小的误差率代价来节省大量的空间占用

在这里插入图片描述
例如通过pfadd加人5个元素,经过pfcount统计后得出结果是5,当元素个数非常多的时候,它统计的结果会产生偏差
pfcount只统计不重复的元素,可以看到元素重复加入后,统计的数量不会增加

P72 UV统计-测试百万数据的统计

在这里插入图片描述
通过插入大量数据来观察内存的空间占用情况

在这里插入图片描述
往Redis里面写入一百万条数据,
这里分一千次写入,每次写入一千条,好处是每次分配的字符串空间少,批量写入减少Redis的写入代价

得到的统计结果并不是一百万,而是997593,结果相差了约0.24%,误差非常小
在这里插入图片描述
HyperLogLog基于String,空间占用12KB,其中的数据是HyperLogLog算法生成的

在这里插入图片描述
测试前(未插入数据)的内存占用情况

在这里插入图片描述
测试后(已插入数据)的内存占用情况,增加了12,387(约12.09KB)

本文完结

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这篇笔记是关于黑马点评项目中使用Redis学习笔记笔记中的图片来源于黑马ppt,并提供了联系方式,如果有侵权问题可以联系删除。笔记内容包括了Redis的安装配置以及一些相关的知识点。需要注意的是,笔记中的配置是按照黑马2022的Redis进行的,仅供学习参考,并可以自由转载。另外,作者使用的是云服务器,所以IP配置不是127.0.0.1,大家需要根据自己的实际情况进行配置。在笔记中还对一些知识进行了补充,例如设置RedisSerializer来解决乱码问题。此外,笔记还提到了Redis的5种常见数据结构,包括String、List、Set、Hash和ZSet。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [redis项目-黑马点评 项目笔记](https://blog.csdn.net/qq_48617775/article/details/127497077)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Redis黑马2022笔记(基础篇)](https://blog.csdn.net/m0_56079407/article/details/123453958)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值