有的方案设计的不好,不如说热门排行榜那一块。牛油们麻烦给点建议
需要黑马点评简历的可以送一朵花之后私我领取
登录
双重拦截器完成登录状态的刷新
S
用户请求进入之后,对于一些业务比如帖子发布,优惠券购买,我们需要判断这个用户是否登录了,登录了放行,没登录则拦截。但是对于其他一些业务比如说浏览博客,对于未登录的用户我们也不需要拦截。
T
但是这里还存在一个token过期的问题,我们一般会给这个token设置一个过期时间。我设置的是7天,如果过期了,用户就会从登录状态变为未登录的状态,但是如果这七天内用户再次登录了,进行了一次查询,我们应该给这个token进行一次续期。
A
我设置成了两层拦截器,第一层拦截器拦截所有请求,如果token存在,则给这个token刷新,进行一个续期操作。如果token不存在则直接放行。接下来这些请求会到达第二个拦截器,这个拦截器只拦截一些必须登录操作,只有token存在才可以执行。
R
所以,我设置两层拦截器,第一层拦截器拦截所有接口的请求,token存在则对token进行一个刷新,不存在直接放行。第二层拦截器拦截那些必须要登录才能完成的操作。
参考代码
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1、获取token,并判断token是否存在 String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)){ // token不存在,说明当前用户未登录,不需要刷新直接放行 return true; } // 2、判断用户是否存在 String tokenKey = LOGIN_USER_KEY + token; Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey); if (userMap.isEmpty()){ // 用户不存在,说明当前用户未登录,不需要刷新直接放行 return true; } // 3、用户存在,则将用户信息保存到ThreadLocal中,方便后续逻辑处理,比如:方便获取和使用用户信息,Redis获取用户信息是具有侵入性的 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); ThreadLocalUtls.saveUser(BeanUtil.copyProperties(userMap, UserDTO.class)); // 4、刷新token有效期 stringRedisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.MINUTES); return true; }
博客
对博客表进行垂直拆表,线程池并行加载博客
S
一张博客表的内容包括,id, title, user_id, content, likes等等,但是博客出现的情形有两种,一种是在首页出现,我们只需要加载title和likes字段,一种是详情页面,我们需要加载所有属性。同时我们在加载博客的时候还要判断这个博客是否被当前用户点赞过。
T
在首页出现的时候,我们并不需要加载出content字段,因为这个字段属性是Text,如果在首页也把这个字段加载出来,对我们的资源消耗会比较大。
A
所以我们需要对博客表进行垂直拆表,把text字段拆出来。在详情页面的时候,为了加载出一张完整的博客,我采用了线程池+CompleteFuture类的办法去完成,并且构建它的依赖树,比如说一张博客由四部分组成,user,content,homepage,isLiked,isLiked的构建依赖于user的查询操作,所以我们需要查询完user之后再去进行isLiked的判断,这样我们并行+异步的方式完成博客的构建工作。相当于把原来串行化的博客加载变成了并行加载。同时为了避免超时的问题,我使用completeOnTimeout方法,超时之后返回响应超时的结果给前端、
R
使用ApiFox对性能进行测试,开启了100个并发用户数,对每个接口两分钟内分别发了大概6000多个请求,优化前的平均响应时间在30ms左右,优化后的平均响应时间在16ms左右。最大响应时间方面,优化前是8000ms,优化后是1600ms。本地写了一个单元测试方法,对每个接口分别执行500次并且计算用时,优化前用时2200ms,优化后用时1600ms。
点赞和关注的设计
S
一个用户可能会点赞多张帖子,你在前端渲染的时候,应该判断一个用户是否对某个帖子点赞过,这个要求比较快做到,一次下拉会产生十几张帖子,如果每张帖子都去db里面查,效率会比较低。 对于关注,也可以同理做到。但是对于被关注呢?一个大V可能有十几万的粉丝,如果存太多value,会存在大key的问题。同时大v的关注量很大,应该怎么解决?
T
对于大v,我们需要快速的获取它的粉丝数,但并不需要精确的获取得到所有粉丝的id,可以采取分步刷新的办法
A
我们用HyperLogLog存储大V的粉丝,可以大概知道一个大V的粉丝量。用SortedSet存储最近关注大V的粉丝,score就用时间戳,然后每次添加的时候淘汰掉那些关注的比较晚的。这样可以保证key的size在可控范围内
R
有一百个用户同时点赞该怎么办?
这就是一个并发问题
选择Lua脚本+消息队列解决,对于一个点赞,我们要做四步操作:
- 在redis中对相关的key 进行i++操作
- 将该帖子添加进入当前用户的点赞列表中
- 在数据库中对相关帖子进行i++
- 在数据库中插入对应的点赞记录
前两步操作,我们使用lua脚本即可完成,保证这两步操作的原子性
对于后两步操作,会比较耗时,我们可以用rabbitMQ异步处理完成,添加到一个队列中,按顺序流处理。
排行榜的设计
S
我们用HyperLogLog存储大V的粉丝,可以大概知道一个大V的粉丝量。用SortedSet存储最近关注大V的粉丝,score就用时间戳,然后每次添加的时候淘汰掉那些关注的比较晚的。这样可以保证key的size在可控范围内
T
我目前的想法是,redis结合mysql。制定一个定时任务,每一个小时去数据库里遍历这24小时内的访问记录,并用hashmap记录好每个帖子的访问数量。算出访问量前十的帖子之后,存入redis的sortedSet中,然后把这个结果同步到jvm缓存中。
A
R
Q
可能存在的问题:1.24小时内的数据量过大,要完成这24小时内的数据清洗会比较麻烦,可能需要用一些大数据处理的组件。2.对于分布式场景,数据同步到caffeine会是一个问题,caffeine可以设置一个一小时过期时间,过期之后触发回调函数,过期之后再去redis中获取到最新的十条热点帖子。
另外的优化:
使用一个sortedset,即可实现用blogView作为key,blogid作为value,浏览次数作为score,每一次访问都对score++。每过一小时,就获取到score值最大的10个blog,然后把它存入caffeine中。同时把这个key清空。
缺点:如果帖子过多,可能存在大key问题,会有大key阻塞问题。
优点:相比与mysql存日志版本,显然获取数据的速度要更快
如何实现一个实时的热榜
上面说的是一个定时热榜,如果想实现一个实时热榜该怎么办?
我的想法是利用redis实现一个滑动窗口
这个滑动窗口,记录的是最近一小时内的浏览记录,然后根据这个浏览记录算出访问次数最多的帖子。
具体实现为:用userid和blogid凭借成一个新的字符串作为value,或者存一个BlogRecord对象(userid,blogid)。时间戳作为score
每次插入的时候,不是简单的add,而是用lua脚本,因为一次插入包含多个redis操作
- 插入现在这个value
- 淘汰掉过期的value
秒杀
秒杀的基本逻辑
A
用户的优惠卷请求进来之后,会调用一个lua脚本,在这个lua脚本里面,我会对它进行资格的校验,判断它是否下过单,是否有下单资格,以及现在是否库存充足。上述上个条件都满足的话,在redis上进行库存的扣减。扣减完成之后,给消息队列发送消息,异步的在mysql层面完成库存的扣减和订单的创建。
对账一致性
redis扣减库存成功,mysql扣减库存失败
S
因为一些原因,比如说redis中扣减了库存,但是异步去mysql里面扣减库存和创建订单失败了。那么就会出现redis中库存少于mysql的情形,同时也面临用户下单之后,查询不到自己的订单的问题。
T
A
对于这个场景,我们可以使用rabbitmq的手动消息确认机制,异步mysql成功扣减库存,创建订单之后,分别对对应的消息进行一个确认。这样的话,如果mysql层面失败了,那就会不停的进行一个重试操作。直到成功完成库存扣减和订单的创建。
全部评论
(39) 回帖