当前位置: 首页 > news >正文

Redis从入门到实战——实战篇(下)

四、达人探店

1. 发布探店笔记

探店笔记类似于点评网站的评价,往往是图文结合。对应的表有两个:

  • tb_blog:探店笔记表,包含笔记中的标题、文字、图片等
  • tb_blog_comments:其他用户对探店笔记的评价

步骤①:根据个人情况修改路径

注:建议把nginx.conf文件里的负载均衡删了,重新加载配置

案例:实现查看发布探店笔记的接口

需求:点击首页的探店笔记,会进入详情页面,实现该页面的查询接口:

实现步骤:

①BlogController 

package com.hmdp.controller;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.service.IBlogService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;
import java.util.List;/*** <p>* 前端控制器* </p>** @author 虎哥* @since 2021-12-22*/
@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;// ... ...@GetMapping("/hot")public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {return blogService.queryHotBlog(current);}/*** 查询blog详情* @param id* @return*/@GetMapping("/{id}")public Result queryBlogById(@PathVariable("id") Long id) {return blogService.queryBlogById(id);}
}

②IBlogService 

package com.hmdp.service;import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.baomidou.mybatisplus.extension.service.IService;/*** <p>*  服务类* </p>** @author 虎哥* @since 2021-12-22*/
public interface IBlogService extends IService<Blog> {Result queryHotBlog(Integer current);Result queryBlogById(Long id);
}

③BlogServiceImpl 

package com.hmdp.service.impl;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.List;/*** <p>*  服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Overridepublic Result queryHotBlog(Integer current) {// 根据用户查询Page<Blog> page = query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();// 查询用户records.forEach(this::queryBlogUser);return Result.ok(records);}@Overridepublic Result queryBlogById(Long id) {// 1. 查询blogBlog blog = getById(id);if (blog == null) {return Result.fail("笔记不存在!");}// 2. 查询blog有关的用户queryBlogUser(blog);// 3. 结果返回return Result.ok(blog);}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}
}

效果:

2. 点赞

在首页的探店笔记排行榜和探店图文详情页都有点赞的功能:

案例:完善点赞功能

需求:

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

实现步骤:

①给Blog类中添加一个isLike字段,标示是否被当前用户点赞

②修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞则点赞数+1,已点赞则点赞数-1

③修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段

④修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段

①BlogController 

package com.hmdp.controller;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.service.IBlogService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;
import java.util.List;@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;@PutMapping("/like/{id}")public Result likeBlog(@PathVariable("id") Long id) {return blogService.likeBlog(id);}@GetMapping("/hot")public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {return blogService.queryHotBlog(current);}/*** 查询blog详情* @param id* @return*/@GetMapping("/{id}")public Result queryBlogById(@PathVariable("id") Long id) {return blogService.queryBlogById(id);}
}

②IBlogService 

package com.hmdp.service;import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.baomidou.mybatisplus.extension.service.IService;public interface IBlogService extends IService<Blog> {Result queryHotBlog(Integer current);Result queryBlogById(Long id);Result likeBlog(Long id);
}

③BlogServiceImpl 

package com.hmdp.service.impl;import cn.hutool.core.util.BooleanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.List;@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** 分页查询热点笔记* @param current* @return*/@Overridepublic Result queryHotBlog(Integer current) {// 根据用户查询Page<Blog> page = query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();// 查询用户和是否点赞records.forEach(blog -> {this.queryBlogUser(blog);this.isBlogLiked(blog);});return Result.ok(records);}/*** 查询笔记详情* @param id* @return*/@Overridepublic Result queryBlogById(Long id) {// 1. 查询blogBlog blog = getById(id);if (blog == null) {return Result.fail("笔记不存在!");}// 2. 查询blog有关的用户queryBlogUser(blog);// 3. 查询blog是否被点赞了isBlogLiked(blog);// 4. 结果返回return Result.ok(blog);}private void isBlogLiked(Blog blog) {// 1. 获取登录用户Long userId = UserHolder.getUser().getId();// 2. 判断当前登录用户是否已经点赞String key = RedisConstants.BLOG_LIKED_KEY + blog.getId();Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());blog.setIsLike(BooleanUtil.isTrue(isMember));}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}/*** 点赞或取消点赞笔记* @param id* @return*/@Overridepublic Result likeBlog(Long id) {// 1. 获取登录用户Long userId = UserHolder.getUser().getId();// 2. 判断当前登录用户是否已经点赞String key = RedisConstants.BLOG_LIKED_KEY + id;Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());if (BooleanUtil.isFalse(isMember)) {// 3. 如果未点赞,可以点赞// 3.1 数据库点赞数 + 1boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();// 3.2 保存用户到Redis的set集合if (isSuccess) {stringRedisTemplate.opsForSet().add(key, userId.toString());}} else {// 4. 如果已点赞,取消点赞// 4.1 数据库点赞数 - 1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();// 4.2 把用户从Redis的set集合移除if (isSuccess) {stringRedisTemplate.opsForSet().remove(key, userId.toString());}}// 5. 结果返回return Result.ok();}
}

注:@Resource按名称匹配,@Autowired按类型匹配

3. 点赞排行榜

在探店笔记的详情页面,应该把给该笔记点赞的人显示出来,比如最早点赞的TOP5,形成点赞排行榜:

ListSetSortedSet
排序方式按添加顺序排序无法排序根据score值排序
唯一性不唯一唯一唯一
查找方式按索引查找或首尾查找根据元素查找根据元素查找

①修改之前的点赞逻辑,以及笔记查询逻辑

