这是本人根据黑马视频学习 Redis 的相关笔记,系列文章导航:《Redis设计与实现》笔记与汇总
需求:
实体类 Blog
:添加一个字段, 注解是 MyBatisPlus 的注解,表示不在表中
/** * 是否点赞过了 */ @TableField(exist = false) private Boolean isLike;
Controller
:
@PutMapping("/like/{id}") public Result likeBlog(@PathVariable("id") Long id) { // 修改点赞数量 return blogService.likeBlog(id); }
Service
:
@Override public 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()); // 3. 未点赞,点赞,修改数据库,保存用户到 Redis 中 if (BooleanUtil.isFalse(isMember)) { boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update(); if (isSuccess) { stringRedisTemplate.opsForSet().add(key, userId.toString()); } } else { // 4. 已经点赞,取消,修改数据库,删除用户 boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update(); stringRedisTemplate.opsForSet().remove(key, userId.toString()); } return Result.ok(); }
这里用的是 Redis 中的 Set 来解决这个问题的
按点赞时间先后排序,返回 Top5 的用户
ZADD
ZSCORE
ZRANGE
Service
:
@Override public Result queryBlogLikes(Long id) { // 1. 查询 top 5 // ZRANGE KEY 0 4 String key = RedisConstants.BLOG_LIKED_KEY + id; Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4); if (top5 == null || top5.isEmpty()) { return Result.ok(); } // 2. 解析其中的用户id List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList()); // 3. 根据id查询用户 String join = StrUtil.join(",", ids); List<UserDTO> userDTOS = userService.query().in("id", ids).last("ORDER BY FIELD (id," + join + ")").list() .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) .collect(Collectors.toList()); // 4. 反回 return Result.ok(userDTOS); }
注意这里的查询数据库的代码
这个和 Redis 没有关系,直接在数据库中查询就可以了
这里是用了一个 follow 表用来记录关注和被关注者的 id 信息
Service
:
@Override public Result follow(Long followUserId, Boolean isFollow) { // 判断是关注还是取关 Long userId = UserHolder.getUser().getId(); // 关注,新增 if (isFollow) { Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); save(follow); } else { remove(new QueryWrapper<Follow>() .eq("user_id", userId).eq("follow_user_id", followUserId)); } // 取关,删除 return Result.ok(); } @Override public Result isFollow(Long followUserId) { Long userId = UserHolder.getUser().getId(); Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count(); return Result.ok(count > 0); }
可以用 Set 结构进行判断
关注的逻辑
:增加:关注时保存在 Redis 中
@Override public Result follow(Long followUserId, Boolean isFollow) { // 判断是关注还是取关 Long userId = UserHolder.getUser().getId(); // 关注,新增 String key = RedisConstants.FOLLOW_KEY + userId; if (isFollow) { Follow follow = new Follow(); follow.setUserId(userId); follow.setFollowUserId(followUserId); boolean isSuccess = save(follow); if (isSuccess) { stringRedisTemplate.opsForSet().add(key, followUserId.toString()); } } else { remove(new QueryWrapper<Follow>() .eq("user_id", userId).eq("follow_user_id", followUserId)); stringRedisTemplate.opsForSet().remove(key, followUserId.toString()); } // 取关,删除 return Result.ok(); }
判断的逻辑
:
@Override public Result followCommons(Long id) { Long userId = UserHolder.getUser().getId(); String key = RedisConstants.FOLLOW_KEY + userId; String key2 = RedisConstants.FOLLOW_KEY + id; Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2); if (intersect == null || intersect.isEmpty()) { System.out.println("111"); return Result.ok(Collections.emptyList()); } List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList()); Stream<UserDTO> users = userService.listByIds(ids) .stream() .map(user -> BeanUtil.copyProperties(user, UserDTO.class)); return Result.ok(users); }
用什么结构?
用 SortedSet
实现滚动分页
如果没有新数据的插入,那么正常的分页查询即可以,即利用 List 也可以完成这个工作
但是由于可能会有新数据的插入,数据角标也在不断变化中,所以需要使用滚动分页
在 Redis 中进行滚动查询需要四个参数,如下图所示:
MAX
:如果是第一次查询,则返回当前时间戳,否则应为上一次查询的最小值MIN
:可以设为 0,无需变动OFFSET
:相对于 MAX 的偏移量,第一次查询时设为 0 即可,之后需要设为上一次查询的最小值的数量SIZE
:大小,一般是固定的,比如 10 条数据/页用户保存博客时
@Override public Result saveBlog(Blog blog) { // 获取登录用户 UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); // 保存探店博文 save(blog); // 查询笔记作者的所有粉丝 List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list(); for (Follow f : follows) { Long userId = f.getUserId(); // 推送 String key = RedisConstants.FEED_KEY + userId; stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis()); } return Result.ok(blog.getId()); }
查询博客时
@Override public Result queryBlogOfFollow(Long max, Integer offset) { // 获取当前用户 Long userId = UserHolder.getUser().getId(); String key = RedisConstants.FEED_KEY + userId; // 找到收件箱 Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet() .reverseRangeByScoreWithScores(key, 0, max, offset, 2); // 判断 if (typedTuples == null || typedTuples.isEmpty()) { return Result.ok(); } // 解析数据 ArrayList<Long> ids = new ArrayList<>(); long minTime = 0; int os = 1; for (ZSetOperations.TypedTuple<String> tuple : typedTuples) { ids.add(Long.valueOf(tuple.getValue())); if (tuple.getScore().longValue() == minTime) { os++; } else { minTime = tuple.getScore().longValue(); } } // 根据 id 查询 blog String 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); } ScrollResult scrollResult = new ScrollResult(); scrollResult.setList(blogs); scrollResult.setOffset(os); scrollResult.setMinTime(minTime); return Result.ok(scrollResult); }
先写一个测试类,将数据保存到 Redis 中:
@Test public void loadShopData() { List<Shop> list = shopService.list(); Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId)); for (Map.Entry<Long, List<Shop>> longListEntry : map.entrySet()) { Long typeId = longListEntry.getKey(); String key = RedisConstants.SHOP_GEO_KEY + typeId; List<Shop> shops = longListEntry.getValue(); List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(shops.size()); for (Shop shop : shops) { locations.add(new RedisGeoCommands.GeoLocation<>( shop.getId().toString(), new Point(shop.getX(), shop.getY()) )); } stringRedisTemplate.opsForGeo().add(key, locations); } }
SpringDataRedis2.3.9 版本并不支持 Redis 6.2 提供的 GEOSEARCH
命令,我们要修改其版本
下载 IDEA 的插件 Maven Helper
移除默认的版本的包:
再手动添加新版本:
<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>
思路:
如果不需要坐标,则直接从数据库查询
如果需要坐标查询,则
@Override public 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、按照距离排序、分页 String key = RedisConstants.SHOP_GEO_KEY + typeId; GeoResults<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()); } List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent(); if (list.size() <= from) { return Result.ok(Collections.emptyList()); } // 截取 List<Long> ids = new ArrayList<>(list.size()); HashMap<String, Distance> distanceMap = new HashMap<>(list.size()); list.stream().skip(from).forEach(result -> { String shopIdStr = result.getContent().getName(); ids.add(Long.valueOf(shopIdStr)); Distance distance = result.getDistance(); distanceMap.put(shopIdStr, distance); }); String idStr = StrUtil.join(",", ids); List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD (id, " + idStr + ")").list(); for (Shop shop : shops) { shop.setDistance(distanceMap.get(shop.getId().toString()).getValue()); } return Result.ok(shops); }
直接上服务层代码:
@Override public Result sign() { Long userId = UserHolder.getUser().getId(); LocalDateTime now = LocalDateTime.now(); String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); String key = USER_SIGN_KEY + userId + keySuffix; int dayOfMonth = now.getDayOfMonth(); stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true); return Result.ok(); }
获取到今天为止的连续签到次数
@Override public Result signCount() { // 获取记录 Long userId = UserHolder.getUser().getId(); LocalDateTime now = LocalDateTime.now(); String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM")); String key = USER_SIGN_KEY + userId + keySuffix; int dayOfMonth = now.getDayOfMonth(); List<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); } int count = 0; while (true) { if ((num & 1) == 0) { break; } else { count++; } num >>>= 1; } return Result.ok(count); }
一个测试:
插入 100 万条数据,模仿 1000 人访问,看最后结果如何