0


黑马点评项目总结

黑马点评

一、短信登陆功能

1.基于session实现

在这里插入图片描述

2.基于session实现登陆的问题

单体应用时用户的会话信息保存在session中,session存在于服务器端的内存中,由于前前后后用户只针对一个web服务器,所以没啥问题。但是一到了

web服务器集群的环境下

(我们一般都是用Nginx做负载均衡,若是使用了轮询等这种请求分配策略),就会导致用户小a在A服务器登录了,session存在于A服务器中,但是第二次请求被分配到了B服务器,由于B服务器中没有用户小a的session会话,导致用户小a还要再登陆一次,以此类推。这样用户体验很不好。当然解决办法也有很多种,比如同一个用户分配到同一个服务处理、使用cookie保持用户会话信息等。
因此,要解决这样的问题必须满足以下条件:

  • 数据共享
  • 内存存储
  • key、value结构

3.基于redis实现短信登陆

在这里插入图片描述

发送验证码:

/**
 * 发送手机验证码
 */@PostMapping("code")publicResultsendCode(@RequestParam("phone")String phone,HttpSession session){return userService.sendCode(phone,session);}@OverridepublicResultsendCode(String phone,HttpSession session){//1.校验手机号if(RegexUtils.isPhoneInvalid(phone)){//2.如果不符合,返回错误信息returnResult.fail("手机号格式错误!");}//3.符合则生成验证码finalString code =RandomUtil.randomNumbers(6);//4.保存验证码到redis
    stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL,TimeUnit.MINUTES);//5.发送验证码
    log.debug("发送短信验证码成功,验证码:{}",code);//6.返回nullreturnResult.ok();}

验证登陆功能:
login方法会把生成的token返回给前端,浏览器会将其保存到session中。

/**
 * 登录功能
 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
 */@PostMapping("/login")publicResultlogin(@RequestBodyLoginFormDTO loginForm,HttpSession session){return userService.login(loginForm,session);}@OverridepublicResultlogin(LoginFormDTO loginForm,HttpSession session){//1.校验手机号finalString phone = loginForm.getPhone();if(RegexUtils.isPhoneInvalid(phone)){//2.如果不符合,返回错误信息returnResult.fail("手机号格式错误!");}//2.校验验证码,从redis中获取finalString cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);finalString code = loginForm.getCode();if(cacheCode==null||!cacheCode.equals(code)){//3.不一直,报错returnResult.fail("验证码错误");}//4.一致,根据手机号查询用户User user =query().eq("phone", phone).one();//5.判断用户是否存在if(user ==null){//6.不存在,创建新用户并保存
        user =createUserWithPhone(phone);}//7.保存用户信息到redis中//7.1随机生成token,作为登陆令牌String token = UUID.randomUUID().toString(true);//7.2将User对象转为HashMap存储UserDTO userDTO =BeanUtil.copyProperties(user,UserDTO.class);finalMap<String,Object> map =BeanUtil.beanToMap(userDTO,newHashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)->{return fieldValue.toString();}));//7.3存储
    stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,map);//7.4设置token有效期
    stringRedisTemplate.expire(LOGIN_USER_KEY+token,3000,TimeUnit.MINUTES);//8.返回tokenreturnResult.ok(token);}privateUsercreateUserWithPhone(String phone){User user =newUser();
    user.setPhone(phone);
    user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(5));save(user);return user;}

这里使用redis的

hash结构

存储user信息,原因是:

  • 若使用String结构,以JSON字符串来保存,比较直观
  • 但Hash结构可以将对象中的每个字段独立存储,可以针对单个字段做CRUD,并且内存占用更少

在这里插入图片描述
拦截器:

  • 首先,对于每个请求,我们首先根据token判断用户是否已经登陆(是否已经保存到ThreadLocal中),如果没有登陆,放行交给登陆拦截器去做,如果已经登陆,刷新token的有效期,然后放行。
  • 之后来到登陆拦截器,如果ThreadLocal没有用户,说明没有登陆,拦截,否则放行。在这里插入图片描述

定义UserHolder工具类:

publicclassUserHolder{privatestaticfinalThreadLocal<UserDTO> tl =newThreadLocal<>();publicstaticvoidsaveUser(UserDTO user){
        tl.set(user);}publicstaticUserDTOgetUser(){return tl.get();}publicstaticvoidremoveUser(){
        tl.remove();}}

刷新token拦截器:

@Slf4jpublicclassRefreshTokenInterceptorimplementsHandlerInterceptor{privateStringRedisTemplate stringRedisTemplate;publicRefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){this.stringRedisTemplate = stringRedisTemplate;}@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{//1.获取请求头中的tokenfinalString token = request.getHeader("authorization");if(token ==null){returntrue;}//2.获取redis中的用户finalMap<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);//3.判断用户是否存在if(userMap.isEmpty()){returntrue;}//5.将查询到的Hash数据转换为UserDto对象finalUserDTO userDTO =BeanUtil.fillBeanWithMap(userMap,newUserDTO(),false);//6.存在,保存用户信息到ThreadLocalUserHolder.saveUser(userDTO);//7.刷新token有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,3000,TimeUnit.MINUTES);//8.放行returntrue;}@OverridepublicvoidafterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex)throwsException{UserHolder.removeUser();}}

登陆拦截器:

publicclassLoginInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequest request,HttpServletResponse response,Object handler)throwsException{//1.判断是否需要拦截(ThreadLocal中是否有用户)if(UserHolder.getUser()==null){
            response.setStatus(401);returnfalse;}//8.放行returntrue;}@OverridepublicvoidafterCompletion(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex)throwsException{UserHolder.removeUser();}}

在配置类中配置拦截器:

@ConfigurationpublicclassMvcConfigimplementsWebMvcConfigurer{@ResourceprivateStringRedisTemplate stringRedisTemplate;@OverridepublicvoidaddInterceptors(InterceptorRegistry registry){//登陆拦截器
        registry.addInterceptor(newLoginInterceptor()).excludePathPatterns("/user/code","/user/login","/blog/hot","/shop/**","/shop-type/**","/upload/**","/voucher/**").order(1);//token属性的拦截器
        registry.addInterceptor(newRefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}}

4.补充ThreadLocal相关知识

a.ThreadLocal的数据结构

在这里插入图片描述

  • Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。
  • ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。
  • 每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
  • ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
  • 我们还要注意Entry, 它的key是ThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。

b.内存泄露问题

由于ThreadLocal的key是

弱引用

,故在

gc

时,key会被回收掉,但是value是

强引用

没有被回收,所以在我们拦截器的方法里必须手动remove()。

二、redis缓存

1.选择缓存更新策略

在这里插入图片描述
项目选择了主动更新策略,相对较好,主动更新又有以下三种方式:
在这里插入图片描述
选择在更新数据库的同时更新缓存。
操作缓存和数据库时有三个问题需要考虑:

  • 删除缓存还是更新缓存?更新缓存:每次更新数据库都更新缓存,无效写操作较多删除缓存:更新数据库时让缓存失效,查询时再更新缓存
  • 如何保证缓存与数据库的操作的同时成功或失败? 单体系统,将缓存与数据库操作放在一个事务 分布式系统,利用TCC等分布式事务方案
  • 先操作缓存还是先操作数据库?

若先删除缓存,再操作数据库:

请求1先把缓存中的A数据删除,请求2从db中读数据,请求1再把db中的A更新

若先操作数据库,再删除缓存:

请求1从db中读取数据A,请求2随后更新db中的数据(缓存中由于没有数据,所以不需要删除),最后请求1更新缓存。

可以看出两种方法都有各自的问题,但是由于写的时间要远大于读的时间,所以先操作db再删除cache的出现问题的几率非常小。

2.业务逻辑

  • 根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间
  • 根据id修改店铺时,先修改数据库,再删除缓存

3.缓存存在的问题

a.缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:

  1. 缓存空对象 优点:实现简单,维护方便 缺点:额外的内存消耗,可能造成短期的不一致适合命中不高,但可能被频繁更新的数据
  2. 布隆过滤 优点:内存占用较少,没有多余key 缺点:实现复杂,存在误判可能适合命中不高,但是更新不频繁的数据在这里插入图片描述 解决方案:在这里插入图片描述
/**
* 缓存穿透方法
 * @param id
 * @return
 */public<R,ID>RqueryWithPassThrough(String keyPrefix,ID id,Class<R> type,Long time,TimeUnit unit,Function<ID,R> dbFallback){String key = keyPrefix+id;//1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);//2.判断是否存在if(StrUtil.isNotBlank(json)){//3.存在,直接返回returnJSONUtil.toBean(json, type);}//命中的是否是空值if(json !=null){returnnull;}//4.不存在,根据id查询数据库R r = dbFallback.apply(id);//5.不存在,返回错误if(r==null){//将空值写入reddis
        stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);returnnull;}//6.存在,写入redisthis.set(key,r,time,unit);//7.返回return r;}

b.缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

c.缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4.基于逻辑过期解决缓存击穿问题

在这里插入图片描述

