黑马点评学习笔记
黑马点评这个很早就学过了,最近在整理简历所以也对进行了笔记整理和补充
黑马点评学习笔记
1.短信登录
1.1 登录
对返回的信息(即存在session中的信息)进行优化
1.1.1 为什么要进行优化?
- 内存压力:session是tomcat的内存空间,这里面存的信息越多,对整个服务来讲,压力就会越大,所以将一些不重要不相关的信息存进去没必要
- 敏感信息:一般来说我们登录成功后只返回用户的账号、头像、昵称就好了,创建时间、密码、电话这些敏感信息都不需要返回,因为存在泄露风险
1.1.2 如何进行优化?
返回一些必要的信息即可
具体就是定义一个UserDTO,里面只有需要存储的必要信息
我们在存储到session之前,先将user转成userDTO
如何转?
正常来说,我们可以new一个dto对象,然后手动一个个存进去就好了
但是我们可以使用现成工具类BeanUtil(cn.hutool.core.bean),他有一个copyProperties方法,意思就是拷贝属性
在后面还会有将user转成map,beanToMap
1.2 校验登录状态
在检验登录状态这一步,我们需要判断用户是否存在,如果存在我们确实可以直接放行,但检验这一步不能只是白做检验这一步了,因为后续的操作可能会用到登录的用户信息,所以我们可以将登录的用户信息进行缓存,方便后面具体的业务使用
1.2.1 用户信息放在本地的哪里呢?
一般会把用户保存到ThreadLocal,那么后续业务就可以直接从ThreadLocal中获取
什么是ThreadLocal?
ThreadLocal就是一个线程域对象,在业务中每一个请求到达我们的服务(进入tomcat)都是一个独立的线程
如果不使用ThreadLocal会出现什么问题?
如果直接保存到一个本地变量那里就可能会出现多线程并发修改数据的安全性问题
但是使用ThreadLocal可以将数据保存到每一个线程的内部,在线程内部会创建一个map去保存,所以每一个线程都有自己独立的存储空间,那么每个请求来了之后都有自己独立的存储空间,相互之间没有干扰(这就是线程隔离)
1.2.2 补充知识点:ThreadLocal很容易造成内存泄漏问题!
那ThreadLocal内存泄漏问题是怎么导致的?
ThreadLocalMap
中使用的 key 为ThreadLocal
的弱引用,而 value 是强引用。所以,如果ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,
ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。简单来说就是
因为ThreadLocal底层是ThreadLocalMap,当线程Threadlocal作为key(弱引用),user作为value(强引用)然后jvm不会把强引用的value回收掉,所以value没被释放
如何解决内存泄漏的问题?
ThreadLocalMap
实现中已经考虑了这种情况,在调用set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完ThreadLocal
方法后最好手动调用remove()
方法什么是弱引用?
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。
在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
1.3 登录拦截器
前面的校验登录状态 我们写在了userController,但是实际上会有很多业务都需要去检验用户的登录状态.但是我们不可能在每一个业务的econtroller 中都编写这一堆校验的代码
这时就应该要想到SpringMVC中的拦截器,它可以在所有controller执行之前去做
如果我们使用了拦截器,那么用户的请求就不再能够直接访问到我们的controller,所有的请求都必须先经过拦截器,再由拦截器判断该不该放行让请求到达controller
所以有了拦截器 我们可以把校验登录状态的相关代码都放到拦截器中去做,那这样一来所有的controller都可以不用再写有关校验的代码了
但这种方法会存在一个小问题:
拦截器确实可以帮助我们实现对用户登录的校验,但是校验完之后的后续的业务中,我们可能需要用到用户信息。
我们可以在校验那一步拿到用户信息,那后续业务如何获得呢?
我们需要把在拦截器中拦截得到的用户信息传递到controller里面去,而且在传递过程中需要注意 线程的安全问题用什么来解决?
综合来看,还是用到了前面提到过ThreadLocal,我们在拦截器中可以把拦截到的用户信息保存到ThreadLocal中
1.3.1 如何实现?
- 实现一个接口HandlerInterceptor
- 实现三个方法preHandle前置拦截、postHandle在Controller执行之后、afterCompletion在视图渲染之后返回给用户之前
在保存用户信息的时候我们可以将ThreadLocal的相关代码写到工具类UserHolder中
在preHandle中一般放行路径存在数据库中,方便管理(但我们现在写出来)
1.4 Tomcat的运行原理
- 当用户发起请求,会访问我们向tomcat注册的端口(任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat也不例外)
- 当监听线程知道用户想要和tomcat连接时,就会由监听线程创建socket连接,socket都是成对出现的,用户通过socket互相传递数据
- 当tomcat端的socket接受到数据后,此时监听线程会从tomcat的线程池中取出一个线程执行用户请求
- 在我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller、service、dao中,并且访问对应的DB
- 在用户执行完请求后,在统一返回,此时找到tomcat端的socket,再将数据写回到用户端的socket,完成请求和响应
也就是说
每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,每个线程都是独立的(这也是为什么后面用ThreadLocal做到线程隔离,每个线程操作自己的一份数据)
1.5 session共享问题
问题:多台Tomcat并不共享session存储空间,当请求切换到到不同tomcat服务时导致数据丢失的问题
具体来说就是
每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat并且把自己的信息存放到第一台服务器的session中
但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session
所以此时整个登录拦截功能就会出现问题(假设登录拦截功能部署在了第一台服务器)
1.5.1 我们如何解决这个问题?
-
早期方案:session拷贝
就是当任意一台服务器的session修改时,都会同步给其他的tomcat服务器的session
但是这种方案有两大问题:
- 内存损耗:每台服务器中都有完整的一份session数据,服务器压力过大
- 同步问题:session拷贝数据时,可能会出现延迟
-
基于redis完成:也就是把原先保存到session的信息,保存到redis中
1.5.2 session共享应该满足以下三方面
- 数据共享
- 内存存储(因为session是基于内存的所以它的读写效率比较高,例如登录校验这种访问频率比较高,如果读写效率低,是难以满足高并发的这种需求的)
- k-v结构
1.6 redis代替session的实现
1.6.1 如何实现?
数据类型的选择
由于存入的数据比较简单,我们可以考虑使用String,或者是使用哈希,如下图,如果使用String,同学们注意他的value,用多占用一点空间,如果使用哈希,则他的value中只会存储他数据本身,如果不是特别在意内存,其实使用String就可以啦。
在redis中保存单个对象比较常见的两种方式:
String结构:
其实就是把我们的Java对象序列化为json字符串
看起来比较直观,但是把整个数据都变成一个串,字段与字段之间都耦合在了一个整体,只能对整个做curd
内存占用较多,因为这里面还会有json串的格式,例如大括号、冒号、引号等,如果这个数据越长里面包含了这种符号也越来越多会有额外的一些数据存储
哈希结构
哈希结构和String结构有比较大的差别
就是把我们Java对象中的每一个字段都作为value中的一个field和value去保存
每个字段都是独立的,我们可以针对单个字段做curd
内存占用更少,因为哈市结构只需要保存数据本身就可以了
使用String结构,就是一个简单的key,value键值对的方式
如何设计key?
在使用session保存用户信息的时候,我们使用例如code作为key,那我们在redis中也用code作为key可以吗?
这显然是不可以的
因为session有一个特点,每一个不同的浏览器在发起请求的时候都有一个独立的session,也就是说在tomccat内部维护了很多很多的session,那么不同浏览器携带的手机号来的时候都是自己独立的session,他们都以code为key,但互相之间互不干扰。
但redis是共享的内存空间,不管是谁来发起请求,在后台只有一个redis,大家都往里面存,不同的手机号都用code为key那就会一直被覆盖,因此大部分数据会丢失。
因此我们要保证每个手机号来的时候保存的key都是不一样的
那既然每个手机号都要有不同的key,那我们可以直接使用手机号作为key
这样做有两个好处:
- 确保每个手机号都有自己唯一的key
- 还有助于我们在后面获取验证码进行验证
关于取数据的问题:
因为tomcat会自动地帮我们去维护session,浏览器发起请求了,就给浏览器创建一个新session,如果session存在,就不用创建。那tomcat怎么知道你的session在哪里?创建session的时候就会自动创建sessionid写到用户浏览器的cookie,那么以后每次请求就会带着cookie、带着sessionid,那样自然就会找到session,那么就会自动从session帮助我们取出数据,这就不用担心取数据的问题
但是现在在redis中怎么取数据呢?
在redis中,我们用手机号作为key存进去了(value是验证码),那用户要进行登录的时候还得带着这个信息来取这个验证码。短信验证码登录、注册的时候用户会提交手机号和验证码,那我们根据这个手机号就能在redis中取出数据(这也是为什么我们要用手机号作为key)
但是呢如果我们采用phone:手机号这个的