黑马商城学习笔记(一)

黑马商城学习笔记(一)
Jie一. 开篇导读
1. 短信登录
这一块我们会使用redis共享session来实现
2. 商户查询缓存
通过本章节,我们会理解缓存击穿,缓存穿透,缓存雪崩等问题,让小伙伴的对于这些概念的理解不仅仅是停留在概念上,更是能在代码中看到对应的内容
3. 优惠卷秒杀
通过本章节,我们可以学会Redis的计数器功能, 结合Lua完成高性能的redis操作,同时学会Redis分布式锁的原理,包括Redis的三种消息队列
4. 附近的商户
我们利用Redis的GEOHash来完成对于地理坐标的操作
5. UV统计
主要是使用Redis来完成统计功能
6. 用户签到
使用Redis的BitMap数据统计功能
7. 好友关注
基于Set集合的关注、取消关注,共同关注等等功能,这一块知识咱们之前就讲过,这次我们在项目中来使用一下
8. 打人探店
基于List来完成点赞列表的操作,同时基于SortedSet来完成点赞的排行榜功能
以上这些内容咱们统统都会给小伙伴们讲解清楚,让大家充分理解如何使用Redis
9. 课程总结
二. 短信登录
1.导入SQL hmdp.sql
a.tb.user
用户表
1 | -- auto-generated definition |
b.tb_user_info
用户详情表
1 | -- auto-generated definition |
c.tb_shop
商户信息表
1 | -- auto-generated definition |
d.tb_shop_type
商户类型表
1 | -- auto-generated definition |
e.tb_blog
用户日记表(达人探店日记)
1 | -- auto-generated definition |
f.tb_follow
用户关注表
1 | -- auto-generated definition |
g.tb_voucher
优惠卷表
1 | -- auto-generated definition |
h.tb_voucher_order
优惠卷的订单表
1 | -- auto-generated definition |
2.当前模型
手机或者app端发起请求,请求我们的nginx服务器,nginx基于七层模型走是HTTP协议,可以实现基于Lua直接绕开tomcat访问redis,也可以作为静态资源服务器,轻松扛下上万并发, 负载均衡到下游tomcat服务器,打散流量,我们都知道一台4核8G的tomcat,在优化和处理简单业务的加持下,大不了就处理1000左右的并发, 经过nginx的负载均衡分流后,利用集群支撑起整个项目,同时nginx在部署了前端项目后,更是可以做到动静分离,进一步降低tomcat服务的压力,这些功能都得靠nginx起作用,所以nginx是整个项目中重要的一环。
在tomcat支撑起并发流量后,我们如果让tomcat直接去访问Mysql,根据经验Mysql企业级服务器只要上点并发,一般是16或32 核心cpu,32 或64G内存,像企业级mysql加上固态硬盘能够支撑的并发,大概就是4000起~7000左右,上万并发, 瞬间就会让Mysql服务器的cpu,硬盘全部打满,容易崩溃,所以我们在高并发场景下,会选择使用mysql集群,同时为了进一步降低Mysql的压力,同时增加访问的性能,我们也会加入Redis,同时使用Redis集群使得Redis对外提供更好的服务。
3.打开前后端工程项目
后端加载依赖后访问: http://localhost:8080
4.基于Session实现登录流程
a.发送验证码:
用户在提交手机号后,会校验手机号是否合法,如果不合法,则要求用户重新输入手机号
如果手机号合法,后台此时生成对应的验证码,同时将验证码进行保存,然后再通过短信的方式将验证码发送给用户
b.短信验证码登录、注册:
用户将验证码和手机号进行输入,后台从session中拿到当前验证码,然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账号信息,保存到数据库,无论是否存在,都会将用户信息保存到session中,方便后续获得当前登录信息
c.校验登录状态:
用户在请求时候,会从cookie中携带者JsessionId到后台,后台通过JsessionId从session中拿到用户信息,如果没有session信息,则进行拦截,如果有session信息,则将用户信息保存到threadLocal中,并且放行
d.代码
- 发送验证码:
1 |
|
- 登录
1 |
|
tomcat的运行原理
当用户发起请求时,会访问我们像tomcat注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat也不例外,当监听线程知道用户想要和tomcat连接连接时,那会由监听线程创建socket连接,socket都是成对出现的,用户通过socket像互相传递数据,当tomcat端的socket接受到数据后,此时监听线程会从tomcat的线程池中取出一个线程执行用户请求,在我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller,service,dao中,并且访问对应的DB,在用户执行完请求后,再统一返回,再找到tomcat端的socket,再将数据写回到用户端的socket,完成请求和响应
通过以上讲解,我们可以得知 每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据
如果小伙伴们看过threadLocal
的源码,你会发现在threadLocal
中,无论是他的put
方法和他的get
方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离
e.拦截器
1 | public class LoginInterceptor implements HandlerInterceptor { |
配置拦截器,使之生效
1 |
|
f.隐藏用户敏感信息
通过直接映射数据库对应表的实体类,里面可能会包含很多重要信息,或者不想要暴露一些字段,再或者我想最后要传进来一个多个表的数据,那么我们就可以添加一个新的DTO类, 用来存储这个不是敏感的数据
g.session共享问题
每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了
但是这种方案具有两个大问题
每台服务器中都有完整的一份session数据,服务器压力过大。
session拷贝数据时,可能会出现延迟
所以咱们后来采用的方案都是基于redis来完成,我们把session换成redis,redis数据本身就是共享的,就可以避免session共享的问题了
5.基于Redis实现登录流程
a.设计Key的结构
b.设计key的具体细节
所以我们可以使用String结构,就是一个简单的key,value键值对的方式,但是关于key的处理,session他是每个用户都有自己的session,但是redis的key是共享的,咱们就不能使用code了
在设计这个key的时候,我们之前讲过需要满足两点
key要具有
唯一性
key要
方便携带
如果我们采用phone:手机号这个的数据来存储当然是可以的,但是如果把这样的敏感数据存储到redis中并且从页面中带过来毕竟不太合适,所以我们在后台生成一个随机串token,然后让前端带来这个token就能完成我们的整体逻辑了
c.整体访问流程
当注册完成后,用户去登录会去校验用户提交的手机号和验证码,是否一致,如果一致,则根据手机号查询用户信息,不存在则新建,最后将用户数据保存到redis,并且生成token
作为redis的key,当我们校验用户是否登录时,会去携带着token进行访问,从redis
中取出token
对应的value
,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal
中,并且放行。
1 |
|
d. 解决状态登录刷新问题
1.初始方案
在这个方案中,他确实可以使用对应路径的拦截,同时刷新登录token令牌的存活时间,但是现在这个拦截器他只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,那么这个拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,所以这个方案他是存在问题的
2.优化方案
既然之前的拦截器无法对不需要拦截的路径生效,那么我们可以添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。
3.代码
- RefreshTokenInterceptor
1 | public class RefreshTokenInterceptor implements HandlerInterceptor { |
- LoginInterceptor
1 | public class LoginInterceptor implements HandlerInterceptor { |
三. 错误总结
在定义StringRedisTemplate的时候定义为了
1 |
|
结果在运行的时候报初始化失败异常
1 | org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'mvcConfig': Injection of resource dependencies failed; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'redisTemplate' is expected to be of type 'org.springframework.data.redis.core.StringRedisTemplate' but was actually of type 'org.springframework.data.redis.core.RedisTemplate' |
这是因为Spring
在创建mvcConfig
这个 Bean
时,尝试注入 StringRedisTemplate
类型的 redisTemplate
,但实际找到的 Bean
类型却是 RedisTemplate
,从而引发了类型不匹配的错误。
定义为
1 |
|
或者使用
1 |
|
就可以正常运行了
为什么会出现这个问题呢?
这里有俩个要点:
- @Autowired 和 @Resource 有什么区别?
- StringRedisTemplate和RedisTemplate有什么区别?
1. @Autowired 和 @Resource的区别
@Autowired 和 @Resource 都是 Spring/Spring Boot 项目中,用来进行依赖注入的注解。它们都提供了将依赖对象注入到当前对象的功能,但二者却有众多不同,并且这也是常见的面试题之一,所以我们今天就来盘它。
@Autowired 和 @Resource 的区别主要体现在以下 5 点:
- 来源不同;
- 依赖查找的顺序不同;
- 支持的参数不同;
- 依赖注入的用法不同;
- 编译器 IDEA 的提示不同。
a.来源不同
@Autowired 和 @Resource 来自不同的“父类”,其中 @Autowired 是 Spring 定义的注解,而 @Resource 是 Java 定义的注解,它来自于 JSR-250(Java 250 规范提案)。
小知识:JSR 是 Java Specification Requests 的缩写,意思是“Java 规范提案”。任何人都可以提交 JSR 给 Java 官方,但只有最终确定的 JSR,才会以 JSR-XXX 的格式发布,如 JSR-250,而被发布的 JSR 就可以看作是 Java 语言的规范或标准。
b.依赖查找顺序不同
依赖注入的功能,是通过先在 Spring IoC 容器中查找对象,再将对象注入引入到当前类中。而查找有分为两种实现:按名称(byName)查找或按类型(byType)查找,其中 @Autowired 和 @Resource 都是既使用了名称查找又使用了类型查找,但二者进行查找的顺序却截然相反。
1.@Autowired 查找顺序
@Autowired 是先根据类型(byType)查找,如果存在多个 Bean 再根据名称(byName)进行查找,它的具体查找流程如下:
关于以上流程,可以通过查看 Spring 源码中的 org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues 实现分析得出,源码执行流程如下图所示:
2.@Resource 查找顺序
@Resource 是先根据名称查找,如果(根据名称)查找不到,再根据类型进行查找,它的具体流程如下图所示:
关于以上流程可以在 Spring 源码的 org.springframework.context.annotation.CommonAnnotationBeanPostProcessor#postProcessPropertyValues 中分析得出。虽然 @Resource 是 JSR-250 定义的,但是由 Spring 提供了具体实现,它的源码实现如下:
3. 查找顺序小结
由上面的分析可以得出:
- @Autowired 先根据类型(byType)查找,如果存在多个(Bean)再根据名称(byName)进行查找;
- @Resource 先根据名称(byName)查找,如果(根据名称)查找不到,再根据类型(byType)进行查找。
c. 支持的参数不同
其中 @Autowired 只支持设置一个 required 的参数,而 @Resource 支持 7 个参数
d.依赖注入的支持不同
@Autowired 支持属性注入、构造方法注入和 Setter 注入,而 @Resource 只支持属性注入和 Setter 注入
e.编译器提示不同
当使用 IDEA 专业版在编写依赖注入的代码时,如果注入的是 Mapper 对象,那么使用 @Autowired 编译器会提示报错信息
f.总结
@Autowired 和 @Resource 都是用来实现依赖注入的注解(在 Spring/Spring Boot 项目中),但二者却有着 5 点不同:
- 来源不同:@Autowired 来自 Spring 框架,而 @Resource 来自于(Java)JSR-250;
- 依赖查找的顺序不同:@Autowired 先根据类型再根据名称查询,而 @Resource 先根据名称再根据类型查询;
- 支持的参数不同:@Autowired 只支持设置 1 个参数,而 @Resource 支持设置 7 个参数;
- 依赖注入的用法支持不同:@Autowired 既支持构造方法注入,又支持属性注入和 Setter 注入,而 @Resource 只支持属性注入和 Setter 注入;
- 编译器 IDEA 的提示不同:当注入 Mapper 对象时,使用 @Autowired 注解编译器会提示错误,而使用 @Resource 注解则不会提示错误。
2. 回顾问题
看到这里答案也就清晰了
1 |
|
看一下Bean容器
1 | redisTemplate |
里面会有俩个,如果使用@Resource
,自然会按名称来优先查询了
3. StringRedisTemplate和RedisTemplate的区别
- 两者的关系是
StringRedisTemplate
继承RedisTemplate
。 - 两者的数据是
不共通
的;也就是说StringRedisTemplate
只能管理StringRedisTemplate
里面的数据,RedisTemplate
只能管理RedisTemplate
中的数据。 - RedisTemplate使用的是
JdkSerializationRedisSerializer
序列化类,存入数据会将数据先序列化成字节数组
然后在存入Redis数据库。 StringRedisTemplate使用的是StringRedisSerializer序列化类 - 当redis数据库里面
本来存的是字符串数据或者要存取的数据就是字符串类型数据的时候,那么使用StringRedisTemplate
即可。但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是更好的选择。 redisTemplate
中存取数据都是字节数组
。当redis中存入的数据是可读形式而非字节数组
时,使用redisTemplate取值的时候会无法获取导出数据,获得的值为null。可以使用StringRedisTemplate
试试
a.RedisTemplate中定义了5种数据结构操作:
1 | redisTemplate.opsForValue(); //操作字符串 |
b.StringRedisTemplate常用操作
1 | // 向redis里存入数据和设置缓存时间 |
c.基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
以下封装涉及到了缓存穿透与缓存击穿的解决方案
1.依赖:
1 | <dependency> |
2.实体类
1 |
|
3.封装
1 |
|