学习笔记 redis项目实践 黑马点评(2)

达人探店

查询探店笔记详情信息

业务分析

  • 获取前端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;
    }

     

  • 现在我们可以看到店铺可以正常显示出来了

 

5941a5c8cfaadd611e5020731fc694db.png

 

点赞功能

业务分析

  • 一个用户只可以点一次赞,没点过赞的点了点赞数+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流和滚动分页

实现方案

  • 拉模式

    • 缺点:延迟高

     

    3b0fdd5015328627b938a438709c9002.png

  • 推模式

    • 缺点:内存占用较高

     

    8ee3afdb8c54e09fede42f1453e23f24.png

  • 推拉结合模式

    • 普通用户使用拉模式,活跃用户使用拉模式

     

    906f19191057d23c24f8f6c86b536ef2.png

  • 总结

25dbb4c0c3d27346a75caeba6e0f109e.png

 

  • 首先改变创建博客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数据结构

    基本用法

     

    122002df1fb999c73bfb79cbd10db785.png

    向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);
            }
        }

    实现我的位置到店铺距离功能

    业务分析

  • 这里我的位置使用的是固定坐标,实际开发中可以从用户手机上获取

     

    71b6fb073a8a6613ac8217a1c1027f6c.png

  • 前端可能不传输地址信息,因此是个选填的参数,需要走两条线

  • 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

基本用法

 

b70b802c9024f832ac66a10b54390e51.png

  • 设置1278910天签到

 

28917c394eb74c550eaaf3975b541969.png

签到实现

业务分析

  • 实现签到接口,将当太难用户信息保存到redis中

 

d183b28616d3be2d0cdb4fbe37f28f5b.png

  • redis的bitmap结构被spring封装到字符串结构中了

 

effb2e7f82e2479586d9205ad42a7506.png

 

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

 连续签到天数功能

1ea35e63df8352fe49c4c7cbe79a5e2a.png

 

代码实现

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的概念

 

b2b54c24e596a7523edda0d55aaf19c1.png

HyperLogLog(HLL)

  • 用于确定非常大的集合的基数,不需要存储所有值的算法
  • redis中HLL是基于string类型实现的,单个HLL的内存永远小于16kb!但是但是测量结果是概率性的,有小于0.81%的误差,但是用于UV统计来说完全没影响,可以忽略

基本用法

  • 添加、计算、合并hll,注意HLL类型相同的值只记录一次,因此很适合做uv统计

 

1482bb3f1ac4f8f2137dbefce1eebc7e.png

  • 通过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

 

54b59ae4aef9b0f61acaa66083d550d9.png

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值