Redis实战篇(四、高级数据结构的使用)

目录

五、达人探店

1.发布探店笔记

 2.查看探店笔记

 3.点赞功能

4.点赞排行榜

六、好友关注

1.关注和取消关注

2.共同关注 

 3.关注推送

(1)Feed流实现方案分析

(2)推送到粉丝收件箱

(3)实现分页查询收件箱

七、附近商户

1.GEO数据结构的基本用法

 2.导入店铺数据到GEO

3.实现附近商户功能

八、用户签到 

1.BitMap功能演示

2.实现签到功能 

3.签到统计 

九、UV统计 

1.HyperLogLog用法 

 2.测试百万数据的统计


五、达人探店

1.发布探店笔记

探店店笔记表,包含笔记中的标题、文字、图片等

 点击首页最下方菜单栏中的 + 按钮,即可发布探店图文:

 将上传图片的地址改为前端 ngnix 服务器中的地址:

上传图片后,会响应该图片在前端保存的路径:

填写完成相关信息后,点击发布: 

此时,个人资料和首页就可以看到新发布的笔记了:

同时,数据库中也新增了一条数据:

 2.查看探店笔记

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

 代码实现:

① controller 层(BlogController)

@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;@Resourceprivate IUserService userService;...@GetMapping("/hot")public Result queryHotBlog(@RequestParam(value = "current", defaultValue = "1") Integer current) {return blogService.queryHotBlog(current);}@GetMapping("/{id}")public Result queryBlogById(@PathVariable("id") Long id){return blogService.queryBlogById(id);}
}

② Service 层

IBlogService 接口:

public interface IBlogService extends IService<Blog> {Result queryHotBlog(Integer current);Result queryBlogById(Long id);
}

 BlogServiceImpl 实现类:

@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);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());}
}

重启项目,运行结果:

 3.点赞功能

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

 打开代码,我们发现点赞的实现仅仅是通过修改数据库点赞数量进行实现的,所以就会存在一个问题:一个用户可以无限次的点赞,刷点赞数,这是不合理的。

需求:

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

实现步骤:

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

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

controller层:

@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;...@PutMapping("/like/{id}")public Result likeBlog(@PathVariable("id") Long id) {return blogService.likeBlog(id);}
}

Service 接口:

public interface IBlogService extends IService<Blog> {Result queryHotBlog(Integer current);Result queryBlogById(Long id);Result likeBlog(Long id);
}

 Service 实现类:

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Resourceprivate StringRedisTemplate stringRedisTemplate;...@Overridepublic Result likeBlog(Long id) {// 1.获取登录用户Long userId = UserHolder.getUser().getId();// 2.判断用户是否已经点赞String key = BLOG_LIKED_KEY + id;Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());if (BooleanUtil.isFalse(isMember)) {// 3.如果未点赞,可以点赞// 3.1.数据库点赞数+1boolean isSuccess = update().setSql("liked = like + 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 = like - 1").eq("id", id).update();// 4.2.把用户从Redis的set集合中移除if (isSuccess) {stringRedisTemplate.opsForSet().remove(key, userId.toString());}}return Result.ok();}
}

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

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

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@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 -> {queryBlogUser(blog);isBlogLiked(blog);});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.查询blog是否被点赞isBlogLiked(blog);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());}private void isBlogLiked(Blog blog) {// 1.获取登录用户Long userId = UserHolder.getUser().getId();// 2.判断用户是否已经点赞String key = BLOG_LIKED_KEY + blog.getId();Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());blog.setIsLike(BooleanUtil.isTrue(isMember));}...
}

 测试:

重启项目,此时进行点赞,就可以成功显示高亮了,redis 中也出现了点赞记录

而且用户只能够点赞一次,再次点击就会取消点赞,redis 中也会删除点赞记录

4.点赞排行榜

当我们点击探店笔记详情页面时,应该按点赞顺序展示点赞用户,比如显示最早点赞的TOP5,形成点赞排行榜。

之前的点赞是放到 Set 集合中,但是 Set 集合又不能排序,所以得换一种数据类型:

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

显然,这里最合适的数据结构就是 SortedSet,在 java 中称作为 ZSet,但之前使用的 Set 和 Sorted 在使用方法上有很多不同。

所以,我们需要修改刚才点赞功能中的代码:

修改BlogServiceImpl:

① ZSet 在使用 add 方法时,不仅需要传入 key 和 value,还要传入分数 score 进行排序,这里我们用时间戳作为 score。

② 由于 ZSet 没有 isMember 方法,所以这里只能通过查询 score 来判断集合中是否有该元素,如果有该元素,则返回值是对应的 score,如果没有该元素,则返回值为 null

