黑马点评面试题
使用Redis+Token解决了在集群模式下的Session共享问题,使用拦截器实现用户的登录校验和权限刷新
# 为什么使用Redis+Token来代替session
由于项目模拟了tomcat集群,用户的请求可能被不同的服务器处理。session是存储在单个服务器的内存中,其他服务器无法访问该Session,导致用户状态丢失,而如果使用服务器间的Session复制,会有延迟,导致数据的不一致,而且Session的复制会占用额内存,影响服务器的性能。
使用Redis+Token可以根据Token为key将用户信息存储在Redis中,所有服务器都可以共享Redis中的用户信息,从而实现SSO。
# 为什么使用Redis+Token而不用JWT
Redis+Token是有状态,用随机的UUID生成Token作为Key,依赖Redis存储用户信息,选择Redis+Token可以在用户登出或者有异常行为时,主动删除Redis中的Token来结束会话
不用JWT主要是因为JWT无法提前失效,只能将其加入Redis的黑名单中,在拦截器中拦截,但是也会违背JWT无状态的特性;而且JWT无法续期,我们的项目主要是用户在每次访问时会续期,如果用JWT,续期需要重新生成JWT,会导致有过多的JWT,增加管理负担;而且JWT通常UUID大,会增加网络传输负担。
通过Redis缓存空数据解决了缓存穿透问题,结合动态TTL机制防止缓存雪崩,缓存失效时使用互斥锁解决缓存击穿
# 缓存穿透
# 缓存雪崩
# 布隆过滤器的实现原理
# 缓存击穿
解决方法:互斥锁从数据库中获取数据、使用逻辑过期,数据过期通过异步获取数据,并发线程返回旧数据。
# 为什么用逻辑过期而不用互斥锁
因为我们缓存的数据是商户的信息,一致性要求不高,使用逻辑过期可以保留redis中缓存的数据,数据过期时开启一个异步线程从数据库中获取数据更新,而并发线程可以直接返回缓存的旧数据。其实也可以使用互斥锁,因为从数据库获取商户信息的过程也不费时间,不会导致可用性降低,如果数据重建过程费时间才需要根据一致性要求决定选择方案。
使用Redis实现全局唯一ID生成,并通过乐观锁(CAS机制)进行库存控制,解决超卖问题。
# 为什么使用Redis生成唯一ID,为什么不选择数据库子自增ID、雪花算法呢
因为Redis是基于内存的,适合高并发,可以保证高可用、高性能、唯一性, 而数据库性能瓶颈明显,且分库分表时维护自增ID复杂度高, 雪花算法需要考虑时钟回拨(回拨时id会归零),而且不同机器之间系统时钟可能存在微小偏差,可能导致id不是有序的
# 为什么使用乐观锁不用悲观锁
在高并发情况下,悲观锁竞争激烈,会轮流竞争锁后才进行库存判断减库存,越后的线程阻塞越久,体现在前端显示加载中,用户体验差。
而乐观锁我们是利用了数据库的行锁,通过SQL语句来实现判断和减库存的原子性,这样,只要库存不够,后面的线程都会直接返回库存不足
也就是说乐观锁锁的粒度比悲观锁小,就是悲观锁是所有线程在获取互斥锁的时候串行化,而乐观锁是在数据库更新操作时利用行锁实现串行化,
但是这样对数据库压力大,QPS(吞吐量/秒请求量)有限,所以后面我们使用了Redis来优化,将秒杀优惠券库存预热到Redis中,通过lua脚本来实现判断资格、减库存操作的原子性,由于Redis是基于内存的,能提供更高的QPS
# 高并发下CAS失败率高,如何优化?
我们的SQL语句是通过库存大于0的条件,不是根据版本号或者库存数量来判断的,保证了先来先得,先操作数据库的线程先成功,只有库存不足才会失败,而不会因为版本号不匹配就失败。
不过如果CAS失败率高,可以使用LongAdder类来进行优化,LongAdder类内部维护一个base值和 Cell[] 数组,刚开始会直接CAS修改base值,如果CAS失败次数过多会进行分片,base值被分配到每个Cell中,每个线程优先修改自己对应的 Cell 分片,只有线程间操作同一个 Cell 时才会触发少量 CAS 操作,
使用Redisson分布式锁解决一人一单的问题,Redis+Lua 脚本实现秒杀活动的抢单业务,并使用阻塞队列(Stream/RabbitMQ)异步完成数据库库存扣减和订单生成
# Redis实现的分布式锁
# 如何解决分布式锁误删的
分布式锁误删是因为设置了锁的过期时间,当获得锁的线程被阻塞时,由于锁过期,其他线程获得到锁,但是原来被阻塞的线程又恢复执行完业务后将别的线程的锁释放。
我们是通过Redis的hash结构,将当前获得锁的线程ID存入hash的值中,每次释放锁之前会判断一些锁是否为当前线程所有。
但是在判断和删除锁之间不是原子性的,并发下还是会有误删的可能,所以我们使用了Lua脚本来保证操作的原子性,
# 使用Redisson的优势
Redisson提供了可重入、非阻塞重试、超时续约(看门狗机制)等功能,而且释放锁是原子操作,不需要通过Lua脚本实现防误删
# Redisson解决一人一单的问题
使用了Redisson的分布式锁,保证查询用户订单和创建订单操作的安全性,确保用户不能重复下单,实现一人一单
# Redis+Lua脚本实现的秒杀业务
通过Lua脚本实现库存检查、一人一单判断、完成抢单操作的原子性,再将下单业务放入阻塞队列,利用独立线程异步下单
# 阻塞队列(Stream/RabbitMQ)异步完成数据库库存扣减和订单生成
基于阻塞队列的异步秒杀存在哪些问题?
- 内存限制问题
- 数据安全问题
# RabbitMQ异步完成数据库库存扣减和订单生成
使用rabbitMQ代替阻塞队列可以看项目优化
通过消息队列由一个消费者处理订单业务,通过持久化、消费者重试机制保证消息的消费,如果三次失败会进入专门的队列中由人工处理
使用Redis的ZSet实现了点赞排行榜功能, 使用Set集合实现关注、共同关注功能;
# ZSET
以时间为score,实现点赞排行榜功能,显示最早点赞的TOP5
# SET
将用户关注的好友id放入Redis中,通过求交集获取共同关注的好友