# 点评网 **Repository Path**: luckydog-tjs/dianping ## Basic Information - **Project Name**: 点评网 - **Description**: 点评网是一个前后端分离项目,类似于大众点评。实现了发布查看商家,达人探店,点赞,关注等功能,平台可以为商家增加曝光度来提升流量,为用户提供查看附件消费场所,场所评价以及抢购优惠券等功能。 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2023-02-12 - **Last Updated**: 2025-01-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 登录Session共享 问题: 每个tomcat中都有一份属于自己的session,假设用户第一次访问第一台tomcat,并且把自己的信息存放到第一台服务器的session中,但是第二次这个用户访问到了第二台tomcat,那么在第二台服务器上,肯定没有第一台服务器存放的session,所以此时 整个登录拦截功能就会出现问题,我们能如何解决这个问题呢?早期的方案是session拷贝,就是说虽然每个tomcat上都有不同的session,但是每当任意一台服务器的session修改时,都会同步给其他的Tomcat服务器的session,这样的话,就可以实现session的共享了 但是这种方案具有两个大问题 1、**每台服务器中都有完整的一份session数据,服务器压力过大。** 2、session拷贝数据时,可能会出现延迟 所以咱们后来采用的方案都是基于redis来完成,我们把session换成redis,redis数据本身就是共享的,就可以避免session共享的问题了 发送验证码:验证码以**phone:验证码** key-value方式保存到Redis。 登录: 1. 根据phone从Redis中获取验证码,然后对比验证码。 2. 验证码正确,根据phone查询用户。不存在就创建 3. 使用Hash方式存储用户。Key:token,value:以Map形式存用户属性。 1. 1. token设置有效期 2. token通过UUID随机生成 1. 将token返回给前端。 登录刷新问题: ![img](img/1679413023145-d267b03d-741b-48fa-9e19-c8e805a3a926.png) # 商户缓存 商户店铺店铺信息,变化不大。 ![img](img/1679413169073-7d4a4702-6712-4974-b537-407ea26dee0e.png) 缓存更新 - 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间 - 根据id修改店铺时,先修改数据库,再删除缓存。【延迟双删】 缓存穿透:用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力 - 缓存空对象 - 布隆过滤器 - 增强id的复杂度,避免被猜测id规律 - 做好热点参数的限流 缓存雪崩:大量Key过期/Redis宕机 - Key设置随机值 - 分布式锁构建缓存。**保证同一时间内只有一个请求来构建缓存** - 高可用集群 缓存击穿:热点Key失效 - 互斥锁:使用互斥锁构建缓存,构建结束释放锁。期间请求被阻塞。 - 逻辑过期:检查过期时间,如果过期,使用互斥锁开个线程构建缓存。子线程结束才释放锁,期间的请求返回过期数据。 # 优惠券秒杀 秒杀接口:**/voucher-order/seckill/{id}** 保存秒杀券的时候,同时存一份到Redis中。或者在项目启动时,进行缓存预热。 优惠券秒杀: 1. 生成订单Id 2. 获取用户Id 3. 执行Lua脚本【保证操作的原子性】 1. 1. 传入 **用户券Id、用户Id、订单Id。** 2. 判断库存是否充足。不足返回1 3. 用户是否下过单。已经下过单返回2 4. 扣库存,**redis.call('incrby', stockKey, -1)** 5. 下单,**redis.call('sadd', orderKey, userId)** **set集合,Key是订单Id,Value是用户Id。** 6. 发送消息到消息队列中去。 1. 不具有资格就返回fail 2. 具有资格就返回**订单Id** 消息队列线程: 1. 获取消息队列中的消息【订单Id、用户Id、秒杀券Id】 2. 解析消息 3. 下单【**数据库事务**】 1. 1. 扣减库存 2. 保存订单 # 达人探店 **点赞维护Redis中的SortSet,笔记Id(blogId)作为key。用户Id作为次Key。score为点赞的时间戳。** 发布探店笔记: - 获取登录用户 - 图片上传,文字存数据库。 - 获取该用户所有粉丝 - 将博文推送到粉丝的收件箱。【SortSet Key:粉丝Id,Value:bolgId,socre:当前时间戳】 查看某个店铺探店笔记: - 查数据库,并根据blogId和userId查询该用户是否点赞过。 点赞: - **Key:blogId,Value:userId,score:时间戳** - 根据key,value查score - - score == null:没点赞过,则进行点赞。 - - - 更新数据库库,blog点赞数+1 - SortSet:添加 - - score != null:点赞过,则进行取消点赞。 - - - 更新数据库库,blog点赞数-1 - SortSet:删除 点赞排行榜: - 根据blogId获取SortSet。根据时间戳range查询前五的用户Id - 根据用户Id查数据库,返回用户集合。 # 好友关注 数据库表: ![img](img/1679467584782-5f0f05be-5d93-48c9-8abc-f6f950fdc58e.png) Redis: **Set集合:Key:用户Id。Value:关注的博主Id。** 关注/取关: - 传入被**关注者Id** 以及 **是否要关注**。 - 关注: - - 插入数据库 - 存Redis - 取关 - - 删除数据库记录 - 删除Redis记录 共同关注: - 传入博主Id,再获取用户Id - 分别查Redis 得到两个Set - 取交集:**redisTemplate.opsForSet().intersect(follow1, follow2);** - 根据用户Id集合 查数据库得到用户集合 返回前端。 ## Feed流 关注了用户后,这个用户发了动态,那么我们应该把这些数据推送给用户,这个需求,其实我们又把他叫做Feed流。 Feed流的实现有两种模式: Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈 - 优点:信息全面,不会有缺失。并且实现也相对简单 - 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户 - 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷 - 缺点:如果算法不精准,可能起到反作用 由于是基于关注的好友来做Feed流,因此采用Timeline的模式。实现方案有三种: - 拉模式:粉丝”主动“拉取 博主的博文。 - - 优点:节约空间。 - 缺点:比较延迟,如果用户量很大,此时拉取海量的内容,对服务压力巨大。 - 推模式:博主的博文主动推送给粉丝的”邮箱“,直接读即可。 - - 优点:时效快,不用临时拉取 - 缺点:内存压力大,假设一个大V写信息,很多人关注他, 就会写很多分数据到粉丝那边去 - 推拉结合: - - 推拉模式是一个折中的方案,站在发件人这一段,如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力,如果是大V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去,现在站在收件人这端来看,如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来,而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息。 数据结构:使用zset结构 **Key:用户Id****、****Value:关注博主的博文Id****、****Socred:时间戳** 博主发送探店笔记时,将探店笔记存起来,然后将该 探店笔记Id 存到其粉丝是zset中。 **滚动分页查询邮箱** 我们需要记录每次操作的最后一条,然后从这个位置开始去读取数据 举个例子:我们从t1时刻开始,拿第一页数据,拿到了10~6,然后记录下当前最后一次拿取的记录,就是6,t2时刻发布了新的记录,此时这个11放到最顶上,但是不会影响我们之前记录的6,此时t3时刻来拿第二页,第二页这个时候拿数据,还是从6后一点的5去拿,就拿到了5-1的记录。我们这个地方可以采用sortedSet来做,可以进行范围查询,并且还可以记录当前获取数据时间戳最小值,就可以实现滚动分页了 ![img](img/1680960396795-44148da6-2cd4-4285-866c-cf566f5cc1f7.png) ```java @Override public Result queryBlogOfFollow(Long max, Integer offset) { // 1.获取当前用户 Long userId = UserHolder.getUser().getId(); // 2.查询收件箱 ZREVRANGEBYSCORE key Max Min LIMIT offset count String key = FEED_KEY + userId; Set> typedTuples = stringRedisTemplate.opsForZSet() .reverseRangeByScoreWithScores(key, 0, max, offset, 2); // 3.非空判断 if (typedTuples == null || typedTuples.isEmpty()) { return Result.ok(); } // 4.解析数据:blogId、minTime(时间戳)、offset List ids = new ArrayList<>(typedTuples.size()); long minTime = 0; // 2 int os = 1; // 2 for (ZSetOperations.TypedTuple tuple : typedTuples) { // 5 4 4 2 2 // 4.1.获取id ids.add(Long.valueOf(tuple.getValue())); // 4.2.获取分数(时间戳) long time = tuple.getScore().longValue(); if(time == minTime){ os++; }else{ minTime = time; os = 1; } } os = minTime == max ? os : os + offset; // 5.根据id查询blog String idStr = StrUtil.join(",", ids); List blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list(); for (Blog blog : blogs) { // 5.1.查询blog有关的用户 queryBlogUser(blog); // 5.2.查询blog是否被点赞 isBlogLiked(blog); } // 6.封装并返回 ScrollResult r = new ScrollResult(); r.setList(blogs); r.setOffset(os); r.setMinTime(minTime); return Result.ok(r); } ``` # 用户签到 使用**bitMap Key:xxxx年xx月** **【写在登录逻辑中】签到:** 获取当前的第该月第几天 ```java stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true); ``` **签到统计:** 连续签到天数,从前向后遍历每个bit位。【数字和1做与运算】