餐厅评价系统项目及面试问题总结
项目地址:gjyyyy/dianping
登录问题
怎么通过 Redis+JWT+ThreadLocal+拦截器 实现单点登录
- 用户登录后,(用用户id等)信息生成JWT Token并将其存储到Redis中。
- 将JWT Token返回给客户端,并在响应头中设置Authorization字段,值为Bearer加上JWT Token。
- 客户端在每次请求时,在请求头中带上Authorization字段,值为Bearer加上JWT Token。
- 服务端在接收到请求时,先经过拦截器,在拦截器中先从请求头中获取JWT Token,并解析出其中的用户信息(获取用户id)。然后再到Redis中验证Token的唯一性和有效期。
- 如果有效则将有效数据存入ThreadLocal中,并放行,无效,则拦截。
补充:
- 并在每次请求时刷新token的过期时间来减少重复登录操作。
- HTTP请求进来的时候将JWT解析的数据放进ThreadLocal,出去的时候需要将数据从ThreadLocal移除,否则会造成内存泄漏。
使用两层拦截器的原因?
使用拦截器是因为,多个线程都需要获取用户,在想要方法之前统一做些操作,就需要用拦截器,还可以拦截没有登录的用户,但只有一层拦截器不是拦截所有请求,所以有些请求不会刷新Token时间,我们就需要再加一层拦截器,拦截所有请求,做到一直刷新。
秒杀问题
使用 Redis + Lua脚本实现对用户秒杀资格的预检,同时用乐观锁解决秒杀产生的超卖问题。
- 由于在判断套餐/优惠券库存足够后,才能进行购买,而在判断库存时并非原子操作,所以可能会出现多线程的安全问题(比如只剩一个库存但是多个线程同时判断库存足够都下单就会出现问题),用lua脚本可以原子性的判断库存是否足够来解决这个问题。
-
并且在解决超卖问题时,考虑性能方面,采用乐观锁来解决,用剩余库存数量作为”版本“字段,只要stock>0就可以购买。
- 使用redisson的分布式锁解决一人一单问题。
缓存三剑客
-
缓存穿透,它是指查询一个不存在的数据,这个数据既不在缓存中,也不在数据库中,导致每次请求都会直接打到数据库上。
造成这个问题的原因主要有两个,一个是用户恶意构造不存在的查询条件(如随机生成的无效 ID);另一个是数据库中确实没有对应的数据,而缓存中也没有命中。
缓存穿透会让数据库承受大量无效请求,导致性能下降或崩溃。解决方案主要有两种,
- 一个是使用布隆过滤器,在请求到达缓存之前,先用布隆过滤器判断数据是否存在。
- 另一个是缓存空值,当查询结果为空时,将空值写入缓存,并设置较短的过期时间。这样可以避免重复查询数据库。
-
缓存击穿,它是指某个热点数据在缓存中过期后,大量并发请求同时访问该数据,导致所有请求都打到数据库上。
造成这个问题的原因主要有两个,一个是热点数据的缓存过期时间设置不合理。另一个是大量用户在同一时间访问同一个热点数据。
缓存击穿会让数据库瞬间承受高并发压力,导致性能下降或崩溃。解决方案主要有两种,
- 一个是采用互斥锁(Mutex Lock),在缓存失效时,只允许一个线程去加载数据并更新缓存,其他线程等待。可以通过分布式锁(如 Redis 的 SETNX 命令)实现。
- 另一个是采用永不过期策略(或者逻辑过期),对于热点数据,可以不设置过期时间,而是通过后台定时任务主动刷新缓存。
-
缓存雪崩,它是指大量缓存在同一时间过期,导致大量请求直接打到数据库上。
造成这个问题的原因主要有两个,一个是缓存的过期时间设置为相同的值,导致集中失效。另一个是缓存服务宕机或不可用,导致所有请求都直接访问数据库。
缓存雪崩会使数据库瞬间承受巨大的请求压力,导致系统崩溃。解决方案主要有三种,
- 第一个是为缓存设置随机的过期时间,避免集中失效。例如,在基础过期时间上加上一个随机值。
- 第二个采用是多级缓存,使用本地缓存(如 Guava Cache)作为一级缓存,Redis 作为二级缓存,减少对数据库的直接访问。
- 第三个是采用降级策略,在缓存不可用时,返回默认值或静态页面,避免请求直接打到数据库。
在本项目中采用:
- 通过设置空值缓存来解决缓存穿透问题;
- 通过使用互斥锁+逻辑过期方式来解决缓存击穿问题:当查询的数据逻辑过期时,开启另一个线程并加上互斥锁去查询数据库并更新缓存,同时返回这个逻辑过期的值;未逻辑过期就直接返回缓存值。
RabbitMq
使用RabbitMQ消息队列存储秒杀消息,通过秒杀消息异步执行数据库订单状态,提高系统处理能力;订单秒杀成功,同步发送延迟消息,延迟检查该秒杀订单的支付状态;
Feed流
关注推送也叫Feed流,直译为投喂。为用户提供“沉浸式”的体验,通过无线下拉刷新获取新的信息。
Feed流常见的两种模式
Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈:
Ø优点:信息全面,不会有缺失。并且实现也相对简单
Ø缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
Ø优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
Ø缺点:如果算法不精准,可能起到反作用
本项目中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:
1. 拉模式 — 也叫读扩散
每个消息只有一份。只有在读的时候才会拷贝一份出来,因此叫做读扩散。
优点:节省空间,收件箱收完就可以扔掉,消息只需要存储一份。
缺点:读取延迟高,每次拉去如果关注信息很多,排序、处理耗时长
2. 推模式 — 也叫写扩散
博主给每个关注自己的用户都发一份信息到他们的收件箱
优点:延迟低,登录及看
缺点:内存占用极大,如果粉丝多,拷贝保存的信息就特别多了
3. 推拉结合 — 读写混合
将粉丝群体分成普通粉丝和活跃粉丝,普通粉丝使用拉模式,活跃粉丝使用推模式
用户签到
我们可以把年和月作为bitMap的key,然后保存到一个bitMap中,每次签到就到对应的位上把数字从0变成1,只要对应是1,就表明说明这一天已经签到了,反之则没有签到。
可以用bitcount来获取本月签到次数,或者由后端逻辑实现连续签到次数。