package com.hmdp.service.impl;import cn.hutool.core.util.BooleanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import java.util.List;@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Resourceprivate StringRedisTemplate stringRedisTemplate;/*** 分页查询热点笔记* @param current* @return*/@Overridepublic Result queryHotBlog(Integer current) {// 根据用户查询Page<Blog> page = query().orderByDesc("liked").page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();// 查询用户和是否点赞records.forEach(blog -> {this.queryBlogUser(blog);this.isBlogLiked(blog);});return Result.ok(records);}/*** 查询笔记详情* @param id* @return*/@Overridepublic Result queryBlogById(Long id) {// 1. 查询blogBlog blog = getById(id);if (blog == null) {return Result.fail("笔记不存在!");}// 2. 查询blog有关的用户queryBlogUser(blog);// 3. 查询blog是否被点赞了isBlogLiked(blog);// 4. 结果返回return Result.ok(blog);}private void isBlogLiked(Blog blog) {// 1. 获取登录用户Long userId = UserHolder.getUser().getId();// 2. TODO 判断当前登录用户是否已经点赞String key = RedisConstants.BLOG_LIKED_KEY + blog.getId();Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());blog.setIsLike(score != null);}private void queryBlogUser(Blog blog) {Long userId = blog.getUserId();User user = userService.getById(userId);blog.setName(user.getNickName());blog.setIcon(user.getIcon());}/*** 点赞或取消点赞笔记* @param id* @return*/@Overridepublic Result likeBlog(Long id) {// 1. 获取登录用户Long userId = UserHolder.getUser().getId();// 2. TODO 判断当前登录用户是否已经点赞String key = RedisConstants.BLOG_LIKED_KEY + id;Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());if (score == null) {// 3. 如果未点赞,可以点赞// 3.1 数据库点赞数 + 1boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();// 3.2 TODO 保存用户到Redis的zset集合 zadd key value scoreif (isSuccess) {stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());}} else {// 4. 如果已点赞,取消点赞// 4.1 数据库点赞数 - 1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();// 4.2 把用户从Redis的zset集合移除if (isSuccess) {stringRedisTemplate.opsForZSet().remove(key, userId.toString());}}// 5. 结果返回return Result.ok();}
}

②BlogController

    /*** 查询笔记的点赞用户列表TOP5* @param id* @return*/@GetMapping("/likes/{id}")public Result queryBlogLikes(@PathVariable("id") Long id) {return blogService.queryBlogLikes(id);}

③IBlogService

Result queryBlogLikes(Long id);