③ 修改 isBlogLiked 方法,在原有逻辑上,判断用户是否已登录,登录状态下才会继续判断用户是否点赞。未登录时,首页的笔记无需查询是否点赞。

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@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 -> {queryBlogUser(blog);isBlogLiked(blog);});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.查询blog是否被点赞isBlogLiked(blog);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());}private void isBlogLiked(Blog blog) {// 1.获取登录用户UserDTO user = UserHolder.getUser();if (user == null) {// 用户未登录,无需查询是否点赞return;}Long userId = user.getId();// 2.判断用户是否已经点赞String key = BLOG_LIKED_KEY + blog.getId();Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());blog.setIsLike(score != null);}@Overridepublic Result likeBlog(Long id) {// 1.获取登录用户Long userId = UserHolder.getUser().getId();// 2.判断用户是否已经点赞String key = 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.保存用户到Redis的set集合if (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的set集合中移除if (isSuccess) {stringRedisTemplate.opsForZSet().remove(key, userId.toString());}}return Result.ok();}
}

重启项目,此时进行点赞后,打开 redis,就可以发现:和原来的 Set 相比,多了一个 score

接下来,我们继续来完善显示点赞列表功能。

④ 查询范围查询前五个用户,zrange

controller 层:

@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;...@GetMapping("/likes/{id}")public Result queryBlogLikes(@PathVariable("id") Long id){return blogService.queryBlogLikes(id);}
}

Service 接口:

public interface IBlogService extends IService<Blog> {Result queryHotBlog(Integer current);Result queryBlogById(Long id);Result likeBlog(Long id);Result queryBlogLikes(Long id);
}

Service 实现类:

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Resourceprivate StringRedisTemplate stringRedisTemplate;...@Overridepublic Result queryBlogLikes(Long id) {// 1.查询top5的店点赞用户 zrange key 0 4String key = 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());// 3.根据用户id去查询用户List<UserDTO> userDTOS = userService.listByIds(ids).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());// 4.返回用户集合return Result.ok(userDTOS);}
}

重启项目,点击笔记详情,这时发现已经可以看见点赞的用户列表了。

但是,这时我们发现,展示的顺序似乎不对,好像反了?

我们发现,在控制台和 redis 中,显示的顺序都是正常的,1010 号在前,1 号在后,这是怎么回事呢?

原因:SQL 语句的问题,在 SQL 中我们使用的是 in,用它查询出的结果,会自动按 id 从小到大进行排序。

 解决办法:使用 order by 手动指定返回的顺序

