达人探店
查询探店笔记详情信息
业务分析
- 获取前端id
- 根据id查询笔记和用户信息
- 返回前端笔记信息
代码
JAVA
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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
*/
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Autowired
private IUserService userService;
@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = this.query()
.orderByDesc("liked")
.page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
// 获取当前页数据
List<Blog> records = page.getRecords();
// 查询用户
records.forEach(blog -> queryBlogUser(blog));
return Result.ok(records);
}
@Override
public Result queryBlogById(Long id) {
// 1.查询blog
Blog blog = getById(id);
if(blog == null){
return Result.fail("笔记不存在!");
}
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());
}
}
-
让我们来解决一点bug,按照之前的代码,在探店详情信息中显示不出来店铺信息,发现是在使用我们自定义的CacheClient.getWithLogicalExpire()方法,中间如果缓存未命中就直接返回null,所以造成了我们的店铺显示不出来,因此需要修改此处的逻辑并且加上缓存穿透问题的思路,修改后如下:
/** * 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题 * @param keyPrefix key前缀 * @param id 标识(注:如果方法中参数id这个地方就传id) * @param type 返回值类型 * @param timeout 过期时间 * @param unit 过期时间单位 * @param dbfailback 如果未获取到缓存中的数据执行的方法 * @param <R> * @param <ID> * @return 查询结果 */ public <R,ID> R getWithLogicalExpire(String keyPrefix,ID id,Class<R> type,Long timeout,TimeUnit unit,Function<ID,R> dbfailback) { String key = keyPrefix + id; //1.从redis查询商铺缓存 String json = stringRedisTemplate.opsForValue().get(key); //2.判断缓存是否命中 if (StrUtil.isBlank(json)) { //3.未命中,直接返回 log.debug("未命中,开始判断redis中是否有\"\"数据"); //检查是否存在redis但是redis数据为"" 说明用户已经请求过,且查询过数据库没有这个值,解决缓存穿透问题 if(json == ""){ log.debug("\"\"数据,直接返回null不走数据库"); return null; } log.debug("redis中不存在这个数据,查询数据库"); //查询数据库是否有这个数据 R rMiss = dbfailback.apply(id); if(rMiss == null){ log.debug("数据库查询结果为null,存入\"\"数据到redis"); //解决缓存穿透问题,向redis插入空值 stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL, TimeUnit.MINUTES); return null; } log.debug("数据库查询结果不为null,存入数据到redis,并返回结果"); //存入redis setWithLogicalExpire(key,rMiss,timeout,unit); //返回数据 return rMiss; } //4.命中,需要把json反序列化为对象 RedisData shopRedisData = JSON.parseObject(json, RedisData.class); LocalDateTime shopExpireTime = shopRedisData.getExpireTime(); R r = JSON.parseObject(shopRedisData.getData().toString(),type); //5.判断是否过期 if(LocalDateTime.now().isBefore(shopExpireTime)){ //5.1未过期,直接返回店铺信息 return r; } //5.2过期,需要缓存重建 //6.缓存重建 //6.1获取互斥锁 String lockKey = LOCK_SHOP_KEY + id; boolean isLock = tryLock(lockKey); //6.2判断是否获取锁成功 if(isLock){ log.debug("获取锁成功!"); //6.3成功 //Redis doubleCheck 重新检查缓存,可能在获取锁之前其他线程已经将数据放入缓存 //"Double Check" 是指在查询缓存之前,首先进行一次检查,看看数据是否存在于缓存中 // 如果存在,则直接返回缓存数据。 // 如果不存在,再进一步进行查询数据库的操作,并在查询到数据后,将数据存入缓存中,以供下一次查询使用。 RedisData redisDataDoubleCheck = JSON.parseObject(stringRedisTemplate.opsForValue().get(key), RedisData.class); LocalDateTime expireTimeDoubleCheck = redisDataDoubleCheck.getExpireTime(); if (LocalDateTime.now().isBefore(expireTimeDoubleCheck)) { //3.未过期,直接返回 R rDoubleCheck = JSON.parseObject(shopRedisData.getData().toString(),type); log.debug("DoubleCheck未过期返回shop:{}",rDoubleCheck); return rDoubleCheck; } //过期,开启独立线程,实现缓存重建 CACHE_REBUILD_EXECUTOR.submit(() -> { //重建缓存 //实际开发中应该设置30分钟,这个地方只设置20s方便测试 try { //存入数据库 R r1 = dbfailback.apply(id); //写入redis setWithLogicalExpire(key,r1,timeout,unit); log.debug("重建缓存成功!"); } catch (Exception e) { throw new RuntimeException(e); } finally { // 释放锁 unlock(lockKey); } }); } //6.4获取锁失败,返回旧的店铺信息 log.debug("获取锁失败!"); return r; }
-
现在我们可以看到店铺可以正常显示出来了
点赞功能
业务分析
- 一个用户只可以点一次赞,没点过赞的点了点赞数+1,点过的再点取消赞点赞数-1
代码
JAVA
@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = this.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);
}
@Override
public Result queryBlogById(Long id) {
// 1.查询blog
Blog blog = getById(id);
if(blog == null){
return Result.fail("笔记不存在!");
}
queryBlogUser(blog);
// 查询blog是否被点赞
isBlogLiked(blog);
return Result.ok(blog);
}
private void isBlogLiked(Blog blog) {
// 1.获取登录用户
Long userId = blog.getUserId();
// 2.判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + blog.getId();
blog.setIsLike(BooleanUtil.isTrue(stringRedisTemplate.opsForSet().isMember(key, userId.toString())));
}
private void queryBlogUser(Blog blog) {
Long userId = blog.getUserId();
User user = userService.getById(userId);
blog.setName(user.getNickName());
blog.setIcon(user.getIcon());
}
@Override
public 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 数据库点赞+1
boolean isSuccess = update().setSql("liked = liked + 1").eq("id",id).update();
if(isSuccess){
stringRedisTemplate.opsForSet().add(key,userId.toString());
}
// 3.2 保存用户到redis的set集合
}else {
// 如果已点赞,取消点赞
// 4.1 数据库点赞数 -1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id",id).update();
// 4.2 把用户从redis的set集合移除
if(isSuccess){
stringRedisTemplate.opsForSet().remove(key,userId.toString());
}
}
return Result.ok();
}
点赞排行
业务分析
- 之前使用的set集合,set集合没有排序功能因为需要增加排序功能所以使用sortSet
- sortSet没有ismenber方法,因此可以使用zscore获取分数来判断,查到了存在,为nil就是不存在
- 查询范围查询前五个用户,zrange
代码
package com.hmdp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
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.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 com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.hmdp.utils.RedisConstants.BLOG_LIKED_KEY;
/**
* <p>
* 服务实现类
* </p>
*
*/
@Service
@Slf4j
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
@Autowired
private IUserService userService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 查询热笔记
* @param current
* @return
*/
@Override
public Result queryHotBlog(Integer current) {
// 根据用户查询
Page<Blog> page = this.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);
}
/**
* 通过id查询笔记
* @param id
* @return
*/
@Override
public Result queryBlogById(Long id) {
// 1.查询blog
Blog blog = getById(id);
if(blog == null){
return Result.fail("笔记不存在!");
}
queryBlogUser(blog);
// 查询blog是否被点赞
isBlogLiked(blog);
return Result.ok(blog);
}
/**
* 判断笔记是否被点赞
* @param blog
*/
private void isBlogLiked(Blog blog) {
if(!UserHolder.isLogin()){
return;
}
// 1.获取登录用户
Long userId = UserHolder.getUser().getId();
// 2.判断当前登录用户是否已经点赞
String key = BLOG_LIKED_KEY + blog.getId();
log.debug("获取key:{} 获取userId:{}",key,userId);
Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
log.debug("score:{}",score);
blog.setIsLike(score != null);
}
/**
* 查询笔记的用户
* @param blog
*/
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
*/
@Override
public 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 数据库点赞+1
boolean isSuccess = update().setSql("liked = liked + 1").eq("id",id).update();
if(isSuccess){
// 3.2 保存用户到redis的zset集合
// zadd key value score
stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis());
}
}else {
// 如果已点赞,取消点赞
// 4.1 数据库点赞数 -1
boolean isSuccess = update().setSql("liked = liked - 1").eq("id",id).update();
// 4.2 把用户从redis的zset集合移除
if(isSuccess){
stringRedisTemplate.opsForZSet().remove(key,userId.toString());
}
}
return Result.ok();
}
@Override
public Result queryBlogLikes(Long id) {
String key = BLOG_LIKED_KEY + id;
// 1.查询top5的点赞用户 zrange key 0 4
Set<String> UserSet = stringRedisTemplate.opsForZSet().range(key, 0, 4);
if(UserSet == null || UserSet.isEmpty()){
return Result.ok(Collections.emptyList());
}
// 2.解析出其中的用户id
List<Long> ids = UserSet.stream().map(Long::valueOf).collect(Collectors.toList());
// 3.根据用户id去查询用户
// 如果使用userService.listByIds(ids)那么sql语句是in()不保证顺序,因此需要自定义顺序
// 自定义sql语法 select * from tb_blog where id in (1999,2000) order by field(id,1999,2000)
String idStr = StrUtil.join(",", ids);
List<UserDTO> userDTOS = userService.query()
.in("id",ids).last("order by field("+idStr+")").list()
.stream()
.map(user -> BeanUtil.copyProperties(user,UserDTO.class))
.collect(Collectors.toList());
// 4.返回
return Result.ok(userDTOS);
}
}
好友关注
关注功能
业务分析
-
要实现一下几个功能
-
根据id查询用户
- 查询id并返回UserDTO
-
注销登录
- 删除redis中的用户信息
-
根据用户id查询笔记
- 根据用户id分页查询笔记并返回前端
-
关注和取消关注
- 前端会返回现在是否关注和关注用户id
- 判断是否关注,未关注添加关注信息到redis和数据库,已关注删除redis信息和数据库信息
-
共同关注
- 接收前端目标用户id,并查询当前用户id
- 根据上述两个id用set集合的方法找到并集
- 转为UserDTO返回前端
-
代码
- 根据id查询用户、注销登录UserController
JAVA
/**
* 登出功能
*
* @return 无
*/
@PostMapping("/logout")
public Result logout() {
// TODO 实现登出功能
stringRedisTemplate.delete(LOGIN_CODE_KEY+UserHolder.getUser().getId());
return Result.ok("退出登录成功");
}
JAVA
/**
* 根据id查询用户
* @param id
* @return
*/
@GetMapping("/{id}")
public Result queryById(@PathVariable("id") Long id){
return Result.ok(BeanUtil.copyProperties(userService.getById(id),UserDTO.class));
}
- 根据用户id查询笔记 BlogController
JAVA
@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);
}
- 关注和取消关注、共同关注 FollowServiceImpl
JAVA
package com.hmdp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.Follow;
import com.hmdp.entity.User;
import com.hmdp.mapper.FollowMapper;
import com.hmdp.service.IFollowService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IUserService;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.hmdp.utils.RedisConstants.FOLLOW_KEY;
/**
* <p>
* 服务实现类
* </p>
*
*/
@Service
@Slf4j
public class FollowServiceImpl extends ServiceImpl<FollowMapper, Follow> implements IFollowService {
@Autowired
public StringRedisTemplate stringRedisTemplate;
@Autowired
public IUserService userService;
@Override
public Result follow(Long followUserId, Boolean isFollow) {
//获取登录用户
Long userId = UserHolder.getUser().getId();
String key = FOLLOW_KEY + userId;
//判断是关注还是取关
if (isFollow) {
//关注,新增数据
Follow follow = new Follow();
follow.setUserId(userId);
follow.setFollowUserId(followUserId);
boolean isSuccess = save(follow);
if (isSuccess) {
// 把关注的id放入redis
stringRedisTemplate.opsForSet().add(key, followUserId.toString());
}
} else {
//取关,删除数据
boolean isSuccess = remove(new LambdaQueryWrapper<Follow>()
.eq(Follow::getUserId, userId).eq(Follow::getFollowUserId, followUserId));
if (isSuccess) {
// redis的关注信息删除
stringRedisTemplate.opsForSet().remove(key, followUserId.toString());
}
}
return Result.ok();
}
@Override
public Result isFollow(Long followUserId) {
Long userId = UserHolder.getUser().getId();
// 1.查询
Integer count = query().eq("user_id", userId).eq("follow_User_Id", followUserId).count();
return Result.ok(count > 0);
}
@Override
public Result commonFollow(Long id) {
//获取当前id
Long loginUserId = UserHolder.getUser().getId();
//设置当前登录用户key,和查询共同目标用户key
String loginUserKey = FOLLOW_KEY + loginUserId;
String commonUserKey = FOLLOW_KEY + id;
//拿到redis中两个set集合的并集
Set<String> intersect = stringRedisTemplate.opsForSet().intersect(loginUserKey, commonUserKey);
if(intersect == null || intersect.isEmpty()){
return Result.fail("你们没有共同关注~");
}
//转set集合为List集合
List<Long> commonListIds = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
log.debug("转换结果为commonListIds:{}",commonListIds.toString());
//根据并集id查询UserDto
List<UserDTO> userDTOS = userService.listByIds(commonListIds).stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());
return Result.ok(userDTOS);
}
}
Feed流和滚动分页
实现方案
-
总结
-
首先改变创建博客service,创建博客的同时推送消息给粉丝的收件箱
JAVA
@Override public Result saveBlog(Blog blog) { //获取登录用户 UserDTO user = UserHolder.getUser(); blog.setUserId(user.getId()); //保存探店笔记 boolean isSuccess = save(blog); if(!isSuccess){ return Result.fail("新增失败.请再试一次吧~"); } // 查询笔记作者的所有粉丝 List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list(); // 推送笔记id给所有粉丝 for (Follow follow : follows) { // 查询粉丝id Long userId = follow.getUserId(); // 推送 String key = FEED_KEY + userId; stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis()); } return Result.ok(blog); }
-
粉丝查询service的分页不能按照正常逻辑记角标查询,应该通过score分数来查询,每次查询记住上次查询的最后一个分数
-
滚动分页查询命令:
zrevrangebyscore [key] [maxscore] [minscore] withscores limit [skipCount] [selectCount]
-
第一次请求
zrevrangebyscore z1 现在时间 0 withscores limit 0 3
-
第N次请求
zrevrangebyscore z1 6 0 withscores limit 2 3
-
-
滚动分页查询参数
| 参数 | 第一次请求 | 第N次请求 |
| ——— | —————————————— | ———————————————————— |
| max | 当前时间戳 | 上一次查询的最小时间戳 |
| min | 0 | 0 |
| offset | 0 | 在上一次结果中,与最小值一样的元素的个数 |
| count | 前端决定,一页显示几条就是几 | 前端决定,一页显示几条就是几 |
-
-
service代码如下
JAVA
@Override public Result queryBlogOffFollow(Long max, Integer offset) { // 1.获取当前用户 UserDTO user = UserHolder.getUser(); // 2.查询收件箱 String key = FEED_KEY + user.getId(); /* 第一次请求 zrevrangebyscore z1 现在时间 0 withscores limit 0 3 第N次请求 zrevrangebyscore z1 6 0 withscores limit 2 3 */ Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet() .reverseRangeByScoreWithScores(key, 0, max, offset, 3); // 3.非空判断 if(typedTuples ==null || typedTuples.isEmpty()){ return Result.ok(); } // 4.解析收件箱:BlogId,minTime(时间戳),offset //集合需要做长度重置,因此在这儿指定长度,避免影响性能 List<Long> blogIds = new ArrayList<>(typedTuples.size()); long minTime = 0L; int os = 1; for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) { //获取id String blogId = typedTuple.getValue(); blogIds.add(Long.valueOf(blogId)); //获取分数 long time = typedTuple.getScore().longValue(); if(time == minTime){ os++; }else { minTime = time; os = 1; } } // 5.根据id查询blog String idsStr = StrUtil.join(",", blogIds); List<Blog> blogs = query().in("id", blogIds).last("order by Field(id," + idsStr + ")").list(); for (Blog blog : blogs) { // 5.1查询blog有关的用户 queryBlogUser(blog); // 5.2查询blog是否被点赞 isBlogLiked(blog); } // 6.封装返回 ScrollResult scrollResult = new ScrollResult(); scrollResult.setList(blogs); scrollResult.setMinTime(minTime); scrollResult.setOffset(os); return Result.ok(scrollResult); }
附近商铺
Redis中的GEO数据结构
基本用法
向redis中写入GEO数据结构
JAVA
@Test void loadShopData(){ // 1.查询店铺信息 List<Shop> shops = shopService.list(); // 2.把店铺分组,按照typeId分组,id一致的放到一个集合 Map<Long,List<Shop>> map = shops.stream().collect(Collectors.groupingBy(Shop::getTypeId)); // 3.分批完成写入redis for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) { // 3.1获取类型id Long typeId = entry.getKey(); String key = SHOP_GEO_KEY + typeId; // 3.2 获取同类型的店铺的集合 List<Shop> list = entry.getValue(); List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(list.size()); // 3.3 写入redis //这种方法要多次访问redis不太好 // for (Shop shop : list) { // stringRedisTemplate.opsForGeo().add(key, new Point(shop.getX(),shop.getY()),shop.getId().toString()); // } //提供了一个重载的方法,使用这个传入一个RedisGeoCommands.GeoLocation的List就可以传集合过去了 for (Shop shop : list) { locations.add(new RedisGeoCommands.GeoLocation<>( shop.getId().toString(), new Point(shop.getX(),shop.getY()) )); } stringRedisTemplate.opsForGeo().add(key,locations); } }
实现我的位置到店铺距离功能
业务分析
-
这里我的位置使用的是固定坐标,实际开发中可以从用户手机上获取
-
前端可能不传输地址信息,因此是个选填的参数,需要走两条线
- controller
/**
* 根据商铺类型分页查询商铺信息
*
* @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
/**
* 分页查询店铺
* @param typeId 类型id
* @param current 当前页码
* @param x x坐标(选填)
* @param y y坐标(选填)
* @return
*/
@Override
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
//判断是否需要根据坐标查询
if(x == null || y == null) {
log.debug("未传输坐标信息");
// 不需要坐标,按照数据库查 根据类型分页查询
Page<Shop> page = query()
.eq("type_id", typeId)
.page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
// 返回数据
return Result.ok(page.getRecords());
}
log.debug("传输坐标信息X:{},Y:{}",x,y);
// 2.计算分页参数
int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
// 3.查询redis,按照距离排序,分页,结果:shopId,distance
String key = SHOP_GEO_KEY + typeId;
//分页查询附近五公里的店铺并且返回结果带上距离,和0~end的数据(这个方法只能返回0~end所以from需要手动处理)
GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
.search(
key,
//指定从哪个点算起
GeoReference.fromCoordinate(x, y),
//指定距离
new Distance(5000),
//把结果带上距离(不加的话只返回符合的名字)
RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
);
// 4.解析出id
if(results == null){
return Result.ok(Collections.emptyList());
}
//获取内容
List<GeoResult<RedisGeoCommands.GeoLocation<String>>> contents = results.getContent();
log.debug("contents:{}",contents);
if(contents.size() <= from){
return Result.ok(Collections.emptyList());
}
//用于shopid和坐标匹配
Map<String,Distance> distanceMap = new HashMap<>(contents.size());
// 4.1 截取 from-end的部分
//用于存放ids查询数据库
List<Long> ids = new ArrayList<>(contents.size());
contents.stream().skip(from).forEach(content ->{
//4.1获取店铺id
String shopIdStr = content.getContent().getName();
ids.add(Long.valueOf(shopIdStr));
//4.2获取店铺距离
Distance distance = content.getDistance();
distanceMap.put(shopIdStr,distance);
});
//根据id批量查询(保证有序)
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);
}
用户签到
BitMap数据结构
基本原理
- 我们现在要做签到功能,假如有1000万个用户一年平均签到10次那也是1亿次,因此如果存储用户每天签到的数据量太大了,因此可以使用31个bit的二进制值分别用0和1表示用户签到的情况,这种思想就叫做位图BitMap
基本用法
- 设置1278910天签到
签到实现
业务分析
- 实现签到接口,将当太难用户信息保存到redis中
- redis的bitmap结构被spring封装到字符串结构中了
public Result sign() {
//1.获取当前登录的用户
Long userId = UserHolder.getUser().getId();
//2.获取日期
LocalDateTime now = LocalDateTime.now();
//3.拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = USER_SIGN_KEY + userId + keySuffix;
//4.获取今天是本月第几天
int dayOfMonth = now.getDayOfMonth();
//5.写入redis
stringRedisTemplate.opsForValue().setBit(key, dayOfMonth - 1, true);
return Result.ok();
}
连续签到天数功能
代码实现
public Result signCount() {
//1.获取当前登录的用户
Long userId = UserHolder.getUser().getId();
//2.获取日期
LocalDateTime now = LocalDateTime.now();
//3.拼接key
String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
String key = USER_SIGN_KEY + userId + keySuffix;
//4.获取今天是本月第几天
int dayOfMonth = now.getDayOfMonth();
//5.获取本月截至所有的签到记录
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;
//6.循环遍历
while (true) {
//7.让这个数字与1做与运算 //判断这个bit位是否为0
if ((num & 1) == 0) {
//为0,说明未签到,结束
break;
} else {
//如果不为0,说明已签到,计数器+1
count++;
}
//把数字右移一位。抛弃最后一位bit位,比较下一位
num>>>=1;
}
return Result.ok(count);
}
UV统计
UV和PV的概念
HyperLogLog(HLL)
- 用于确定非常大的集合的基数,不需要存储所有值的算法
- redis中HLL是基于string类型实现的,单个HLL的内存永远小于16kb!但是但是测量结果是概率性的,有小于0.81%的误差,但是用于UV统计来说完全没影响,可以忽略
基本用法
- 添加、计算、合并hll,注意HLL类型相同的值只记录一次,因此很适合做uv统计
- 通过java统计100万个用户并放入到HHL并统计
JAVA
@Test
void testHyperLogLog(){
String[] values = new String[1000];
int j = 0;
for (int i = 0; i < 1000000; i++) {
j = i % 1000;
values[j] = "user_"+i;
if(j == 999){
//发送到redis
stringRedisTemplate.opsForHyperLogLog().add("hll2",values);
}
}
// 统计数量
Long hll2 = stringRedisTemplate.opsForHyperLogLog().size("hll2");
log.debug("hll2:{}",hll2);
}
- 输出结果如下,误差大概为0.0025,一百万的数据才10几kb