介绍一下项目

  • 功能:这是一个集校园美食、美景分享、店铺搜索、店家优惠券秒杀等功能于一体的生活分享平台。

  • 项目框架:由springboot开发的前后端分类项目,使用了redis集群、tomcat集群、
    mysql集群来提高服务器的性能

  • 项目的技术栈: springboot+nginx+mysql+lombok+mybatis+hutool+redis

集成第三方登录

短信登录功能

初次登录

注册之后,校验用户登陆的手机号和验证码,然后根据手机号查询用户信息,不存在则创建,然后将用户信息保存到redis,以生成的token作为redis的key

为什么用redis?

因为使用了负载均衡有许多服务器,而不同的服务器之间session无法共享,所以用redis代替session

校验登录状态

用户携带token进行访问,从redis中取出token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则将其保存到threadLocal中,并且放行。

[!TIP]
因为不同服务器之间的session无法共享,使用redis可以实现数据共享
将部分可展示属性封装成dto单独传递是为了实现数据的脱敏处理

双重拦截器干什么用的?

浏览需要验证身份的模块时,在这些模块的共同的拦截器中拦截请求,刷新token。

但是有些页面不需要验证登录状态,但是也要刷新token,所以新添加一层拦截器专门用来刷新token有效期,避免出现token过期用户需要重新登录

threadLocal是干什么用的?

服务器获取用户信息 获取到之后将用户缓存到ThreadLocal中,方便后续的使用。
因为在业务中,每一个请求都是一个独立的线程,所以不能将用户信息保存到本地变量中,这样会导致多线程并发修改的安全问题,使用threadLocal就是在拦截器拦截到用户请求传递给控制器的时候,让每一个请求都开启一个独立的线程,避免出现线程安全问题

使用延迟双删方案解决缓存与数据的数据一致性问题?

首先为什么会出现数据一致性问题?

数据一致性问题值的是数据库和缓存中保存的数据不一致。

使用redis来缓存商户商品信息,提升访问速度

查询数据库之前先查询缓存,如果缓存数据存在,则直接从缓存中返回

如果缓存数据不存在,再查询数据库,然后将数据存入redis。

缓存与数据库的数据一致性问题及其解决方案

什么是延迟双删方案?

其实就是缓存更新方案中的双写方案,也叫旁路缓存读写策略

  • 双写方案(也叫旁路缓存读写策略):
    查询时,如果缓存中没有,那么查询数据库,将数据库结果写入缓存,并设置超时时间
    修改时,先修改数据库,再删除缓存。等再查询数据时,再从数据库中加载到缓存中

  • 并采用超时删除和内存淘汰机制作为兜底方案

  • 双删指的是更新数据库的时候一并删除缓存

  • 延迟指的是延迟更新缓存,当查询的时候再去更新缓存

  • 解决数据一致性问题有几种常见的缓存更新策略:

    • 内存淘汰: 内存不足的时候自动淘汰部分数据,下次查询的时候更新缓存
    • 超时删除: 给数据添加ttl,到期自动删除
    • 主动更新

为什么要使用延迟双删方案?

延迟双删主要就是先操作数据库,再操作缓存

  • 因为数据库操作太慢,为了防止删除缓存到更新数据库的时候有其他线程过来查询数据出现缓存击穿问题,因此必须先操作数据库,将缓存和数据库的操作放到同一个事务中就可以保证缓存与数据库操作的原子性

使用缓存空值和布隆过滤器缓存穿透问题

  • 缓存穿透问题是什么?:客户端请求的数据在数据库和缓存中都不存在

  • 自定义布隆过滤器:在缓存之前添加一个布隆过滤器,提前将用户可能查询的数据放在过滤器里,用hash思想判断数据是否存在,但是布隆过滤器会有误判可能(存在的不一定存在,不存在的一定不存在)

  • 缓存空值:如果查询的数据数据库中没有,那么就在缓存中缓存一个空对象,下次再来查的时候就不会找到数据库了,在缓存中发现是空值则直接结束查询

使用缓存预热解决缓存雪崩问题

  • 什么是缓存雪崩?:同一时间大量缓存key同时失效 或者 redis服务器宕机导致大量请求同时到达数据库

  • 针对redis宕机:可以使用reids集群和多级缓存

  • 针对大量key同时失效:可以采用缓存预热:提前将热点key存入缓存中并设置合理的不同的过期时间,这个过滤实践可以手动设置逻辑过期

    • 缓存预热方法:这里可使用消息队列kafka,异步地进行缓存预热。将数据库中的热点数据的主键或者 ID 发送到消息队列中,然后由缓存服务 消费 消息队列中的数据,根据主键或者 ID 查询数据库并更新缓存)

