黑马点评
一、短信登陆功能
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.缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
- 缓存空对象 优点:实现简单,维护方便 缺点:额外的内存消耗,可能造成短期的不一致适合命中不高,但可能被频繁更新的数据
- 布隆过滤 优点:内存占用较少,没有多余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都属于悲观锁
实现乐观锁主要有以下两种方法:
- 版本号法
每次更新数据库的时候按照版本查询,并且要更新版本。
- 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属性)
实现步骤:
- 给Blog类中添加一个isLike字段,标示是否被当前用户点赞
- 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1
- 修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
- 修改分页查询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();}
版权归原作者 wazouu 所有, 如有侵权,请联系我们删除。