Redis_缓存验证码

一、基于 Session 实现验证码登录
1、发送验证码信息

用户在提交手机号后,会校验手机号是否合法

  • 如果不合法,则要求用户重新输入手机号。
  • 如果合法,后台此时生成对应的验证码,同时将验证码保存到Session中,然后再通过短信的方式将验证码发送给用户

为啥什么要存到Session中?因为登录的时候还要校验这个验证码。后面会用Redis代替Session。

2、验证码登录、注册

用户将验证码和手机号进行输入,后台从Session中拿到当前验证码。然后和用户输入的验证码进行校验,如果不一致,则无法通过校验,如果一致,则后台根据手机号查询用户,如果用户不存在,则为用户创建账户信息,保存到数据库,最后都将用户保存到session中,方便后续获得当前登录信息。

3、校验登录状态

用户在请求时候,会从Cookie中携带着JsessionId到后台,后台通过Session中拿到用户信息,如果没有Session信息,则进行拦截,如果有则将用户信息保存到ThreadLocal中,并且放行。

在这里插入图片描述

4、实现登录拦截功能

登录校验功能不止需要在登录地过程中需要实现,在其他功能上也需要实现登录校验功能,让很多业务逻辑执行之前需要实现登录校验,可以使用拦截器来实现。

有一个问题:实现拦截之后,在后续的使用过程中是需要获取用户信息的,让拦截器拦截到的用户信息传递到Controller里,并且还要实现线程安全问题。

可以使用ThreadLocal来做到线程隔离,让每个线程操作自己的一份数据。

ThreadLocal是如何实现线程隔离的?

看过threadLocal的源码,你会发现在threadLocal中,无论是他的put方法还是get方法, 都是先获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离。

如何隐藏用户敏感信息?

通过浏览器观察到此时用户的全部信息都在,这样极为不靠谱,所以我们应当在返回用户信息之前,将用户的敏感信息进行隐藏,采用的核心思路就是书写一个UserDto对象,这个UserDto对象就没有敏感信息了,在返回前,将有用户敏感信息的User对象转化成没有敏感信息的UserDto对象,那么就能够避免这个尴尬的问题了

Session 共享问题

每个服务器中都有一份属于自己的session,假设用户第一次访问第一台服务器,并且把信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台服务器,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,如何解决这个问题呢?

  • 早期的方案是session拷贝,就是说虽然每个服务器上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的服务器,这样的话,就可以实现session的共享

  • 但是这种方案具有两个大问题

    • 1、每台服务器中都有完整的一份session数据,服务器压力过大。

    • 2、session拷贝数据时,可能会出现延迟

  • 后来采用的方案都是基于redis来完成,把session换成redis,redis数据本身就是共享的,就可以避免session共享的问题了

    • 数据共享
    • 内存存储
    • Key - Value结构
二、Redis 代替 session 的业务流程

发送验证码信息:将手机号作为key,生成的验证码作为value存到redis中,等登录的时候,在去校验验证码是否一致。

验证码登录、注册:将用户发过来的手机号和验证码进行校验,查询redis对应的验证码,检验是否和发送过来的验证码是否一致,如果一致查询是否存在用户信息,如果不存在则去创建用户信息,最后将该用户信息存入redis中,以token作为key,用户信息作为value。为了后续的校验登录状态。

校验登录状态:根据请求携带的token去redis查询对应的用户信息,如果没有则拦截,如果有则保存到Threadlocal中,并且放行。

在这里插入图片描述

1、验证码过期逻辑

首次请求验证码

  • 用户首次请求验证码的时候,产生验证码,调用阿里云短信接口发送给用户,同时保存到redis中;
  • 保存到redis中的时候,为其设置过期时间;
  • 同时,手机号的验证码请求次数加一,ip地址的请求此处加一;

非首次请求验证码

  • 首先判断redis中是否存在改手机号的验证码;
  • 如果存在,则返回验证码已发送;
  • 如果不存在,判断手机号是否达到最大请求次数;
  • 如果达到,返回手机号已经达到最大验证码发送次数;
  • 如果未达到,判断请求ip地址是否达到最大请求次数;
  • 如果达到,返回ip地址已经达到最大验证码发送次数;
  • 如果未达到,发送验证码,保存到redis中,并为其设置过期时间,同时,手机号和ip地址的请求次数加一。