什么是kafka?

分布式消息队列,一般用于系统或服务之间的消息传递

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

  • 缓存击穿是什么?:也叫热点key问题,指的是一个高并发访问且缓存重建比较复杂的key突然失效

  • 热点key过期之后,使用一把互斥锁,让失效key排队依次重建。查询缓存之后之后如果没查到,会先去获取互斥锁,获取到互斥锁才能进行查询数据库;还可以设置逻辑过期来解决缓存过期问题

[!TIP]
利用redis的setnx方法来获取锁

使用rabbitmq消息队列实现优惠券秒杀功能,并通过乐观锁解决超卖问题,使用分布式锁解决一人一单问题

⭐️rabbitmq消息队列+令牌桶算法实现优惠券秒杀问题(你项目的亮点是是什么!!!??)

因为为秒杀涉及到很多步骤,查询用户是否持有优惠券,判断库存是否充足,查询用户是否下单,校验是否一人一单,扣减库存,创建订单这些步骤,很多操作是要操作数据库的,这样如果是一个线程串行执行就会很慢,而我们只需要确认用户有下单资格(持有优惠券、库存充足)就可以了。因此将将耗时比较短的逻辑判断放入到redis中,确认能下单之后在后台再开一个线程执行后续步骤,实现异步下单。

其次秒杀是一个高并发的场景,短时间内后端访问量巨大,可能会压垮系统,只有少许人能秒杀成功,因此首先要做的是限流,针对用户的id进行限流
令牌桶算法是这样的:
首先定义一个容积给定的桶,可以存放一定数量的令牌,按照一定的速率往桶里放令牌,桶满了多余的令牌被丢弃。
当一个请求来了,必须先从桶里拿令牌,然后执行,执行完将令牌丢弃,如果桶里没有令牌了,那么请求被拒绝或等待

令牌桶的优点

  • 速率是平均的,可以动态调整速率

令牌桶的缺点

  1. 速率和桶的容量需要合理设置

还有什么限流策略

  1. 滑动窗口:把固定窗口的时间片再细分,可以更平滑限流
  2. 漏桶算法:以一定速率往桶里添加请求排队,满出来的直接丢弃,再以一定速率从桶里漏水处理请求(类似于消息队列)
  3. 固定窗口:限制单位时间处理的请求数量

乐观锁解决超卖问题

首先什么是超卖问题:线程a来查库存,此时大于1,要去扣库存但还没来得及扣,此时线程b来查库存,发现也大于1,那么两个线程都去扣减库存,导致超卖

乐观锁:只在数据更新时判断有没有其他线程对数据进行了修改

[!TIP]
悲观锁:认为线程安全问题一定会发生,因此每次执行数据库操作的时候都需要加锁
常见的悲观锁有:synchronized、lock

  • 操作数据库的时候对数据的版本号进行+1 只有当版本号不变的时候才能操作 ,假设线程a操作之后,version号从1变成了2,那么线程2操作的时候发现version != 1 因此不能操作

使用分布式锁解决一人一单问题

  • 一人一单问题是指,同一个优惠券,一个用户只能下一单

  • 分布式锁:满足分布式系统或者集群模式下多线程可见并且互斥的锁。

  1. 分布式系统 集群模式

  2. 多线程可见 互斥

使用redis的setNx方法,当多个线程过来抢优惠券,第一个线程抢到锁之后,去执行业务,然后删除释放锁,其他线程等待并重试

[!TIP]
利用setnx的方法加锁,同时设置过期时间,防止死锁

  • 误删别人的锁的问题: 如果持有锁的线程在锁的内部出现了阻塞,导致他的锁自动释放,这时其他线程,线程2来尝试获得锁,就拿到了这把锁,然后线程2在持有锁执行过程中,线程1反应过来,继续执行,而线程1执行过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁进行删除,这就是误删别人锁,因此在删除锁之前要判断这把锁是不是自己的

综上:使用redisson分布式锁替代setnx解决这些问题

压测的时候主要关注的指标

  • 并发量
  • 响应时间
  • 错误率
  • 吞吐量
    • QPS:每秒能够处理的查询数量
    • TPS:每秒能够处理的事务数量
  • 资源使用率