所以,我们需要修改 BlogServiceImpl 中显示点赞列表的代码:

    @Overridepublic Result queryBlogLikes(Long id) {// 1.查询top5的店点赞用户 zrange key 0 4String key = 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());// 把id拼成一串用 , 隔开的字符串String idStr = StrUtil.join(",", ids);// 3.根据用户id去查询用户 WHERE id IN (1010, 1) ORDER BY FIELD(id, 1010, 1)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.关注和取消关注

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

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

  1.  关注和取关接口
  2. 判断是否关注接口

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

代码实现:

controller 层:

@RestController
@RequestMapping("/follow")
public class FollowController {@Resourceprivate IFollowService followService;@PutMapping("/{id}/{isFollow}")public Result follow(@PathVariable("id") Long followUserId,@PathVariable("isFollow")Boolean isFollow){return followService.follow(followUserId,isFollow);}@GetMapping("/or/not/{id}")public Result isFollow(@PathVariable("id") Long followUserId){return followService.isFollow(followUserId);}
}

service 接口:

public interface IFollowService extends IService<Follow> {Result follow(Long followUserId, Boolean isFollow);Result isFollow(Long followUserId);
}

service 实现类:

@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {@Overridepublic Result follow(Long followUserId, Boolean isFollow) {// 1.获取登录用户Long userId = UserHolder.getUser().getId();// 2.判断到底是关注还是取关if (isFollow) {// 3.关注,新增数据Follow follow = new Follow();follow.setUserId(userId);follow.setFollowUserId(followUserId);save(follow);}else {// 4.取关,删除数据remove(new QueryWrapper<Follow>().eq("user_id",userId).eq("follow_user_id",followUserId));}return Result.ok();}@Overridepublic Result isFollow(Long followUserId) {// 1.获取登录用户Long userId = UserHolder.getUser().getId();// 2.查询是否关注Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();return Result.ok(count > 0);}
}

重启项目,查看笔记详情页面:

点击关注:

打开数据库,能够发现此时多了一条记录:

2.共同关注 

点击用户头像,进入到用户详情页,可以查看用户发布的笔记,和共同关注列表

但现在我们还没写具体的业务逻辑,所以现在暂时看不到数据。

准备工作:

① 编写查询用户信息方法(UserController):

@GetMapping("/{id}")
public Result queryUserById(@PathVariable("id") Long userId){// 查询详情User user = userService.getById(userId);if (user == null) {return Result.ok();}UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);// 返回return Result.ok(userDTO);
}

② 编写查询用户笔记方法(BlogController):

@GetMapping("/of/user")
public Result queryBlogByUserId(@RequestParam(value = "current", defaultValue = "1") Integer current,@RequestParam("id") Long id) {// 根据用户查询Page<Blog> page = blogService.query().eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));// 获取当前页数据List<Blog> records = page.getRecords();return Result.ok(records);
}

重启项目,此时我们点击笔记中的用户头像,已经能够看见用户信息和发布的笔记了。

 接下来我们来看看怎么实现共同关注

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

实现方式我们可以联想到,之前学过的set集合。

在 set 集合中,求有交集并集补集的命令,可以把二者关注的人放入到 set 集合中,然后通过SINTER 查询两个 set 集合的交集。

为此,我们必须要修改我们之前的关注逻辑,在关注博主的同时,需要将数据放到set集合中。当取消关注时,也需要将数据从 set 集合中删除。

FollowServiceImpl:

    @Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result follow(Long followUserId, Boolean isFollow) {// 1.获取登录用户Long userId = UserHolder.getUser().getId();String key = "follows:" + userId;// 2.判断到底是关注还是取关if (isFollow) {// 3.关注,新增数据Follow follow = new Follow();follow.setUserId(userId);follow.setFollowUserId(followUserId);boolean isSuccess = save(follow);if (isSuccess) {// 把关注用户的id,放入redis的set集合 sadd userId followUserIdstringRedisTemplate.opsForSet().add(key, followUserId.toString());}} else {// 4.取关,删除数据boolean isSuccess = remove(new QueryWrapper<Follow>().eq("user_id", userId).eq("follow_user_id", followUserId));if (isSuccess) {// 把关注用户的id,从redis的set集合中删除 srem userId followUserIdstringRedisTemplate.opsForSet().remove(key, followUserId.toString());}}return Result.ok();}

重启项目,重新关注进行测试,可以看见 redis 中已经新增了关注数据了。

那么接下来,我们实现共同关注代码:

controller 层:

@RestController
@RequestMapping("/follow")
public class FollowController {@Resourceprivate IFollowService followService;...@GetMapping("/common/{id}")public Result followCommons(@PathVariable("id") Long id){return followService.followCommons(id);}
}

service 接口:

public interface IFollowService extends IService<Follow> {Result follow(Long followUserId, Boolean isFollow);Result isFollow(Long followUserId);Result followCommons(Long id);
}

service 实现类:

@Service
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate IUserService userService;...@Overridepublic Result followCommons(Long id) {// 1.获取当前用户Long userId = UserHolder.getUser().getId();String key1 = "follows:" + userId;String key2 = "follows:" + id;// 2.求交集Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, 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(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());return Result.ok(users);}
}

重启项目,进行测试:

redis 中显示,1010号(当前用户)和 1号用户(小鱼同学),都关注了 2 号用户。

页面展示的共同关注,确实如此。 

 3.关注推送

(1)Feed流实现方案分析

当我们关注了用户之后,这个用户发布了动态,那我们应该把这些数据推送给用户,这个需求,我们又称其为 Feed 流。

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

  • 对于传统的模式内容检索:用户需要主动通过搜索引擎或者是其他方式去查找想看的内容
  • 对于新型 Feed 流的效果:系统分析用户到底想看什么,然后直接把内容推送给用户,从而使用户能更加节约时间,不用去主动搜素

Feed 流的实现有两种模式

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

我们这里是基于关注的好友来做 Feed 流,因此采用的是 Timeline 方式,只需要拿到我们关注用户的信息,然后按照时间排序即可。


采用 Timeline 模式,有三种具体的实现方案:

 ① 拉模式:也叫读扩散

核心含义:当张三和李四、王五发了消息之后,都会保存到自己的发件箱中,如果赵六要读取消息,那么他会读取他自己的收件箱,此时系统会从他关注的人群中,将他关注人的信息全都进行拉取,然后进行排序

优点:比较节约空间,因为赵六在读取信息时,并没有重复读取,并且读取完之后,可以将他的收件箱清除

缺点:有延迟,当用户读取数据时,才会去关注的人的时发件箱中拉取信息,假设该用户关注了海量用户,那么此时就会拉取很多信息,对服务器压力巨大

② 推模式:也叫写扩散

核心含义:推模式是没有发邮箱的,当张三写了一个内容,此时会主动把张三写的内容发送到它粉丝的收件箱中,假设此时李四再来读取,就不用再去临时拉取了

优点:时效快,不用临时拉取

缺点:内存压力大,假设一个大V发了一个动态,很多人关注他,那么就会写很多份数据到粉丝那边去。

③ 推拉结合:页脚读写混合,兼具推和拉两种模式的优点。

核心含义:推拉模式是一个折中的方案,站在发件人这一边,如果是普通人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝收件箱中,因为普通人的粉丝数量较少,所以这样不会产生太大压力。但如果是大V,那么他是直接将数据写入一份到发件箱中去,在直接写一份到活跃粉丝的收件箱中,站在收件人这边来看,如果是活跃粉丝,那么大V和普通人发的都会写到自己的收件箱里,但如果是普通粉丝,由于上线不是很频繁,所以等他们上线的时候,再从发件箱中去拉取信息。

总结:

综上,在本案例中, 由于是点评项目,我们选择第二种:推模式。

(2)推送到粉丝收件箱

需求:

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

Feed 流中的数据会不断更新,所以数据的角标也会不断变化,所以我们不能使用传统的分页模式

假设在 t1 时刻,我们取读取第一页,此时 page = 1,size = 5,那么我们拿到的就是 10~6 这 5 条记录,假设 t2 时刻有发布了一条新纪录,那么在 t3 时刻,我们来读取第二页,此时 page = 2,size = 5,那么此时读取的数据是从第 6 条开始的,读到的是 6~2,那么我们就读到了重复的数据,所以我们要使用 Feed 流的分页,不能使用传统的分页。

Feed流的滚动分页:

我们需要记录每次操作的最后一条,然后从这个位置去开始读数据。

举个例子:我们从 t1 时刻开始,拿到第一页数据,拿到了 10~6 ,然后记录下当前最后一次读取的记录,就是 6,t2 时刻发布了新纪录,此时这个 11 在最上面,但不会影响我们之前拿到的 6,此时 t3 时刻来读取第二页,第二页读数据的时候,从 6-1=5 开始读,这样就拿到了 5~1 的记录。我们在这个地方可以使用 SortedSet 来做,使用时间戳来充当表中的 1~10.

代码实现:

改变原有逻辑,创建博客的同时推送消息给粉丝的收件箱 

controller 层:

@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;@PostMappingpublic Result saveBlog(@RequestBody Blog blog) {return blogService.saveBlog(blog);}...
}

Service 接口:

public interface IBlogService extends IService<Blog> {Result queryHotBlog(Integer current);Result queryBlogById(Long id);Result likeBlog(Long id);Result queryBlogLikes(Long id);Result saveBlog(Blog blog);
}

Service 实现类:

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate IFollowService followService;...@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.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ?List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();// 4.推送笔记id给所有粉丝for (Follow follow : follows) {// 4.1.获取粉丝idLong userId = follow.getUserId();// 4.2.推送String key = "feed:" + userId;stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());}//5.返回idreturn Result.ok(blog.getId());}
}

重启项目,登录之前关注的 2 号用户(可可今天不吃肉),发布新的笔记:

此时打开 redis,就可以看见刚才关注的 1号 和 1010 号用户的收件箱中都多了一条记录:

而 25 号记录正是刚才 2 号用户新创建的笔记。

(3)实现分页查询收件箱

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

分页查询不能按照正常逻辑记角标查询,应该通过 score 分数来查询,每次查询记住上次查询的最后一个分数

滚动分页查询命令:

zrevrangebyscore key maxscore minscore [withscores] [limit offset count]

每次查询完成之后,我们要分析出查询出的最小时间戳,这个值会作为下一次的查询条件中的maxscore。

我们需要找到与上一次查询中相同 minscore 的查询个数,并作为偏移量 offset,下次查询的时候,跳过这些查询过的数据,拿到我们需要的数据。

例如:时间戳8 6 6 5 5 4,我们每次查询 3 个,第一次是 8 6 6,此时最小时间戳是 6,如果不设置偏移量,会从第一个 6 之后开始查询,那么查询到的就是 6 5 5,而不是 5 5 4。

综上,我们的请求参数中需要携带 lastId 和 offset ,即上一次查询时的最小时间戳和偏移量。这两个参数是我们在后台分析处理完返回的,所以返回值也要包含这两个,以便下次前端发送请求时携带。(第一次查询时,lastId 就是当前时间戳,offset 就是 0)

代码实现:

① 编写一个通用的实体类,不一定只对 blog 进行分页查询,这里用泛型做一个通用的分页查询,list 是封装返回的结果,minTime 是记录的最小时间戳,offset 是记录偏移量。

@Data
public class ScrollResult {private List<?> list;private Long minTime;private Integer offset;
}

② 修改业务方法

controller 层:

@RestController
@RequestMapping("/blog")
public class BlogController {@Resourceprivate IBlogService blogService;...@GetMapping("/of/follow")public Result queryBlogOfFollow(@RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset) {return blogService.queryBlogOfFollow(max, offset);}
}

service 接口:

public interface IBlogService extends IService<Blog> {...Result queryBlogOfFollow(Long max, Integer offset);
}

service 实现类:

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {@Resourceprivate IUserService userService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate IFollowService followService;...@Overridepublic Result queryBlogOfFollow(Long max, Integer offset) {// 1.查询登录用户Long userId = UserHolder.getUser().getId();// 2.获取收件箱 ZREVRANGEBYSCORE key max min LIMIT offset countString key = FEED_KEY + userId;Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);// 3.非空判断if (typedTuples == null || typedTuples.isEmpty()) {return Result.ok();}// 4.解析数据:blogId、minTime(时间戳)、offsetList<Long> ids = new ArrayList<>(typedTuples.size());long minTime = 0;int newOffset = 1;for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {// 4.1.获取idids.add(Long.valueOf(tuple.getValue()));// 4.2.获取分数(时间戳)long time = tuple.getScore().longValue();if (time == minTime) {newOffset++;} else {newOffset = 1;minTime = time;}}// 5.根据id查询blogString idStr = StrUtil.join(",", ids);List<Blog> 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(newOffset);r.setMinTime(minTime);return Result.ok(r);}
}

细节:

i. 这里和之前查询点赞排行榜一样,不能直接使用 listByIds() 方法批量根据 id 获取 blog,因为其底层 SQL 是 in,会把原有的顺序破坏,按照 id 进行排序。

ii. 这里查询到 blog 信息之后,还要注意:我们每次查询到 blog 后,都应执行 queryBlogUser 和 isBlogLiked 方法,去查询和 blog 有关的用户信息和 blog 是否被点赞,因为前端会进行展示。


重启项目,进行测试:

七、附近商户

1.GEO数据结构的基本用法

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

命令作用

GEOADD

添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)

GEODIST

计算指定的两个点之间的距离并返回

GEOHASH

将指定 member 的坐标转化为 hash 字符串形式并返回

GEOPOS

返回指定 member 的坐标

GEOGADIUS

指定圆心、半径,找到该园内包含的所有 member,并按照与圆心之间的距离排序后返回,6.2 之后已废弃

GEOSEARCH

在指定范围内搜索 member,并按照与制定点之间的距离排序后返回,范围可以使圆形或矩形,6.2 的新功能

GEOSEARCHSTORE

与 GEOSEARCH 功能一致,不过可以把结果存储到一个指定的 key,也是 6.2 的新功能

需求:

1. 添加下面几条数据:

  • 北京南站(116.378248        39.865275)
  • 北京站(116.42803        39.903738)
  • 北京西站(116.322287        39.893729)
GEOADD g1 116.378248 39.865275 bjn 116.42803 39.903738 bjz 116.322287 39.893729 bjx

查看 redis 客户端, 我们发现 geo 底层采用 ZSET 实现。

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

# 默认单位是米
GEODIST g1 bjn bjx

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

GEOSEARCH g1 FROMLONLAT 116.397904 39.909005 BYRADIUS 10 km WITHDIST

 2.导入店铺数据到GEO

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

具体场景说明,例如美团、饿了么这种外卖App,你是可以看到商家离你有多远的,那我们现在也要实现这个功能。

我们可以使用 GEO 来实现该功能,以当前坐标为圆心,同时绑定相同的店家类型 type,以及分页信息,把这几个条件插入后台,后台查询出对应的数据再返回。

那现在我们要做的就是:将数据库中的数据导入到 Redis中去,GEO 在Redis中就是一个 member和一个经纬度,经纬度对应的就是 tb_shop 中的 x 和 y,而 member,我们用 shop_id 来存。

因为Redis 只是一个内存级数据库,如果存海量的数据,还是力不从心,所以我们只存一个 id,用的时候再拿 id 去 SQL 数据库中查询 shop 信息。

但是此时还有一个问题,我们在 redis 中没有存储 shop_type,无法根据店铺类型来对数据进行筛选。

解决办法:不将所有数据存到同一个 GEO 集合中,而是根据 type_id 作为 key,同 type 的店铺存入同一个 GEO 集合中,在 redis 中存储多个 GEO 集合。

KeyValueScore
shop:geo:美食海底捞40691512240174598
吉野家40691519846517915
shop:geo:KTVKTV 0140691165486458787
KTV 0240691514154651657

 代码实现:

@SpringBootTest
class HmDianPingApplicationTests {@Resourceprivate ShopServiceImpl shopService;@Resourceprivate StringRedisTemplate stringRedisTemplate;@Testpublic void loadShopData() {//1. 查询所有店铺信息List<Shop> shopList = shopService.list();//2. 按照typeId,将店铺进行分组,typeId一致的放到一个集合Map<Long, List<Shop>> map = shopList.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> shops = entry.getValue();String key = "shop:geo:" + typeId;//3.3 写入redis GEOADD key 经度 纬度 memberfor (Shop shop : shops) {stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());}}}
}

但是这种写法效率较低,是一条一条写入的,那我们现在来改进一下,一次性将同一 type_id 的数据写到一个 GEO 集合中。

    @Testpublic void loadShopData() {//1. 查询所有店铺信息List<Shop> shopList = shopService.list();//2. 按照typeId,将店铺进行分组,typeId一致的放到一个集合Map<Long, List<Shop>> map = shopList.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> shops = entry.getValue();String key = "shop:geo:" + typeId;List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(shops.size());//3.3 写入redis GEOADD key 经度 纬度 memberfor (Shop shop : shops) {// 先将当前type的商铺都统一添加到locations集合中locations.add(new RedisGeoCommands.GeoLocation<>(shop.getId().toString(), new Point(shop.getX(), shop.getY())));}// 批量写入,将locations集合中所有数据一次性写入stringRedisTemplate.opsForGeo().add(key, locations);}}

运行结果:

3.实现附近商户功能

注意:SpringDataRedis 的 2.3.9 版本并不支持 Redis 6.2 提供的 GEOSEARCH 命令,因此我们需要提升其版本,修改自己的 pom.xml 文件

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><exclusions><exclusion><artifactId>spring-data-redis</artifactId><groupId>org.springframework.data</groupId></exclusion><exclusion><artifactId>lettuce-core</artifactId><groupId>io.lettuce</groupId></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>

代码实现:

controller 层:

@RestController
@RequestMapping("/shop")
public class ShopController {@Resourcepublic IShopService shopService;..../*** 根据商铺类型分页查询商铺信息** @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);}
}

service 接口:

public interface IShopService extends IService<Shop> {Result queryById(Long id);Result update(Shop shop);Result queryShopByType(Integer typeId, Integer current, Double x, Double y);
}

service 实现类:

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@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.MAX_PAGE_SIZE;int end = current * SystemConstants.MAX_PAGE_SIZE;String key = SHOP_GEO_KEY + typeId;//3. 查询redis、按照距离排序、分页; 结果:shopId、distance//GEOSEARCH key FROMLONLAT x y BYRADIUS 5000 m WITHDISTGeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(key,GeoReference.fromCoordinate(x, y),new Distance(5000),RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end));if (results == null) {return Result.ok(Collections.emptyList());}//4. 解析出idList<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();if (list.size() < from) {//起始查询位置大于数据总量,则说明没数据了,返回空集合return Result.ok(Collections.emptyList());}//4.1.截取from~end的部分ArrayList<Long> ids = new ArrayList<>(list.size());HashMap<String, Distance> distanceMap = new HashMap<>(list.size());list.stream().skip(from).forEach(result -> {//4.2.获取店铺idString shopIdStr = result.getContent().getName();ids.add(Long.valueOf(shopIdStr));//4.3.获取距离Distance distance = result.getDistance();distanceMap.put(shopIdStr, distance);});//5. 根据id查询shopString idsStr = StrUtil.join(",", ids);List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD( id," + idsStr + ")").list();for (Shop shop : shops) {//设置shop的距离属性,从distanceMap中根据shopId查询shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());}//6. 返回return Result.ok(shops);}...
}

重启项目,进行测试,发现已经可以正常显示出距离并且排序:

八、用户签到 

1.BitMap功能演示

假如我们用一张表来存储用户签到信息,其结构应该如下:

用户签到一次,就是一条记录,假如有 1000W 用户,平均每人每年签到 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 数组,并以十进制形式返回
BITOP将多个 BitMap 的结果做位运算(与、或、异或)
BITPOS查找 bit 数组中指定范围内第一个 0 或 1 出现的位置

2.实现签到功能 

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

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

代码实现:

controller 层:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@Resourceprivate IUserService userService;@Resourceprivate IUserInfoService userInfoService;...@PostMapping("/sign")public Result sign(){return userService.sign();}
}

Service 接口:

public interface IUserService extends IService<User> {Result sendCode(String phone, HttpSession session);Result login(LoginFormDTO loginForm, HttpSession session);Result sign();
}

Service 实现类:

@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result sign() {//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. 获取今天是当月第几天(1~31)int dayOfMonth = now.getDayOfMonth();//5. 写入Redis  BITSET key offset 1stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);return Result.ok();}...
}

重启项目,使用 PostMan 发送请求测试:

请求地址:http://localhost:8080/api/user/sign

打开 redis,发现的确新增了一条数据,第 23 位(今天 23 号)是 1。

3.签到统计 

Question1:什么叫做连续签到天数?

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

Question2:如何获取本月到今天为止的所有签到数据?

BITFIELD key GET u[dayOfMonth] 0

Question3:如何从后往前遍历每个 bit 位,获取连续签到天数

与 1 做与运算,就能得到最后一个 bit 位。

随后右移 1 位,下一个 bit 位就成为了最后一个 bit 位。

直到与运算的结果为 0 时停止,说明断签了。


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

代码实现:

controller 层:

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@Resourceprivate IUserService userService;@Resourceprivate IUserInfoService userInfoService;...@GetMapping("/sign/count")public Result signCount(){return userService.signCount();}
}

Service 接口:

public interface IUserService extends IService<User> {Result sendCode(String phone, HttpSession session);Result login(LoginFormDTO loginForm, HttpSession session);Result sign();Result signCount();
}

Service 实现类:

@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result signCount() {//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. 获取今天是当月第几天(1~31)int dayOfMonth = now.getDayOfMonth();//5. 获取本月截止今天为止的所有签到记录,返回的是一个十进制的数字  BITFIELD key GET uDay 0List<Long> result = stringRedisTemplate.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) {//6.1.让这个数字与1做与运输,得到数字的最后一个bit位,判断这个bit是否为0if ((num & 1) == 0) {//6.2.如果为0,说明未签到,结束break;} else//6.3.如果不为0,说明已签到,计数器+1count++;//6.4.数字无符号右移,抛弃最后一个bit位,继续下一个bit位num >>>= 1;}return Result.ok(count);}...
}

重启项目,使用 PostMan 发送请求测试:

请求地址:http://localhost:8080/api/user/sign/count

可以发现,的确返回了正确的连续签到天数。

九、UV统计 

1.HyperLogLog用法 

UV:全称 Unique Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1 天内同一个用户多次访问该网站,只记录 1 次。

PV:全称 Page View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录 1 次 PV,用户多次打开页面,则记录多次 PV。往往用来衡量网站的流量。

UV 统计在服务端做会很麻烦,因为要判断该用户是否已经统计过了,需要将统计过的信息保存,但是如果每个访问的用户都保存到 Redis 中,那么数据库会非常恐怖,那么该如何处理呢?

HyperLogLog(HLL)是从 Loglog 算法派生的概率算法,用户确定非常大的集合基数,而不需要存储其所有值,算法相关原理可以参考下面这篇文章:https://juejin.cn/post/6844903785744056333#heading-0

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

 2.测试百万数据的统计

我们直接使用单元测试,向 HyperLogLog 中添加 100W 条数据,看看内存占用是否真的那么低,以及统计误差如何。

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

插入100W 条数据,得到的 count 为 997593,误差率为 0.002407%。

插入后内存增加:

2242288 - 2227904 = 14384 字节

14384 / 1024 = 14.046875 kb < 16 kb

100W 条数据只占用了 14 kb 的内存,占用率确实是极低!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/65224.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基本操作:iframe、alert

背景 如果你的目标元素出现在一个iframe标签下&#xff0c;则不能直接定位&#xff0c;必须先完成切换才能进行定位操作&#xff0c;如下图 整个理解为一个大的房间&#xff0c;里面是客厅&#xff0c;driver进到客厅后&#xff0c;如果想操作iframe A里的数据&#xff0c;需…

【C++11】类型分类、引用折叠、完美转发

目录 一、类型分类 二、引用折叠 三、完美转发 一、类型分类 C11以后&#xff0c;进一步对类型进行了划分&#xff0c;右值被划分纯右值(pure value&#xff0c;简称prvalue)和将亡值 (expiring value&#xff0c;简称xvalue)。 纯右值是指那些字面值常量或求值结果相当于…

IntelliJ Idea常用快捷键详解

文章目录 IntelliJ Idea常用快捷键详解一、引言二、文本编辑与导航1、文本编辑2、代码折叠与展开 三、运行和调试四、代码编辑1、代码补全 五、重构与优化1、重构 六、使用示例代码注释示例代码补全示例 七、总结 IntelliJ Idea常用快捷键详解 一、引言 在Java开发中&#xff…

kafka的备份策略:从备份到恢复

文章目录 一、全量备份二、增量备份三、全量恢复四、增量恢复 前言&#xff1a;Kafka的备份的单元是partition&#xff0c;也就是每个partition都都会有leader partiton和follow partiton。其中leader partition是用来进行和producer进行写交互&#xff0c;follow从leader副本进…

怎么模仿磁盘 IO 慢的情况?并用于MySQL进行测试

今天给大家分享一篇在测试环境或者是自己想检验自己MySQL性能的文章 实验环境&#xff1a; Rocky Linux 8 镜像&#xff1a;Rocky-8.6-x86_64-dvd.iso 1. 创建一个大文件作为虚拟磁盘 [rootlocalhost ~] dd if/dev/zero of/tmp/slowdisk.img bs1M count100 记录了1000 的读入…

1.微服务灰度发布(方案设计)

前言 微服务架构中的灰度发布&#xff08;也称为金丝雀发布或渐进式发布&#xff09;是一种在不影响现有用户的情况下&#xff0c;逐步将新版本的服务部署到生产环境的策略。通过灰度发布&#xff0c;你可以先将新版本的服务暴露给一小部分用户或特定的流量&#xff0c;观察其…

【开源免费】基于SpringBoot+Vue.JS安康旅游网站(JAVA毕业设计)

本文项目编号 T 098 &#xff0c;文末自助获取源码 \color{red}{T098&#xff0c;文末自助获取源码} T098&#xff0c;文末自助获取源码 目录 一、系统介绍二、数据库设计三、配套教程3.1 启动教程3.2 讲解视频3.3 二次开发教程 四、功能截图五、文案资料5.1 选题背景5.2 国内…

基于SpringBoot的4S店汽车销售管理系统的设计与实现

一、课题背景 为汽车销售公司设计了一个汽车管理系统 技术&#xff1a;前台采用网页技术&#xff0c;后端采用SpringBoottMybatistvue 项目 描述&#xff1a;随着人们生活水平的不断提高&#xff0c;人们对汽车的消费和需求也越来越旺盛。多汽车销售公司仍然采用人工记账的传…

电子应用设计方案72:智能扫地机器人系统设计

智能扫地机器人系统设计 一、引言 智能扫地机器人作为现代智能家居的重要组成部分&#xff0c;旨在为用户提供便捷、高效的地面清洁服务。本设计方案将详细阐述智能扫地机器人的系统架构、功能模块及实现方式。 二、系统概述 1. 系统目标 - 自主规划清扫路径&#xff0c;覆盖…

路由策略

控制层流量 --- 路由协议传递路由信息时产生的流量 数据层流量 --- 设备访问目标地址时产生的流量 所谓的路由策略----在控制层面转发流量的过程中&#xff0c;截取流量&#xff0c;之后修改流量再转发或不转发的技术&#xff0c;最终达到影响路由器路由表的生成&#xff0c…

【CSS in Depth 2 精译_095】16.3:深入理解 CSS 动画(animation)的性能

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第五部分 添加动效 ✔️【第 16 章 变换】 ✔️ 16.1 旋转、平移、缩放与倾斜 16.1.1 变换原点的更改16.1.2 多重变换的设置16.1.3 单个变换属性的设置 16.2 变换在动效中的应用 16.2.1 放大图标&am…

数据之林的守护者:二叉搜索树的诗意旅程

文章目录 前言一. 二叉搜索树的概念1.1 二叉搜索树的定义1.1.1 为什么使用二叉搜索树&#xff1f; 二. 二叉搜索树的性能分析2.1 最佳与最差情况2.1.1 最佳情况2.1.2 最差情况 2.2 平衡树的优势 三.二叉搜索树的基本操作实现3.1.1 详细示例3.1.2 循环实现插入操作3.1.2.1 逻辑解…

利索能及 ▏外观专利相似度多少算侵权?

判断是否侵权前提&#xff1a; 双方产品属于同类产品&#xff0c;不属于同类产品的不能比较。 判定同类产品不仅仅要依据《国际外观设计分类表》&#xff0c;还要依据一般商品商品的分类标准来却确定。 简单概括来说&#xff0c;判定侵权前提就是被控侵权产品和外观设计专利…

【编译原理】往年题汇总(山东大学软件学院用)

&#x1f308; 个人主页&#xff1a;十二月的猫-CSDN博客 &#x1f525; 系列专栏&#xff1a; &#x1f3c0;编译原理_十二月的猫的博客-CSDN博客 &#x1f4aa;&#x1f3fb; 十二月的寒冬阻挡不了春天的脚步&#xff0c;十二点的黑夜遮蔽不住黎明的曙光 目录 1. 前言 2. …

适用于Synology NAS的在线办公套件:ONLYOFFICE安装指南

使用 Synology NAS 上的 ONLYOFFICE 文档&#xff0c;您能在私有云中直接编辑文本文档、电子表格、演示文稿和 PDF&#xff0c;确保工作流程既安全又高效。本指南将分步介绍如何在 Synology 上安装 ONLYOFFICE 文档。 关于 Synology Synology NAS&#xff08;网络附加存储&…

SpringbBoot如何实现Tomcat集群的会话管理

在使用 Tomcat 集群时&#xff0c;由于每个 Tomcat 实例的 Session 存储是独立的&#xff0c;导致无法实现 Session 的共享&#xff0c;这可能影响到用户跨节点的访问。为了实现跨 Tomcat 实例共享 Session&#xff0c;可以使用 Spring Session 配合 Redis 进行集中式会话管理。…

机器人C++开源库The Robotics Library (RL)使用手册(三)

进入VS工程,我们先看看这些功能函数及其依赖库的分布关系: rl命名空间下,主要有八大模块。 搞定VS后将逐个拆解。 1、编译运行 根据报错提示,配置相应错误的库(根据每个人安装位置不同而不同,我的路径如下:) 编译所有,Release版本耗时大约10分钟。 以rlPlan运动…

零基础微信小程序开发——页面导航之声明式导航(保姆级教程+超详细)

&#x1f3a5; 作者简介&#xff1a; CSDN\阿里云\腾讯云\华为云开发社区优质创作者&#xff0c;专注分享大数据、Python、数据库、人工智能等领域的优质内容 &#x1f338;个人主页&#xff1a; 长风清留杨的博客 &#x1f343;形式准则&#xff1a; 无论成就大小&#xff0c;…

Ch9 形态学图像处理

Ch9 形态学图像处理 blog点此处&#xff01;<--------- 四大算子相应性质。 腐蚀、膨胀、开闭之间的含义、关系 文章目录 Ch9 形态学图像处理预备知识(Preliminaries)膨胀和腐蚀(Dilation and Erosion)腐蚀膨胀膨胀与腐蚀的对偶关系 开闭操作(Opening and Closing)开运算闭…

【UE5 C++课程系列笔记】14——GameInstanceSubsystem与动态多播的简单结合使用

效果 通过在关卡蓝图中触发GameInstanceSubsystem包含的委托&#xff0c;来触发所有绑定到这个委托的事件&#xff0c;从而实现跨蓝图通信。 步骤 1. 新建一个C类 这里命名为“SubsystemAndDelegate” 引入GameInstanceSubsystem.h&#xff0c;让“SubsystemAndDelegate”继承…