ip地址或手机号每发送一条验证码,其计数就会加1,同时为其设置过期时间(每次计数加一就会重新设置过期时间),因此,如果一段时间内发送的验证码达到上限,就会停止发送,直到该计数过期。

2、Key 过期删除策略
  • 定时删除策略:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。(创建定时器删除

    • 优点
      • 对内存友好,使用定时器保证了过期的键尽可能快地被删除释放占用的内存
    • 缺点
      • 对CPU不友好,过期键比较多时删除过期键这一行为可能会占用相当一部分CPU时间,对服务器的响应时间和吞吐量造成影响。
  • 惰性删除策略:放任键过期不管,每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。(使用的时候删除

    • 优点
      • 对CPU友好,只有在取出键的时候才会对过期键进行检查,即不需要cpu定期扫描,也不需要创建大量的定时器。
    • 缺点
      • 对内存最不友好:如果一个键已经过期,但是后面不会被访问到的话,那么就一直保留在数据库中。如果这样的键过多,无疑会占用很大的内存。
  • 定期删除策略:每隔一段时间,程序就对数据库进行一次检查,删除里面过期的键。至于要删除多少过期键,以及要检查多少个数据库,则有算法决定。(定期扫描删除

    • 优点
      • 定期删除每隔一段时间执行一次过期删除操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响;
      • 通过删除过期键,能有效的减少因为过期键而带来的内存浪费
    • 缺点:难以确定删除操作执行的时长和频率
      • 如果删除操作执行得太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除,以至于占用太多CPU的执行时间;
      • 如果删除操作执行的时间太少,或执行时间太短,定期删除策略又会和惰性删除一样,出现内存浪费。

Redis 实际使用的过期键删除策略是定期删除策略惰性删除策略

Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。除了定时遍历之外,它还会使用惰性策略来删除过期的 key,在客户端访问这个 key 的时候,Redis 对 key 的过期时间进行检查,如果过期了就立即删除。定期删除是集中处理,惰性删除是零散处理。通过配合使用这两种删除策略,服务器可以很好地合理使用CPU时间和避免浪费内存空间之间取得平衡。

3、内存淘汰策略

Redis是部署在物理机上的,内存不可能无限扩充的,当内存达到我们设定的界限后,便自动触发Redis内存淘汰策略,而具体的策略方式要根据实际业务情况进行选取。

  • noeviction:只返回错误,不会删除任何key。该策略是Redis的默认淘汰策略,一般不会选用。
  • volatile-ttl:在设置了过期时间的key中,即将过期(剩余存活时间最短)的key删除掉。
  • volatile-random:在设置了过期时间的key中,随机删除某个key。
  • volatile-lru:基于LRU算法,从设置了过期时间的key中,删除掉最近最少使用的key。
  • volatile-lfu:基于LFU算法,从设置了过期时间的key中,删除掉**最不经常使用(使用次数最少)**的key。
  • allkeys-random:在所有key中随机删除某个key。
  • allkeys-lru:基于LRU算法,从所有key中,删除掉最近最少使用的key。该策略是最常使用的策略。
  • allkeys-lfu:基于LFU算法,从所有key中,删除掉**最不经常使用(使用次数最少)**的key。
3.1 LRU 算法

LRU的全称为Least Recently Used,翻译出来就是最近最少使用的意思。它是一种内存淘汰算法,当内存不够时,将内存中最久没使用的数据清理掉。LRU算法常用于缓存的淘汰策略。

  • 原理:以一个队列举例,队列的头部表示最久没有被访问的数据,队列的尾部表示最近刚访问的数据。当我们访问一个元素,根据LRU算法的思想就将当前访问的元素放至队列的尾部,原本该元素后面的元素依次向前挪。所以,当再向该队列中添加新的元素,由于队列已经满了,所以我们需要将该队列的头部的元素(最久没有访问的元素)移除掉,然后将后面的元素依次前移,将新元素添加到队列的尾部。
  • LRU算法实现
    • LRU 算法在实际实现时,需要用链表管理所有的缓存数据,移除元素时直接从链表队尾移除,增加时加到头部就可以了,但这会带来额外的空间开销。而且,当有数据被访问时,需要在链表上把该数据移动到 MRU 端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
    • 在 Redis 中,LRU 算法被做了简化,以减轻数据淘汰对缓存性能的影响。
    • 具体来说:
      • Redis 默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构 RedisObject 中的 lru 字段记录)。
      • 然后,Redis 在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。
      • 接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。
      • 当需要再次淘汰数据时,Redis 需要挑选数据进入第一次淘汰时创建的候选集合。这里的挑选标准是:能进入候选集合的数据的 lru 字段值必须小于候选集合中最小的 lru 值。当有新数据进入候选数据集后,如果候选数据集中的数据个数达到了 N 个,Redis 就把候选数据集中 lru 字段值最小的数据淘汰出去。这样一来,Redis 缓存不用为所有的数据维护一个大链表,也不用在每次数据访问时都移动链表项,提升了缓存的性能。
3.2 LFU 算法

为什么要引入LFU算法呢?因为LRU算法淘汰数据的立足点是根据使用时间间隔淘汰数据,将当前内存中最长时间没有使用过的数据清理掉。这样就存在一个问题:如果一个不会经常使用的数据偶然被使用了一次,那这个并不被经常使用的数据就会一直待在内存中,浪费内存空间。如果我们想淘汰内存中不经常使用的数据,保留经常使用的热点数据,就要使用到LFU算法了。

LFU的全称为Least Frequently Used,意思就是最不频繁使用,所以,LFU算法会淘汰掉使用频率最低的数据。如果存在相同使用频率的数据,则再根据使用时间间隔,将最久未使用的数据淘汰。

3.3 LRU和LFU对比
  • LRU算法淘汰数据的注重点是时间间隔,只淘汰最久未使用的数据;LFU算法淘汰数据的注重点是使用频率,只淘汰最不经常使用的数据。

  • LRU算法实现简单,只需要一个hash表+一个双向链表即可实现;LFU算法实现就复杂很多,需要两个hash表+多个双向链表才能实现。

具体使用时,选择哪种算法作为内存淘汰策略要看具体场景,如果对于热点数据的查询要求比较高,则最好采用LFU算法作为内存淘汰策略。如果没有那么高的热点数据要求,则可以选择实现更为简单的LRU算法。

需要注意的是,Redis中的LRU算法并没有严格按照常规的LRU算法的方式实现,而是基于LRU算法的思想做了自己的优化。我们知道,实现LRU算法时,需要将所有的数据按照访问时间距离当前时间的长短排序放到一个双向链表中,基于这个链表实现数据的淘汰。但Redis中存储的数据量是非常庞大的,如果要基于常规的LRU算法,就需要把所有的key全部放到这个双向链表中,这样就会导致这个链表非常非常大,不止需要提供更多的内存来存放这个链表结构,而且操作这么庞大的链表的性能也是比较差的。

所以,Redis中的LRU算法是这样实现的:首先定义一个淘汰池,这个淘汰池是一个数组(大小为16),然后触发淘汰时会根据配置的淘汰策略,先从符合条件的key中随机采样选出5(可在配置文件中配置)个key,然后将这5个key按照空闲时间排序后放到淘汰池中,每次采样之后更新这个淘汰池,让这个淘汰池里保留的总是那些随机采样出的key中空闲时间最长的那部分key。需要删除key时,只需将淘汰池中空闲时间最长的key删掉即可。

过期策略是在正常情况下清除过期键的策略;内存淘汰策略是在非正常情况下为保证 Redis顺利运行的保护策略

4、Redis 内存淘汰策略的配置
  • 配置Redis最大内存:在配置文件redis.conf 中,可以通过参数 maxmemory 来设定最大内存;当数据内存达到 maxmemory 时,便会触发redis的内存淘汰策略。该参数通常设定为其物理内存的四分之三。

  • 配置Redis淘汰策略:在配置文件redis.conf 中,通过设置 maxmemory-policy 来指定使用哪种内存淘汰策略

  • 配置最大采样数量:在讲Redis的LRU算法的时候,提到了Redis每次会随机选择5个key放入淘汰池中,这个5的数量就是在redis.conf配置文件中通过 maxmemory-samples 选项配置的

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值