基于Redis实现好友动态推送、附近商铺功能
好友动态推送
基于推模式实现探店笔记,一个人发布blog,在将blog保存到数据库的同时将blog发送到每个粉丝的收信箱中;收信箱按时间戳进行排序(类似于朋友圈);收信箱查询数据时按滚动分页进行查询。
滚动分页
在进行分页查询时,如果关注的人有新的动态,则不予查询,接着按照上一页的分页查询(每次查询记录上一页的最小值,第二页就接着最小值查询)
实现思路
用户收信箱的数据结构,采用zset,{"feed:followUserId" : "blogId" : "timeScore"}
使用zrevrangebyscore [key] [max] [min] [withscores] [limit offset count]
进行滚动分页查询
key指定集合,min、max指定查询范围、withscores把时间戳作为结果带上(方便下一页的查询)、limit指定从最大值的第几个开始查、查多少个
分页参数
lastId->上一次查询的最小值(初始时为查询时的时间戳) --max
offset->上一次查询中最小值重复情况下的重复次数 --offset
min --0 、count --pageSize 这两个参数固定不变
业务流程
1、获取当前用户
2、在redis中查询当前用户的收信箱
3、解析数据blogId、minTime、offset
3.1获取时间戳和博客id
3.2获取查询中minTime重复的值作为下一轮的offset
4、根据blogId批量查询blog
5、需要将博客的点赞信息进行完善
6、将blog封装并返回
代码实现
BlogServiceImpl.queryBlogOfFollow()
@Override
public Result queryBlogOfFollow(Long max, Integer offset) {
//1、获取当前用户
UserDTO user = UserHolder.getUser();
//2、在redis中查询当前用户的收信箱
String key = "feed:" + user.getId();
Set<ZSetOperations.TypedTuple<String>> typedTuples =
stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 3);
//3、解析数据blogId、minTime、offset
long minTime = 0;
int os = 1;
List<Long> ids = new ArrayList<>(typedTuples.size()); //查询出的博客的id
for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
//3.1获取时间戳和博客id
long time = typedTuple.getScore().longValue();
String blogId = typedTuple.getValue();
ids.add(Long.valueOf(blogId));
//3.2获取查询中minTime重复的值作为下一轮的offset
if(time == minTime){
os++;
}else{
minTime = time;
os = 1;
}
}
if(ids == null || ids.isEmpty()) return Result.ok();
//4、根据blogId批量查询blog
String idsStr = StrUtil.join(",", ids);
List<Blog> blogs = query().in("id", ids).last("order by field(id, " + idsStr + ")").list();
//5、需要将博客的点赞信息进行完善
for (Blog blog : blogs) {
//博客作者的信息
User blogUser = userService.getById(blog.getId());
blog.setIcon(blogUser.getIcon());
blog.setName(blogUser.getNickName());
//对当前登录的用户是否已经对该博客点赞
Double isLike = stringRedisTemplate.opsForZSet().score(BLOG_LIKED_KEY + blog.getId(), UserHolder.getUser().getId().toString());
blog.setIsLike(isLike != null);
}
//6、将blog封装并返回
ScrollResult scrollResult = new ScrollResult();
scrollResult.setList(blogs);
scrollResult.setMinTime(minTime);
scrollResult.setOffset(os);
return Result.ok(scrollResult);
}
附近商铺
传入参数,有当前商铺id、商铺类型id、用户的个人位置信息
设置具体的redis数据结构为geoset和zset很像:{"shop:geo:typeId":"shopId":"geoscore"}
也就是{key:value:score},score是redis将地理的经纬度转化为一个分值进行存储
注意使用了GEOSearch,redis版本需要在6.2以上
业务流程
一、先将店铺的地理数据导入到redis中
1、查询店铺信息
2、将店铺信息按照typeId进行分组
3、分类型写入redis
3.1、获取类型的id,拼接成key
3.2、获取同类型的店铺集合
3.3、使用geoAdd写入redis中
二、使用redis的GeoSearch进行查询、排序
1、判断是否需要根据坐标查询
2、计算分页参数
3、查询redis,按照举例进行排序、分页,结果以Map<shopId,distance>存储
4、解析出id
4.1、截掉0~from的部分
4.2、获取店铺id
4.3、获取距离
5、查询店铺信息
6、将距离信息放入店铺信息中返回
代码实现
加载地理数据loadShopGeoData()
@Test
public void loadShopGeoData(){
//1、查询店铺信息
List<Shop> shopList = shopService.list();
//2、将店铺信息按照typeId进行分组
Map<Long, List<Shop>> map = shopList.stream().collect(Collectors.groupingBy(Shop::getTypeId));
//3、分类型写入redis
Set<Map.Entry<Long, List<Shop>>> entries = map.entrySet();
for (Map.Entry<Long, List<Shop>> entry : entries) {
//3.1、获取类型的id,拼接成key
String key = SHOP_GEO_KEY + entry.getKey();
//3.2、获取同类型的店铺集合
List<Shop> shops = entry.getValue();
//3.3、使用geoAdd写入redis中
for (Shop shop : shops) {
stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(), shop.getY()), shop.getId().toString());
}
}
根据当前位置坐标查询方圆5000m方位内的店铺
ShopServiceImpl.queryShopByType()
@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;
String key = SHOP_GEO_KEY + typeId;
//3、查询redis,按照举例进行排序、分页,结果以Map<shopId,distance>存储
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo() //用geoSearch进行查找指定中心点、查询半径、排序、记录条数等信息
.search(key,
GeoReference.fromCoordinate(x, y),
new Distance(5000),
//加上redis的一些命令参数,比如查找结果带有距离、分页等等
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
);
//4、解析出id
if(results == null){
return Result.ok(Collections.emptyList());
}
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();
//4.1、截掉0~from的部分
List<Long> ids = new ArrayList<>(list.size());
Map<String, Distance> distanceMap = new HashMap<>();
list.stream().skip(from).forEach(result -> {
//4.2、获取店铺id
String shopId = result.getContent().getName();
ids.add(Long.valueOf(shopId));
//4.3、获取距离
Distance distance = result.getDistance();
//将id和distance匹配存放在map中
distanceMap.put(shopId, distance);
});
//5、查询店铺信息
//根据ids查询店铺数据
String strIds = StrUtil.join(",", ids);
List<Shop> shopList = query().in("id", ids).last("order by field(id, " + strIds + ")").list();
//6、将距离信息放入店铺信息中返回
for (Shop shop : shopList) {
shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
}
return Result.ok(shopList);
}