④BlogServiceImpl

    /*** 查询笔记的点赞用户列表TOP5** @param id* @return*/@Overridepublic Result queryBlogLikes(Long id) {// 1. 查询top5的点赞用户 zrange key 0 4String key = RedisConstants.BLOG_LIKED_KEY + id;Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);if (top5 == null || top5.isEmpty()) {return Result.ok(Collections.emptyList());}// 2. 解析出其中的用户idList<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());String idStr = StrUtil.join(",", ids);// 3. 根据id查询用户List<UserDTO> userDTOS = userService.query().in("id", ids).last("ORDER BY FIELD(id, " + idStr + ")").list().stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());// 4. 返回return Result.ok(userDTOS);}

五、好友关注

1. 关注和取关

在探店图文的详情页面中,可以关注发布笔记的作者:

案例:实现关注和取关功能

需求:基于该表数据结构,实现两个接口:

  • 关注和取关接口
  • 判断是否关注的接口

关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示:

①FollowController 

package com.hmdp.controller;import com.hmdp.dto.Result;
import com.hmdp.service.IFollowService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;/*** <p>*  前端控制器* </p>** @author 虎哥* @since 2021-12-22*/
@RestController
@RequestMapping("/follow")
public class FollowController {@Autowiredprivate IFollowService followService;/*** 关注或取关* @param followUserId* @param isFollow* @return*/@PutMapping("/{id}/{isFollow}")public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") Boolean isFollow) {return followService.follow(followUserId, isFollow);}/*** 判断是否关注* @param followUserId* @return*/@GetMapping("/or/not/{id}")public Result isFollow(@PathVariable("id") Long followUserId) {return followService.isFollow(followUserId);}
}

②IFollowService 

package com.hmdp.service;import com.hmdp.dto.Result;
import com.hmdp.entity.Follow;
import com.baomidou.mybatisplus.extension.service.IService;/*** <p>*  服务类* </p>** @author 虎哥* @since 2021-12-22*/
public interface IFollowService extends IService<Follow> {Result follow(Long followUserId, Boolean isFollow);Result isFollow(Long followUserId);
}

③FollowServiceImpl 

package com.hmdp.service.impl;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.Follow;
import com.hmdp.mapper.FollowMapper;
import com.hmdp.service.IFollowService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;/*** <p>*  服务实现类* </p>** @author 虎哥* @since 2021-12-22*/
@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {/*** 关注或取关* @param followUserId* @param isFollow* @return*/@Overridepublic Result follow(Long followUserId, Boolean isFollow) {// 1. 获取登录用户UserDTO user = UserHolder.getUser();if (user == null) {return Result.fail("请先登录");}Long userId = user.getId();// 2. 判断是关注或是取关if (isFollow) {// 3. 关注,新增数据Follow follow = new Follow();follow.setUserId(userId);follow.setFollowUserId(followUserId);save(follow);} else {// 4. 取关,删除数据 delete from tb_follow where userId = ? and follow_user_id = ?remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", followUserId));}// 5. 结果返回return Result.ok();}/*** 判断是否关注* @param followUserId* @return*/@Overridepublic Result isFollow(Long followUserId) {// 1. 获取登录用户UserDTO user = UserHolder.getUser();if (user == null) {return Result.fail("请先登录");}Long userId = user.getId();// 2. 查询是否关注Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();// 3. 判断count是否大于0return Result.ok(count > 0);}
}

2. 共同关注

点击博主头像,可以进入博主首页:

①UserController

package com.hmdp.controller;import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.entity.UserInfo;
import com.hmdp.service.IUserInfoService;
import com.hmdp.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;
import javax.servlet.http.HttpSession;@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@Resourceprivate IUserService userService;@Resourceprivate IUserInfoService userInfoService;// ... .../*** 根据id查询用户* @param userId* @return*/@GetMapping("/{id}")public Result queryUserById(@PathVariable("id") Long userId) {// 1. 查询用户User user = userService.getById(userId);if (user == null) {return Result.ok();}// 2. 属性拷贝UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);// 3. 返回return Result.ok(userDTO);}}

②BlogController

package com.hmdp.controller;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.service.IBlogService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import org.springframework.web.bind.annotation.*;import javax.annotation.Resource;
import java.util.List;@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;// ... ...@GetMapping("/of/user")public Result queryBlogByUserId(@RequestParam(value = "current", defaultValue = "1") Integer current,@RequestParam("id") Long id) {// 1. 根据用户查询Page<Blog> page = blogService.query().eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 2. 获取当前页数据List<Blog> records = page.getRecords();// 3. 结果返回return Result.ok(records);}}

案例:实现共同关注功能

需求:利用Redis中恰当的数据结构,实现功能关注功能。在博主个人主页展示出当前用户与博主的共同好友。

①FollowController

    /*** 查询共同关注* @param id* @return*/@GetMapping("/common/{id}")public Result followCommons(@PathVariable("id") Long id) {return followService.followCommons(id);}

②IFollowService

Result followCommons(Long id);

③FollowServiceImpl

    /*** 查询共同关注* @param id* @return*/@Overridepublic Result followCommons(Long id) {// 1. 获取当前用户UserDTO user = UserHolder.getUser();if (user == null) {return Result.fail("请先登录");}Long userId = user.getId();String key = "follows:" + userId;// 2. 求交集String key2 = "follows:" + id;Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);if (intersect == null || intersect.isEmpty()) {return Result.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(u -> BeanUtil.copyProperties(u, UserDTO.class)).collect(Collectors.toList());// 5. 结果返回return Result.ok(users);}

3. 关注推送

关注推送也叫做Feed流,直译为投喂。为用户持续的提供”沉浸式“的体验,通过无限下拉刷新获取新的信息。

Feed流的模式

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

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

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

  • 拉模式:也叫做读扩散(内存占用较低,但延迟高)

  • 推模式:也叫写扩散(延迟较低,但内存占用高)

  • 推拉结合:也叫做读写混合,兼具推和拉两种模式的优点

拉模式推模式推拉结合
写比例
读比例
用户读取延迟
实现难点复杂简单很复杂
使用场景很少使用用户量少、没有大V过千万的用户量,有大V

案例:基于推模式实现关注推送功能

需求:

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

Feed流的分页问题

Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。

Feed流的滚动分页

实现步骤:

①BlogController

    @PostMappingpublic Result saveBlog(@RequestBody Blog blog) {return blogService.saveBlog(blog);}

②IBlogService

Result saveBlog(Blog blog);

③BlogServiceImpl

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

案例:实现关注推送页面的分页查询

需求:在个人主页的”关注“卡片中,查询并展示推送的Blog信息

滚动分页的查询参数:

  • max:当前时间戳 | 上一次查询的最小时间戳
  • min:0
  • offset:0 | 在上一次的结果中,与最小值一样的元素的个数
  • count:3(与前端约定)

①BlogController

    /*** 查询所关注博主的笔记* @param max* @param offset* @return*/@GetMapping("/of/follow")public Result queryBlogOfFollow(@RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset) {return blogService.queryBlogOfFollow(max, offset);}

②IBlogService

    Result queryBlogOfFollow(Long max, Integer offset);

③BlogServiceImpl

    /*** 查询所关注博主的笔记* @param max* @param offset* @return*/@Overridepublic Result queryBlogOfFollow(Long max, Integer offset) {// 1. 获取当前用户UserDTO user = UserHolder.getUser();if (user == null) {return Result.fail("请先登录");}Long userId = user.getId();// 2. 获取收件箱String key = RedisConstants.FEED_KEY + userId;// ZREVRANGEBYSCORE key Min Max Score LIMIT offset countSet<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 3);// 3. 非空判断if (typedTuples == null || typedTuples.isEmpty()) {return Result.ok();}// 4. 解析数据:blogId、minTime(时间戳)、offsetList<Long> ids = new ArrayList<>(typedTuples.size());long minTime = 0;int os = 1;for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {// 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;}}// 5. 根据blogId查询blogString idStr = StrUtil.join(",", ids);List<Blog> blogList = query().in("id", ids).last("ORDER BY FIELD(id, " + idStr + ")").list();for (Blog blog : blogList) {// 5.1 查询blog的作者queryBlogUser(blog);// 5.2 查询blog是否被当前用户点赞isBlogLiked(blog);}// 6. 封装返回ScrollResult result = new ScrollResult();result.setList(blogList);result.setOffset(os);result.setMinTime(minTime);return Result.ok(result);}

效果:

六、附近的商户

1. GEO数据结构

GEO就是Geolocation的简写形式,代表地理坐标。Redis在3.2版本中加入了对GEP的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。常见的命令有:

  • GEOADD:添加一个地理空间信息,包含:经度(longitude)、维度(latitude)、值(member)
  • GEODIST:计算指定的两个点之间的距离并返回
  • GEOHASH:将指定member的坐标转为hash字符串形式并返回
  • GEOPOS:返回指定member的坐标
  • GEORADIUS:指定圆心、半径,找到该园内包含的所有member,并按照与圆心之间的距离排序后返回。6.2以后已废弃
  • GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2新功能
  • GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。6.2新功能

案例:练习Redis的GEO功能

需求:

1. 添加下面几条数据

  • 北京南站(116.378248 39.865275)
  • 北京站(116.42803 39.903738)
  • 北京西站(116.322287 39.893729)

2. 计算北京南站到北京西站的距离

3. 搜索天安门(116.397904 39.909005)附近10km内的所有火车站,并按照距离升序排序

2. 附近商户搜索

在首页中点击某个频道,即可看到频道下的商户:

按照商户类型做分组,类型相同的商户作为同一组,以typeId为key存入同一个GEO集合中即可

package com.hmdp;import com.hmdp.entity.Shop;
import com.hmdp.service.IShopService;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisWorker;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.geo.Point;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;@SpringBootTest
class HmDianPingApplicationTests {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate IShopService shopService;@Testvoid loadShopData() {// 1. 查询店铺信息List<Shop> list = shopService.list();// 2. 按照商户类型分组 typeIdMap<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));// 3. 分批写入redisfor (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {// 3.1 获取类型idLong typeId = entry.getKey();// 3.2 获取同类型的店铺的集合List<Shop> value = entry.getValue();String key = RedisConstants.SHOP_GEO_KEY + typeId;List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());// 3.3 批量写入redis GEOADD key 经度 纬度 memberfor (Shop shop : value) {// redisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(),new Point(shop.getX(), shop.getY())));}redisTemplate.opsForGeo().add(key, locations);}}
}

①SpringDataRedis的2.3.9版本并不支持Redis6.2提供的GEOSEARCH命令,因此我们需要提高其版本,修改POM文件,内容如下:

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId></exclusion><exclusion><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-redis</artifactId><version>2.6.2</version></dependency><dependency><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId><version>6.1.6.RELEASE</version></dependency>

②ShopController

    /*** 根据商铺类型分页查询商铺信息* @param typeId 商铺类型* @param current 页码* @return 商铺列表*/@GetMapping("/of/type")public Result queryShopByType(@RequestParam("typeId") Integer typeId,@RequestParam(value = "current", defaultValue = "1") Integer current,@RequestParam(value = "x", required = false) Double x,@RequestParam(value = "y", required = false) Double y) {return shopService.queryShopByType(typeId, current, x, y);}

③IShopService

Result queryShopByType(Integer typeId, Integer current, Double x, Double y);

④ShopServiceImpl

    /*** 根据商铺名称关键字分页查询商铺信息* @param typeId* @param current* @param x* @param y* @return*/@Overridepublic Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {// 1. 判断是否需要根据坐标查询if (x == null || y == null) {// 不需要坐标查询,按照数据库查询Page<Shop> page = query().eq("type_id", typeId).page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));// 返回数据return Result.ok(page.getRecords());}// 2. 计算分页参数int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;int end = current * SystemConstants.DEFAULT_PAGE_SIZE;// 3. 查询redis、按照距离升序、分页、结果:shopId、distanceString key = RedisConstants.SHOP_GEO_KEY + typeId;GeoResults<RedisGeoCommands.GeoLocation<String>> results = redisTemplate.opsForGeo().search(key,GeoReference.fromCoordinate(x, y),new Distance(5000),RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));// 4. 解析出shopIdif (results == null ) {return Result.ok(Collections.emptyList());}List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();if (list.size() <= from ) {// 没有下一页了,结束return Result.ok(Collections.emptyList());}// 4.1 截取from - end的部分List<Long> ids = new ArrayList<>(list.size());Map<String, Distance> distanceMap = new HashMap<>(list.size());list.stream().skip(from).forEach(r -> {// 4.2 获取店铺idString shopIdStr = r.getContent().getName();ids.add(Long.valueOf(shopIdStr));// 4.3 获取距离Distance distance = r.getDistance();distanceMap.put(shopIdStr, distance);});// 5. 根据id查询shopString idStr = StrUtil.join(",", ids);List<Shop> shopList = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();for (Shop shop : shopList) {shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());}// 6. 结果返回return Result.ok(shopList);}

七、用户签到

1. BitMap用法

假如有1000万用户,平均每人每年签到次数为10次,则这张表一年的数据量为1亿条

每签到一次需要使用(8 + 8 + 1 + 1 + 3 + 1)共22字节的内存,一个月则最多需要600多字节

我们按月来统计用户签到信息,签到记录为1,未签到则记录为0

把每一个bit位对应当月的每一天,形成了映射关系。用0和1标识业务状态,这种思路就称为位图(BitMap)。Redis中是利用String类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是2^32个bit位。

BitMap的操作命令有:

  • SETBIT:向指定位置(offset)存入一个0或1

  • GETBIT:获取指定位置(offset)的bit值

  • BITCOUNT:统计BitMap中值为1的bit位的数量

  • BITFIELD:操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值

  • BITFIELD_RO:获取BitMap中bit数组,并以十进制形式返回(只读)
BITFIELD_RO mykey GET u4 0  # 只读方式获取位字段
  • BITOP:将多个BitMap的结果做位运算(与、或、异或)

  • BITPOS:查找bit数组中指定范围内第一个0或1出现的位置

2. 签到功能

案例:签到功能

需求:实现签到接口,将当前用户当天签到信息保存到Redis中

提示:因为BitMap底层是基于String数据结构,因此其操作也都封装在字符串相关操作中了。

①UserController

    /*** 用户签到* @return*/@PostMapping("/sign")public Result sign() {return userService.sign();}

②IUserService

    Result sign();

③UserServiceImpl

    /*** 用户签到* @return*/@Overridepublic Result sign() {// 1. 获取当前登录用户UserDTO user = UserHolder.getUser();if (user == null) {return Result.fail("请先登录");}Long userId = user.getId();// 2. 获取日期LocalDateTime now = LocalDateTime.now();// 3. 拼接keyString keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));String key = RedisConstants.USER_SIGN_KEY + userId + keySuffix;// 4. 获取今天是本月的第几天int dayOfMonth = now.getDayOfMonth();// 5. 写入redis SETBIT key offset 1redisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);// 6. 结果返回return Result.ok();}

3. 签到统计

问题1:什么是连续签到次数?

从最后一次签到开始向前统计,直到遇到第一次未签到为止,计算总的签到次数,就是连续签到天数。

问题2:如何得到本月到今天为止的所有签到数据?

BITFIELD key GET u[dayOfMonth] 0

问题3:如何从后向前遍历每个bit位?

与1做与运算,就能得到最后一个bit位。随后右移1位,下一个bit位就成为了最后一个bit位。

案例:实现签到统计功能

需求:实现下面接口,统计当前用户截止当前时间在本月的连续签到天数

①UserController

    /*** 连续签到天数* @return*/@GetMapping("/sign/count")public Result signCount() {return userService.signCount();}

②IUserService

    Result signCount();

③UserServiceImpl

    /*** 连续签到天数* @return*/@Overridepublic Result signCount() {// 1. 获取当前登录用户UserDTO user = UserHolder.getUser();if (user == null) {return Result.fail("请先登录");}Long userId = user.getId();// 2. 获取日期LocalDateTime now = LocalDateTime.now();// 3. 拼接keyString keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));String key = RedisConstants.USER_SIGN_KEY + userId + keySuffix;// 4. 获取今天是本月的第几天int dayOfMonth = now.getDayOfMonth();// 5. 获取本月截止今天为止的所有签到记录,返回的是一个十进制的数字List<Long> result = redisTemplate.opsForValue().bitField(key,BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0));if (result == null || result.isEmpty()) {// 没有任何签到结果return Result.ok(0);}Long num = result.get(0);if (num == null || num == 0) {return Result.ok(0);}// 6. 循环遍历int count = 0;while(true) {// 让这个数字与1做与运输,得到数字的最后一个bit位// 判断这个bit位是否是0if ((num & 1) == 0) {// 如果是0,说明未签到,结束break;} else {// 如果不为0,说明已签到,计数器加1count++;}// 把数字右移一位,抛弃最后一个bit位,继续下一个bit位num >>>= 1;}// 7. 结果返回return Result.ok(count);}

八、UV统计

1. HyperLogLog用法

  • UV:全称Unique Visitor,也叫独立访客量,是指通过互连网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
  • PV:全称Page View,也叫页面访问或点击量,用户每访问网站的一个页面,记录一次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。

UV统计在服务端做会比较麻烦,因为要判断该用户是否已经被统计过了,需要将统计过的用户信息保存。但是如果每个访问的用户都保存到Redis中,数据量会非常恐怖。

HyperLogLog(HLL)是从LogLog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。相关算法原理可以参考:HyperLogLog 算法的原理讲解以及 Redis 是如何应用它的聪明的你可能会马上想到,用 HashMap 这种数 - 掘金

Redis中的HLL是基于string结构实现的,单个HLL的内存永远小于16kb,内存占用低的令人发指!作为代价,其测量结果是概率性的,有小于0.81%的误差。不过对于UV统计来说,这完全可以忽略。

2. UV统计

我们直接利用单元测试,向HyperLogLog中添加100万条数据,看看内存占用和统计效果如何:

    @Testvoid testHyperLogLog() {// 准备数组,装用户数据String[] users = new String[1000];// 数组角标int index = 0;for (int i = 1; i <= 1000000; i++) {// 赋值users[index++] = "user_" + i;// 每1000条发送一次if (i % 1000 == 0) {index = 0;redisTemplate.opsForHyperLogLog().add("hll1", users);}}// 统计数量Long size = redisTemplate.opsForHyperLogLog().size("hll1");System.out.println("size = " + size);}

相关文章:

Redis从入门到实战——实战篇(下)

四、达人探店 1. 发布探店笔记 探店笔记类似于点评网站的评价&#xff0c;往往是图文结合。对应的表有两个&#xff1a; tb_blog&#xff1a;探店笔记表&#xff0c;包含笔记中的标题、文字、图片等tb_blog_comments&#xff1a;其他用户对探店笔记的评价 步骤①&#xff1…...

算法中的数学:质数(素数)

1.质数 1.1定义 一个大于1的自然数&#xff0c;除了1和它自身外&#xff0c;不能被其他自然数整除&#xff0c;那么他就是质数&#xff0c;否则他就是合数。 注意&#xff1a;1既不是质数也不是合数 唯一的偶质数是2&#xff0c;其余所有质数都是奇质数 1.2质数判定求法 试除法…...

linux、window安装部署nacos

本文以nacos 2.2.0为例 文章目录 1.下载安装包2.按需修改配置配置单机模式配置内存 -Xms -Xmx -Xmn配置数据库为MySQL 3. 访问http://ip:8848/nacos4.常见问题找不到javac命令 1.下载安装包 打开官网&#xff0c;下载2.2.0版本 2.按需修改配置 配置单机模式 默认集群模式&…...

C++ 外观模式详解

外观模式&#xff08;Facade Pattern&#xff09;是一种结构型设计模式&#xff0c;它为复杂的子系统提供一个简化的接口。 概念解析 外观模式的核心思想是&#xff1a; 简化接口&#xff1a;为复杂的子系统提供一个更简单、更统一的接口 降低耦合&#xff1a;减少客户端与子…...

42. 接雨水(相向双指针/前后缀分解),一篇文章讲透彻

给定一个数组&#xff0c;代表柱子的高度 求出下雨之后&#xff0c;能接的水有多少单位。我们将每一个柱子想象成一个水桶&#xff0c;看他能接多少水 以这个水桶为例&#xff0c;他所能接的水取决于左边的柱子的最大高度和右边柱子的最大高度&#xff0c;因为只有柱子高的时候…...

vue实现AI问答Markdown打字机效果

上线效果 功能清单 AI问答&#xff0c;文字输出跟随打字机效果格式化回答内容&#xff08;markdown格式&#xff09;停止回答&#xff0c;复制回答内容回答时自动向下滚动全屏切换历史问答查看 主要技术 vue 2.7.1markdown-it 14.1.0microsoft/fetch-event-source 2.0.1high…...

【QT】QT中的事件

QT中的事件 1.事件的定义和作用2.QT中事件产生和派发流程2.1 步骤2.2 图示示例代码&#xff1a;&#xff08;event函数接收所有事件&#xff09; 3.常见的事件3.1 鼠标事件示例代码&#xff1a;现象&#xff1a; 3.2 按键事件3.3 窗口大小改变事件 4.举例说明示例代码&#xff…...

【QT】QT中的软键盘设计

QT的软键盘设计 1.软键盘制作步骤2.介绍有关函数的使用3.出现的编译错误及解决办法示例代码1&#xff1a;按键事件实现软键盘现象&#xff1a;示例代码2&#xff1a;按键事件实现软键盘&#xff08;加特殊按键&#xff09;现象&#xff1a; 软键盘移植到新的工程的步骤&#xf…...

【Unity】一个AssetBundle热更新的使用小例子

1.新建两个预制体&#xff1a; Cube1&#xff1a;GameObject Material1&#xff1a;Material Cube1使用了Material1材质 之后设置打包配置 Cube1的打包配置为custom.ab Material1的打包配置为mat.ab 2.在Asset文件夹下创建Editor文件夹&#xff0c;并在Editor下创建BuildBundle…...

【Bootstrap V4系列】学习入门教程之 组件-按钮组(Button group)

Bootstrap V4系列 学习入门教程之 组件-按钮组&#xff08;Button group&#xff09; 按钮组&#xff08;Button group&#xff09;一、Basic example二、Button toolbar 按钮工具条三、Sizing 尺寸四、Nesting 嵌套五、Vertical variation 垂直变化 按钮组&#xff08;Button …...

Linux进程间的通信

IPC 即 Inter-Process Communication&#xff0c;也就是进程间通信&#xff0c;它指的是在不同进程之间进行数据交换和协调同步的机制。在操作系统里&#xff0c;每个进程都有自己独立的内存空间&#xff0c;一般情况下不能直接访问其他进程的内存&#xff0c;所以需要借助 IPC…...

常用非对称加密算法的Python实现及详解

非对称加密算法&#xff08;Asymmetric Encryption&#xff09;使用公钥加密、私钥解密&#xff0c;解决了对称加密的密钥分发问题。本文将详细介绍 RSA、ECC、ElGamal、DSA、ECDSA、Ed25519 等非对称加密算法的原理&#xff0c;并提供Python实现代码及安全性分析。 1. 非对称加…...

【题解-洛谷】B4303 [蓝桥杯青少年组省赛 2024] 字母移位

题目&#xff1a;B4303 [蓝桥杯青少年组省赛 2024] 字母移位 题目描述 字母移位表示将字母按照字母表的顺序进行移动。 例如&#xff0c; b \texttt{b} b 向右移动一位是 c \texttt{c} c&#xff0c; f \texttt{f} f 向左移动两位是 d \texttt{d} d。 特别地&#xff0c;…...

详讲viewer查看器

将Python与Cesium结合起来&#xff0c;可以实现高效的数据处理与可视化展示。本文将详细介绍如何在Python环境中集成Cesium&#xff0c;以及实现数据可视化的具体方法。 我们可以通过在app.vue中的修改来更改我们查看器的显示方法 修改前 修改后 还可以进行各式各样的自定义操作…...

开关电源原理

开关电源原理 一、 开关电源的电路组成&#xff1a; 开关电源的主要电路是由输入电磁干扰滤波器&#xff08;EMI&#xff09;、整流滤波电路、功率变换电路、PWM控制器电路、输出整流滤波电路组成。辅助电路有输入过欠压保护电路、输出过欠压保护电路、输出过流保护电路、输出短…...

数据库的并发控制

并发控制 12.1 并发级别 问题&#xff1a;交错的读写 并发客户端可以随意进入和退出事务&#xff0c;并在中途请求读取和写入。为了简化分析&#xff0c;假设enter/exit/read/write是原子步骤&#xff0c;因此并发事务只是这些步骤的交错。 我们还将区分只读事务和读写事务…...

力扣第448场周赛

赛时成绩如下: 这应该是我力扣周赛的最好成绩了(虽然还是三题) 1. 两个数字的最大乘积 给定一个正整数 n。 返回 任意两位数字 相乘所得的 最大 乘积。 注意&#xff1a;如果某个数字在 n 中出现多次&#xff0c;你可以多次使用该数字。 示例 1&#xff1a; 输入&#xff1…...

关于Python:9. 深入理解Python运行机制

一、Python内存管理&#xff08;引用计数、垃圾回收&#xff09; Python&#xff08;CPython&#xff09;采用的是&#xff1a; “引用计数为主&#xff0c;垃圾回收为辅” 的内存管理机制。 也就是说&#xff1a; 引用计数机制&#xff1a;负责大部分内存释放&#xff0c;简…...

Cron表达式的用法

最近几天开发东西用到了定时脚本的问题&#xff0c;中间隔了一段时间没有用到&#xff0c;再次复习一下Cron表达式的用法。 Cron表达式是一种用于定义定时任务执行时间的字符串格式&#xff0c;广泛应用于Unix/Linux系统以及各种编程语言中。其主要用途是通过灵活的时间规则来…...

手机通过局域网访问网狐接口及管理后台网站

1.本地部署接口及后台网站 2.设置允许网站端口通过防火墙 3.查看网站服务器IP 4.手机连接到本地服务器同一局域网 5.手机访问本地服务器接口...

JavaSE核心知识点01基础语法01-01(关键字、标识符、变量)

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;笑口常开&#x1f7ea;生日快乐⬛早点睡觉 &#x1f4d8;博主相关 &#x1f7e7;博主信息&#x1f7e8;博客首页&#x1f7eb;专栏推荐&#x1f7e5;活动信息 文章目录 JavaSE核心知识点01基础语法01-01&#xff0…...

Sliding Window Attention(Longformer)

最简单的自注意力大家肯定都会啦。 但这种全连接的自注意力&#xff08;即每个 token 需要 attend 到输入序列中的所有其他 token&#xff09;计算与内存开销是 O ( n 2 ) O(n^2) O(n2) 。为了缓解这个问题&#xff0c;研究者们提出了 Sliding Window Attention。 Sliding W…...

ROS2 开发踩坑记录(持续更新...)

1. 从find_package(xxx REQUIRED)说起&#xff0c;如何引用其他package(包&#xff09; 查看包的安装位置和include路径详细文件列表 例如&#xff0c;xxx包名为pluginlib # 查看 pluginlib 的安装位置 dpkg -L ros-${ROS_DISTRO}-pluginlib | grep include 这条指令的目的是…...

刷leetcodehot100返航版--哈希表5/5、5/6

回顾一下之前做的哈希&#xff0c;貌似只有用到 unordered_set&#xff1a;存储无序元素unordered_map&#xff1a;存储无序键值对 代码随想录 常用代码模板2——数据结构 - AcWing C知识回顾-CSDN博客 1.两数之和5/5【30min】 1. 两数之和 - 力扣&#xff08;LeetCode&am…...

嵌入式开发学习日志Day13

第九章 预处理命令 前面加“#”的都为预处理命令&#xff1b; 预处理命令是无脑的文本替换&#xff1b; 一、宏定义 1、不带参数的宏定义 一般形式为&#xff1a; #define 标识符 字符串 &#xff08;谷歌规定&#xff09;&#xff1a;所有的宏名均大写&#xff0c;便于…...

AI预测的艺术品走势靠谱吗?

首席数据官高鹏律师团队 AI预测艺术品价格走势&#xff1a;技术与法律的双重考量在当今数字化浪潮席卷全球的时代&#xff0c;人工智能&#xff08;AI&#xff09;技术正以前所未有的速度渗透到各个领域&#xff0c;艺术品市场也不例外。AI预测艺术品价格走势这一新兴事物&…...

AVL树 和 红黑树 的插入算法

1.AVL树 按照二叉搜索树的规则找到要插入的位置并插入&#xff0c;插入过后看是父节点的左还是右孩子&#xff0c;然后把父节点的平衡因子-1或1&#xff0c;调整后如果父节点的平衡因子是0&#xff0c;那就说明这颗子树的高度插入前后不变&#xff0c;上面的就不用调整平衡因子…...

【项目】基于ArkTS的网吧会员应用开发(1)

一、效果图展示 二、界面讲解 以上是基于ArkTS的鸿蒙应用网吧会员软件的引导页&#xff0c;使用滑动组件滑动页面&#xff0c;至最后一页时&#xff0c;点击立即体验&#xff0c;进入登录页面。 三、代码演示 import promptAction from ohos.promptAction; import router fr…...

命令模式(Command Pattern)

非常好&#xff01;现在我们来深入讲解行为型设计模式之一 —— 命令模式&#xff08;Command Pattern&#xff09;。 我将通过&#xff1a; ✅ 定义解释 &#x1f3af; 使用动机 &#x1f40d; Python 完整调用代码&#xff08;含注释&#xff09; &#x1f9ed; 清晰类图 …...

CMake基础介绍

1、CMake 概述 CMake 是一个项目构建工具&#xff0c;并且是跨平台的&#xff1b;关于项目构建目前比较流行的还有 Makefile(通过 Make 命令进行项目的构建)&#xff0c; 大多 IDE 软件都集成了 make,比如&#xff1a;VS 的 nmake、linux 下的 GNU make、Qt 的 qmake 等&…...

偷钱包行为检测数据集VOC+YOLO格式922张1类别有增强

有320张图片是原图剩余为增强图片 数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;922 标注数量(xml文件个数)&#xff1a;922 标注数量…...

C 语言比较运算符:程序如何做出“判断”?

各类资料学习下载合集 ​​https://pan.quark.cn/s/8c91ccb5a474​​ 在编写程序时,我们经常需要根据不同的条件来执行不同的代码。比如,如果一个分数大于 60 分,就判断为及格;如果用户的年龄小于 18 岁,就禁止访问某个内容等等。这些“判断”的核心,就依赖于程序能够比…...

Webug4.0靶场通关笔记16- 第20关文件上传(截断上传)

目录 第20关 文件上传(截断上传) 1.打开靶场 2.源码分析 &#xff08;1&#xff09;右键查看前端源码 &#xff08;2&#xff09;服务端源码分析 &#xff08;3&#xff09;渗透思路 3.渗透实战 &#xff08;1&#xff09;构建脚本 &#xff08;2&#xff09;bp抓包 …...

10 种最新的思维链(Chain-of-Thought, CoT)增强方法

防御式链式思维&#xff08;Chain-of-Defensive-Thought&#xff09; 该方法通过引入结构化、防御性的推理示例&#xff0c;提高大语言模型在面对被污染或误导信息时的稳健性。 &#x1f4c4; 论文链接&#xff1a;https://arxiv.org/abs/2504.20769 混合链式思维&#xff08;…...

力扣119题解

记录 2025.5.5 题目&#xff1a; 思路&#xff1a; 代码: class Solution {public List<Integer> getRow(int rowIndex) {List<Integer> row new ArrayList<Integer>();row.add(1);for (int i 1; i < rowIndex; i) {row.add((int) ((long) row.get(i…...

NSOperation深入解析:从使用到底层原理

1. 基础概念与使用 1.1 NSOperation概述 NSOperation是Apple提供的一个面向对象的并发编程API&#xff0c;它基于GCD&#xff08;Grand Central Dispatch&#xff09;构建&#xff0c;但提供了更高层次的抽象和更丰富的功能。NSOperation允许开发者以面向对象的方式管理并发任…...

suna工具调用可视化界面实现原理分析(二)

这是一个基于React的浏览器操作可视化调试组件&#xff0c;主要用于在AI开发工具中展示网页自动化操作过程&#xff08;如导航、点击、表单填写等&#xff09;的执行状态和结果。以下是关键技术组件和功能亮点的解析&#xff1a; 一、核心功能模块 浏览器操作状态可视化 • 实时…...

【大模型面试每日一题】Day 9:BERT 的 MLM 和 GPT 的 Next Token Prediction 有什么区别?

【大模型面试每日一题】Day 9&#xff1a;BERT 的 MLM 和 GPT 的 Next Token Prediction 有什么区别&#xff1f; &#x1f4cc; 题目重现 &#x1f31f; 面试官&#xff1a;预训练任务中&#xff0c;BERT 的 MLM&#xff08;Masked Language Modeling&#xff09;和 GPT 的 …...

分析strtol(),strtoul()和strtod()三个函数的功能

字符串转换为数值部分和子字符串首地址的函数有strtol(),strtoul()和strtod()三个函数。 1、strtol()函数 long int strtol(const char *str, char **endptr, int base) //当base0时,若字符串不是以"0","0x"和"0X"开头,则将数字部分按照10进制…...

Spring Boot 加载application.properties或application.yml配置文件的位置顺序。

我换一种更通俗易懂的方式&#xff0c;结合具体例子来解释 Spring Boot 加载application.properties或application.yml配置文件的位置顺序。 生活场景类比 想象你要找一本书&#xff0c;你有几个可能存放这本书的地方&#xff0c;你会按照一定顺序去这些地方找&#xff0c;直…...

C++进阶之——多态

1. 多态的概念 多态是用来描述这个世界的 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具体点就是去完成某个行为&#xff0c;当不同的对象去完成时会 产生出不同的状态。 这里就很厉害了&#xff0c;能够实现特殊处理&#xff0c;本文章就是来仔细…...

第13项三期,入组1123例:默沙东启动TROP2 ADC+PD-1子宫内膜癌头对头临床

Umabs DB作为目前全球最全面的抗体药物专业数据库&#xff0c;收录全球近10000个从临床前到商业化阶段抗体药物&#xff0c;涉及靶点1600&#xff0c;涉及疾病种类2400&#xff0c;研发机构2900&#xff0c;覆盖药物蛋白序列、专利和临床等多种专业信息。Umabs DB药物数据库已正…...

政务服务智能化改造方案和案例分析

政务服务智能化改造方案和案例分析 一、引言 在数字化时代浪潮的推动下&#xff0c;政务服务智能化改造已成为提升政府服务效能、优化营商环境、增强民众满意度的关键举措。传统政务服务模式存在流程繁琐、信息孤岛、办理效率低等问题&#xff0c;难以满足现代社会快节奏发展和…...

15.日志分析入门

日志分析入门 第一部分&#xff1a;日志分析基础第二部分&#xff1a;日志分析方法与工具第三部分&#xff1a;日志分析实践总结 目标&#xff1a; • 理解日志分析在网络安全中的作用 • 掌握日志的基本类型和分析方法 • 通过实践初步体验日志分析的过程 第一部分&#xff…...

EPSG:3857 和 EPSG:4326 的区别

EPSG:3857 和 EPSG:4326 是两种常用的空间参考系统&#xff0c;主要区别在于坐标表示方式和应用场景。以下是它们的核心差异&#xff1a; 1. 坐标系类型 EPSG:4326&#xff08;WGS84&#xff09; 地理坐标系&#xff08;Geographic Coordinate System&#xff09;&#xff0c;基…...

Python Cookbook-7.2 使用 pickle 和 cPickle 模块序列化数据

任务 你想以某种可以接受的速度序列化和重建Python 数据结构&#xff0c;这些数据既包括基本Python 对象也包括类和实例。 解决方案 如果你不想假设你的数据完全由基本 Python 对象组成&#xff0c;或者需要在不同的 Python 版本之间移植&#xff0c;再或者需要将序列化后的…...

Java学习手册:Spring 多数据源配置与管理

在实际开发中&#xff0c;有时需要连接多个数据库&#xff0c;例如&#xff0c;一个系统可能需要从不同的数据库中读取和写入数据。Spring 提供了多种方式来配置和管理多数据源&#xff0c;以下将介绍常见的配置和管理方法。 一、多数据源配置 在 Spring 中&#xff0c;可以通…...

六、shell脚本--正则表达式:玩转文本匹配的“万能钥匙”

想象一下&#xff0c;你需要在一大堆文本&#xff08;比如日志文件、配置文件、网页代码&#xff09;里查找符合某种特定模式的字符串&#xff0c;而不是仅仅查找固定的单词。比如说&#xff1a; 找出所有的电子邮件地址 &#x1f4e7;。找到所有看起来像电话号码 &#x1f4d…...

Gradio全解20——Streaming:流式传输的多媒体应用(4)——基于Groq的带自动语音检测功能的多模态Gradio应用

Gradio全解20——Streaming&#xff1a;流式传输的多媒体应用&#xff08;4&#xff09;——基于Groq的带自动语音检测功能的多模态Gradio应用 本篇摘要20. Streaming&#xff1a;流式传输的多媒体应用20.4 基于Groq的带自动语音检测功能的多模态Gradio应用20.4.1 组件及配置1.…...

力扣hot100 (除自身以外数组的乘积)

238. 除自身以外数组的乘积 中等 给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除…...