privatestaticfinalExecutorService CACHE_REBUILD_EXECUTOR =Executors.newFixedThreadPool(10);/**
 * 逻辑过期解决缓存击穿
 * @param id
 * @return
 */public<R,ID>RqueryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Long time,TimeUnit unit,Function<ID,R> dbFallback){String key = keyPrefix+id;//1.从redis查询商铺缓存String json = stringRedisTemplate.opsForValue().get(key);//2.判断是否存在if(StrUtil.isBlank(json)){//3.不存在,直接返回returnnull;}//4.命中,先把json反序列化RedisData redisData =JSONUtil.toBean(json,RedisData.class);JSONObject data =(JSONObject) redisData.getData();R r =JSONUtil.toBean(data, type);LocalDateTime expireTime = redisData.getExpireTime();//5.判断是否过期if(expireTime.isAfter(LocalDateTime.now())){//5.1未过期,直接返回return r;}//5.2已过期,需要缓存重建//6.缓存重建//6.1获取互斥锁String lockkey = LOCK_SHOP_KEY + id;boolean lock =tryLock(lockkey);//6.2判断是否获取锁成功if(lock){//6.3成功,开启独立线程,实现缓存重建
        CACHE_REBUILD_EXECUTOR.submit(()->{try{//查询数据库R r1 = dbFallback.apply(id);//写入redisthis.setWithLogicalExpire(key,r1,time,unit);}catch(Exception e){
                e.printStackTrace();}finally{//释放锁unlock(lockkey);}});}//6.4返回商铺信息return r;}

三、优惠券秒杀

1.优惠券秒杀下单

一般流程:
在这里插入图片描述

2.超卖问题

请求a查询库存,发现库存为1,请求b这时也来查询库存,库存也为1,然后请求a让数据库减1,这时候b查询到的仍然是1,也继续让库存减1,就会导致超卖。

超卖问题有以下几个解决方案:

  • 乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。如果没有修改则认为是安全的,自己才更新数据。如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。
  • 悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。例如Synchronized、Lock都属于悲观锁

实现乐观锁主要有以下两种方法:

  1. 版本号法

每次更新数据库的时候按照版本查询,并且要更新版本。
在这里插入图片描述

  1. CAS

CAS是英文单词

Compare And Swap

的缩写,翻译过来就是比较并替换
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
在这里插入图片描述

CAS的缺点:

1.CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

2.不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

3.一人一单功能

要求同一个优惠券,一个用户只能下一单

在这里插入图片描述
这样的方式会产生并发安全问题:
在这里插入图片描述

通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了(每个jvm都有自己的锁监视器,集群模式下各个服务器的锁不共享)。
因此,我们的解决方案就是实现一个共享的锁监视器,即:
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

4.基于redis的分布式锁

a.setnx命令

setnx = SET if Not eXists

  • 将 key 的值设为 value ,当且仅当 key 不存在。
  • 若给定的 key 已经存在,则 SETNX 不做任何动作

在这里插入图片描述

b.普通setnx分布式锁出现的问题

在某个线程获取锁执行业务时若发生阻塞,且阻塞过程中锁超时,此时另一个线程同样来请求锁,发现可以获取锁,但实际上前一个线程还没执行完。

解决方案:

  • 在获取锁时存入线程标示(可以用UUID表示)
  • 在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
  • 如果一致则释放锁
  • 如果不一致则不释放锁

在这里插入图片描述

四、消息队列优化

(三四章先占坑)

五、达人探店

1.发布探店笔记

简单的crud
在这里插入图片描述

2.实现点赞功能

需求:

  • 同一个用户只能点赞一次,再次点击则取消点赞
  • 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)

实现步骤:

  1. 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
  2. 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1
  3. 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
  4. 修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段

3.点赞排行榜

需求:按照点赞时间先后排序,返回Top5的用户
使用SortedSet:

  • 通过 ZSCORE 命令获取 SortedSet 中存储的元素的相关的 SCORE 值。
  • 通过 ZRANGE 命令获取指定范围内的元素。

在这里插入图片描述
完整代码:
BlogController

@RestController@RequestMapping("/blog")publicclassBlogController{@ResourceprivateIBlogService blogService;@PutMapping("/like/{id}")publicResultlikeBlog(@PathVariable("id")Long id){return blogService.likeBlog(id);}@GetMapping("/hot")publicResultqueryHotBlog(@RequestParam(value ="current", defaultValue ="1")Integer current){return blogService.queryHotBlog(current);}@GetMapping("/{id}")publicResultqueryBlogById(@PathVariable("id")String id){return blogService.queryBlogById(id);}@GetMapping("/likes/{id}")publicResultqueryBlogLikes(@PathVariable("id")String id){return blogService.queryBlogLikes(id);}}

IBlogService

publicinterfaceIBlogServiceextendsIService<Blog>{ResultqueryBlogById(String id);ResultqueryHotBlog(Integer current);ResultlikeBlog(Long id);ResultqueryBlogLikes(String id);}

BlogServiceImpl

ServicepublicclassBlogServiceImplextendsServiceImpl<BlogMapper,Blog>implementsIBlogService{@AutowiredprivateIUserService userService;@AutowiredprivateStringRedisTemplate stringRedisTemplate;@OverridepublicResultqueryHotBlog(Integer current){// 根据用户查询Page<Blog> page =query().orderByDesc("liked").page(newPage<>(current,SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();// 查询用户
        records.forEach(blog ->{this.queryBlogUser(blog);this.isBlogLiked(blog);});returnResult.ok(records);}@OverridepublicResultlikeBlog(Long id){// 1、获取登录用户UserDTO user =UserHolder.getUser();// 2、判断当前登录用户是否已经点赞Double score = stringRedisTemplate.opsForZSet().score(RedisConstants.BLOG_LIKED_KEY + id, user.getId().toString());if(score ==null){// 3、如果未点赞,可以点赞// 3.1、数据库点赞数 +1boolean isSuccess =update().setSql("liked = liked+1").eq("id", id).update();// 3.2、保存用户到 Redis 的 set 集合if(isSuccess){// 时间作为 key 的 score
                stringRedisTemplate.opsForZSet().add(RedisConstants.BLOG_LIKED_KEY + id, user.getId().toString(),System.currentTimeMillis());}}else{// 4、如果已点赞,取消点赞// 4.1、数据库点赞数 -1boolean isSuccess =update().setSql("liked = liked - 1").eq("id", id).update();// 4.2、把用户从 Redis 的 set 集合移除if(isSuccess){
                stringRedisTemplate.opsForZSet().remove(RedisConstants.BLOG_LIKED_KEY + id, user.getId().toString());}}returnResult.ok();}@OverridepublicResultqueryBlogLikes(String id){String key =RedisConstants.BLOG_LIKED_KEY + id;// 查询 top5 的点赞用户Set<String> top5 = stringRedisTemplate.opsForZSet().range(key,0,4);if(top5 ==null){returnResult.ok(Collections.emptyList());}// 解析出其中的用户idList<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());String join =StrUtil.join(",", ids);// 根据用户id查询用户List<UserDTO> userDTOS = userService.query().in("id", ids).last("order by filed(id, "+join+")").list().stream().map(user ->BeanUtil.copyProperties(user,UserDTO.class)).collect(Collectors.toList());returnResult.ok(userDTOS);}privatevoidqueryBlogUser(Blog blog){Long userId = blog.getUserId();User user = userService.getById(userId);
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());}@OverridepublicResultqueryBlogById(String id){Blog blog =getById(id);if(blog ==null){returnResult.fail("笔记不存在!");}queryBlogUser(blog);// 查询 Blog 是否被点赞isBlogLiked(blog);returnResult.ok(blog);}privatevoidisBlogLiked(Blog blog){UserDTO user =UserHolder.getUser();if(user ==null){return;}Long userId = user.getId();String key =RedisConstants.BLOG_LIKED_KEY + blog.getId();Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
        blog.setIsLike(score !=null);}}

