我们常见的web网站或者app中,用户使用的第一步就是手机验证登录。
1、输入手机号
2、点击获取验证码
3、点击登录
那么这里在后端涉及两个操作,1:获取验证码,2:登录
在获取验证码之后,那么手机号和验证码将一一绑定,然后返回给前端,当前端登录的时候,拿手机号和验证码进行验证。
此功能主要以下几步
1、验证手机号是否合规。正则表达式:^1[3-9]\d{9}$
2、生成验证码&发送。可以使用hutool工具获取一个随机6位数字
3、将验证码保存。直接利用redis的 set key value。其中key为手机号,value为验证码。
登录时,前端将会把手机号和验证码发送给后端。后端的主要功能是校验手机号,校验
验证码。
1、手机号验证直接查询数据库判断当前是否存在。
2、从redis中通过get key获取对应的验证码,进行匹配。
当用户登录成功之后,可以将用户的登录状态放入redis中,在用户每次请求的时候,利用拦截器去redis中获取登录状态。
为了保证请求后续的逻辑都能拿到用户的信息,可以将获取到的用户登录状态放入ThreadLocal中。
一般在电商网站,日均PV可能达到亿级流量,这就意味着会有大量的流量过来请求商品数据。但是商品的数据一般都放在结构化数据库中,例如mysql。如果这些请求都直接请求mysql的话,那么将导致mysql压力非常大。
考虑到请求的类型一般都是读请求,因此可以将商品数据放入到redis中,来应对大流量的请求。
1、redis需要多大内存呢?是否要把成千万的商品信息都放入redis?
2、redis和mysql如何保证数据一致性呢?
3、如果redis里面没有数据,怎么保证mysql不被打挂?
首先我们先分析一下查询商品的一般过程:
1、通过id查询redis。
2、如果redis存在,则直接返回
3、如果redis不存在,则查询数据库
4、如果数据库不存在,返回错误。
5、如果数据库存在,则写入redis,然后返回。
上述过程是一个简单的流程。
那么上述流程是否有问题,是否考虑了2.2的问题呢? 显然没有。
问题一:redis需要内存多大,是否需要把所有商品都放入redis呢?
回答一:显然不需要,在流程中,我们从数据库中查询出来一个商品,就将其放入到redis,这样随着系统运行,redis里面的商品会越来越多。但是我们知道redis是基于内存,内存的成本很昂贵,因此对于内存的使用需要格外的慎重。解决办法就是在每次放入redis时,需要增加一个超时时间,这样当到达了超时时间,redis的内存就会被清理。这样就防止redis存储僵尸数据。
问题二:redis和mysql如何一致呢?(执行异常&线程并发)
回答二:
方法一:如果系统压力不是很大的情况下,当针对商品进行修改的时候,我们可以将mysql的数据数据之后,删除redis数据,这样在下次请求的时候,redis数据不存在,直接查询数据库,然后将最新的数据更新进去。但是此时还是会发生小概率的数据不一致情况。
这里还有一个问题,就是如果mysql数据更新成功,但是删除redis失败了,这也会导致数据不一致,这个时候就需要将redis和mysql的执行放入同一个事物中。
方法二:如果要求数据强一致,我们就需要针对mysql的写和redis的度和写进行 互斥锁。即,当更新数据库的时候,禁止redis操作。当redis在读和写的时候,禁止mysql写。
方法三:不直接操作mysql,而是增删改都在redis,然后通过一个异步线程,将redis数据同步到mysql。但是一致性和redis内存使用都不是最优的。
问题三:如果redis没有数据,如何保证数据库不被打挂?
回答三:由于我们在向redis里面放数据的时候增加一个超时,这就导致redis数据会失效,如果数据大量失效,就会导致所有的请求都直接请求redis。这就会导致redis压力很大。针对这种情况,可以采用redis随机超时,即超时时间是被打散的,避免所有redis数据同时失效。
另外一种极端情况,流量频繁请求一个根本就不存在的数据(恶意攻击),这就导致每次redis都未命中,流量打到mysql,mysql又查不到数据。这就导致缓存穿透。这种情况一般采用缓存空对象来解决。但是如果恶意攻击不断更换不存在的数据,将导致redis缓存大量的空对象,浪费空间。因此可以采用布隆过滤器来解决。
上述三个概念是面试经常提到的,而且也是开发过程中一定要考虑的,但是这个几个概念非常难记,老搞混,今天就详细介绍一下这个三个概念:
1、缓存击穿:
概念解释: | 重点在一个【穿】字。指的是,查询的数据redis没有,但是mysql有。 按理来说这个是正常的,因为redis的key终会有过期的哪天,而此时请求来的时候一定会击穿。但是如果这个key是一个热点key并且构建缓存耗时很长,那么就会出现问题。 设想一下,如果是一个秒杀品key,一瞬间很多人来抢,如果redis不存在,那么一定会打到mysql。而由于构建缓存耗时很长,这就导致在构建缓存这段时间,大量的请求都会来mysql。那mysql肯定受不了。 |
解决办法 | 首先需要预热,这对这种热点key,一定要预热。 方法一:就是可以尝试互斥锁,当第一个线程发现没有数据时,先redission加锁,然后去查询mysql。此时第二个线程来的时候,发现加锁失败,那就一直等。直到redis数据被线程一构建成功。 方法二:可以尝试逻辑过期。意思就是rediskey不设置过期,而是在value里面增加一个过期时间戳。当线程来请求的时候,都能拿到value,但是检查发现value是一个过期的。那么就去加锁,重新构建。但此时只有一个线程能加锁成功,其他线程无需等待,直接返回旧数据。 |
2、缓存穿透:
概念解释 | 重点在一个【透】字。指的是,查询的数据redis没有,去mysql查也没有,直接透了。 按理说透了直接返回给前端失败就行,为什么会有问题呢? 问题就在于这种穿透的数据,下次前端可能还会请求,每次都打到mysql,那如果有恶意攻击,那mysql肯定受不了。 |
解决办法 | 方法一:缓存空对象。当mysql查询不存在的时候,向redis插入一个空数据,这样下次再请求,redis就会命中了。这种办法很常用,但是有一个缺陷就是增加redis占用空间。如果有恶意攻击,导致redis有大量垃圾空对象 方法二:布隆过滤器。当请求来的时候,先不检查redis,先检查布隆过滤器,如果过滤器返回不存在,那么就直接返回前端。如果过滤器提示存在,这时候再去查redis。 但是缺点就是布隆过滤器有概率误差问题。而且比较复杂,还需要考虑hash函数等问题 除此之外,我们可以先主动校验id规则,id长度,等等。 |
3、缓存雪崩:
概念解释 | 重点在一个【崩】字。指的是,大量的请求redis中没有或者redis宕机,导致大量的请求达到了mysql。这mysql肯定受不了 |
解决办法 | 第一种情况是rediskey过期。这种解决办法就是不要让大量的rediskey同时过期。在设置过期时间的时候,增加一个随机偏移量,把过期时间打撒一点。 第二种情况就是redis挂了,这个比较难解决,首先redis集群必须有主从,高可用,第二我们可以增加一些多级缓存,例如前端缓存,jvm本地loadingcache缓存等。 |
全局id在互联网业务中非常常见,我们这里不去讨论各种其他方案了,直接一步到位。
1、雪花算法
2、redis自增key
第一种雪花算法在大部分场景都能满足,其实很多大厂也在用雪花算法。基本上就够用。如果非说雪花算法的缺点那也就是需要获取机器编号或者服务时钟。
第二种就是利用redis的increby命令。但是这里面我们需要加一点花样,而不是直接从0一直加下去。
通常来将全局id一般都是一个long类型。因此可以将long类型前面32bit和后面31bit作为拆分。
32bit:表示当前【业务开始的时间】到【当前时间】的秒数。正常来讲32bit能表示4294967295秒,也就是136年。完全够用了。
31bit:表示当前redis的自增key的value。当然redis的key不要所有业务都公用同一个,应该按照不同业务进行区分。例如 “user:id”, "order:id"。由于redis的incrby对value的数值有限制,因为这个value不能一直自增。那么我们可以分而治之。在key上拼接date。也就是 "order:id" + date。这样每一天的订单数就在这key下存储。
这样一个全局唯一id就生成了。一般来讲redis的写操作的qps极限在10wqps。基本上所有业务都能满足了。如果你在一个非常大型的互联网且做的是一个平台,那就搞多个redis节点就好了。
这里需要注意的是,这个全局唯一中的【全局】两个字指的是业务范围,比如用户id范围,订单范围,但是订单id和用户id完全是可以重复的。