六、好友关注

1.关注和取关

在这里插入图片描述

基于mysql实现:
在这里插入图片描述
基于redis实现:
设置一个给每个用户设置一个key,利用set结构存储关注该用户的人。
代码:

@PutMapping("/{id}/{isFollow}")publicResultfollow(@PathVariable("id")Long followUserId,@PathVariable("isFollow")Boolean isFollow){return followService.follow(followUserId,isFollow);}@OverridepublicResultfollow(Long followUserId,Boolean isFollow){Long userId =UserHolder.getUser().getId();String key ="follows:"+ userId;//1.判断关注还是取关if(isFollow){//2.关注,新增数据Follow follow =newFollow();
        follow.setUserId(userId);
        follow.setFollowUserId(followUserId);boolean success =save(follow);if(success){//把关注用户的id,放入redis的set集合
            stringRedisTemplate.opsForSet().add(key,followUserId.toString());}}else{//3.取关,删除数据boolean success =remove(newQueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", followUserId));//把关注的用户id从redis集合中移除if(success)  stringRedisTemplate.opsForSet().remove(key,followUserId.toString());}returnResult.ok();}
@GetMapping("/or/not/{id}")publicResultisFollow(@PathVariable("id")Long followUserId){return followService.isFollow(followUserId);}@OverridepublicResultisFollow(Long followUserId){Long userId =UserHolder.getUser().getId();//1.查询是否关注Integer count =query().eq("user_id", userId).eq("follow_user_id", followUserId).count();returnResult.ok(count>0);}

2.共同关注

在这里插入图片描述
在关注点击关注用户时,用redis的set结构存储自己关注了哪些用户,然后利用集合的交集就能轻松求出共同关注的用户了。

@GetMapping("/common/{id}")publicResultfollowCommons(@PathVariable("id")Long id){return followService.followCommons(id);}@OverridepublicResultfollowCommons(Long id){//1.获取当前用户Long userId =UserHolder.getUser().getId();String key ="follows:"+ userId;//2.求交集String key2 ="follows:"+ id;Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);if(intersect==null||intersect.isEmpty()){//无交集returnResult.ok(Collections.emptyList());}//3.解析id集合List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());//4.查询用户List<UserDTO> users = userService.listByIds(ids).stream().map(user ->BeanUtil.copyProperties(user,UserDTO.class)).collect(Collectors.toList());returnResult.ok(users);}

3.关注推送

在这里插入图片描述

Feed流产品有两种常见模式:

  • Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈 优点:信息全面,不会有缺失。并且实现也相对简单 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
  • 智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷 缺点:如果算法不精准,可能起到反作用

本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:

  • 拉模式
  • 推模式
  • 推拉结合在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

这里基于推模式模式实现关注推送,需求:

  • 修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱
  • 收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
  • 查询收件箱数据时,可以实现分页查询在这里插入图片描述

4.实现推送功能

推送:

@PostMappingpublicResultsaveBlog(@RequestBodyBlog blog){return blogService.saveBlog(blog);}@OverridepublicResultsaveBlog(Blog blog){// 1.获取登录用户UserDTO user =UserHolder.getUser();
    blog.setUserId(user.getId());// 2.保存探店博文boolean success =this.save(blog);if(!success)returnResult.fail("新增笔记失败!");//3.查询笔记作者的所有粉丝List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();//4.推送笔记id给有所粉丝for(Follow follow : follows){//4.1获取粉丝idLong followId = follow.getUserId();//4.2推送String key = FEED_KEY+followId;
        stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());}// 返回idreturnResult.ok(blog.getId());}

读取:

@GetMapping("/of/follow")publicResultqueryBlogOfFollow(@RequestParam("lastId")Long max,@RequestParam(value ="offset",defaultValue ="0")Integer offset){return blogService.queryBlogOfFollow(max,offset);}@OverridepublicResultqueryBlogOfFollow(Long max,Integer offset){//1.获取当前用户Long userId =UserHolder.getUser().getId();//2.查询收件箱String key = FEED_KEY + userId;Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key,0, max, offset,2);if(typedTuples==null||typedTuples.isEmpty())returnResult.ok();//3.解析数据:blogId,minTime(时间戳),offsetList<Long> ids =newArrayList<>(typedTuples.size());long minTime =0;int os =1;for(ZSetOperations.TypedTuple<String> tuple : typedTuples){//3.1查询idString idStr = tuple.getValue();
        ids.add(Long.valueOf(idStr));//4.2获取分数(时间戳)if(tuple.getScore().longValue()==minTime){
            os++;}else os  =1;
        minTime = tuple.getScore().longValue();}//4.根据id查询blogString idStr =StrUtil.join(",", ids);List<Blog> blogs =query().in("id",ids).last("ORDER BY FIELD(ID,"+idStr+")").list();for(Blog blog : blogs){queryBlogUser(blog);isBlogLiked(blog);}//5.封装并返回ScrollResult r =newScrollResult();
    r.setList(blogs);
    r.setOffset(os);
    r.setMinTime(minTime);returnResult.ok(r);}

七、签到功能

1.数据库实现

在这里插入图片描述

2.redis实现

在这里插入图片描述

3.具体代码

@PostMapping("/sign")publicResultsign(){return userService.sign();}@OverridepublicResultsign(){//1.获取当前用户Long userId =UserHolder.getUser().getId();//2.获取日期LocalDateTime now =LocalDateTime.now();//3.拼接keyString keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));String key = USER_SIGN_KEY + userId + keySuffix;//4.获取今天是本月的第几天int dayOfMonth = now.getDayOfMonth()-1;//5.写入redis
     stringRedisTemplate.opsForValue().setBit(key,dayOfMonth,true);returnResult.ok();}

本文转载自: https://blog.csdn.net/qq_45733304/article/details/126443684
版权归原作者 wazouu 所有, 如有侵权,请联系我们删除。

“黑马点评项目总结”的评论:

还没有评论