14、Redis项目实战:达人探店(Redis实现点赞

**作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
*上期文章:Redis:原理速成+项目实战——Redis实战10(Redis消息队列实现异步秒杀)
*订阅专栏:Redis:原理速成+项目实战
希望文章对你们有所帮助

之前一直在用Redis做缓存,并利用Redis的性能、利用Redis的一些数据结构去不断的做业务的优化,耗时很长,每次功能做完总得啃一点源码,梳理一下知识,并查缺补漏,还是挺有收获的。
其实走到现在,不光是Redis的很多操作都会了,中途也为了这个项目学了很多东西,例如docker,mybatis-plus,nginx的负载均衡、反向代理等等,但没去做总结。
现在要用Redis继续给项目增加功能了。

达人探店

  • 发布探店笔记
    • 图片上传
  • 发布
  • 查看探店笔记
  • Redis实现点赞功能
  • Redis实现点赞排行榜

发布探店笔记

探店笔记类似于网站评价,图文结合,对应2张表:
tb_blog:探店笔记表,包含笔记的标题、文字、图片等
tb_blog_comments:其他用户对探店笔记的评价
*
*
点击首页的“+”按钮,即可进入该页面:
*
发布探店笔记的业务主要分为两部分,第一部分是上传图片,第二部分是发布,应该将这两个步骤分离,因为上传照片不光是这里有,在其他地方也会用到,需要写到特定controller层中。
上传图片成功以后,应当返还这个照片的地址,作为表单的参数,点击发布的时候提交到后台。

图片上传

UploadController,图片上传后保存到前端服务器中:

	@PostMapping("blog")
    public Result uploadImage(@RequestParam("file") MultipartFile image) {
   
     
        try {
   
     
            // 获取原始文件名称
            String originalFilename = image.getOriginalFilename();
            // 生成新文件名
            String fileName = createNewFileName(originalFilename);
            // 保存文件到nginx目录下,IMAGE_UPLOAD_DIR = "D:\\nginx-1.18.0\\html\\hmdp\\imgs"
            image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));
            // 返回结果
            log.debug("文件上传成功,{}", fileName);
            return Result.ok(fileName);
        } catch (IOException e) {
   
     
            throw new RuntimeException("文件上传失败", e);
        }
    }

发布

BlogController接口:

	@Resource
    private IBlogService blogService;

    @PostMapping
    public Result saveBlog(@RequestBody Blog blog) {
   
     
        //标题、图片、内容、店铺id都已经在前端提交了,所以只需要保存用户id
        // 获取登录用户
        UserDTO user = UserHolder.getUser();
        blog.setUserId(user.getId());
        // 保存探店博文
        blogService.save(blog);
        // 返回id
        return Result.ok(blog.getId());
    }

启动服务,上传照片,可以直接看到上传的地址:
*
点击上传,就会跳转回主页。

查看探店笔记

需求:点击首页中的探店笔记,会进入到详情页,需要实现该页面的查询接口。
请求路径定义为/blog/{id},利用GET请求,获取博客信息以及用户用户信息返还到前端,业务的流程我全部交给了service层(下面代码也包括了查看多个笔记的分页查询):

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
   
     

    @Resource
    private IUserService userService;

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

    @Override
    public Result queryBlogById(Long id) {
   
     
        //查看blog
        Blog blog = getById(id);
        if(blog == null){
   
     
            return Result.fail("笔记不存在!");
        }
        //查询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());
    }
}

Redis实现点赞功能

当我们在首页的笔记或者笔记详情页点击点赞,将会带上这篇笔记的id,发一个请求,实现点赞功能,同时我们需要满足一个用户,对于一个笔记只能进行一次点赞,因此我们不可以每次发起请求就直接操作数据库,这之间应当加上一些处理。
需求:

1、一个用户只能点赞一次,再次点击则要取消点赞
2、若当前用户已经点赞,则点赞按钮高亮显示(由前端来判断Blog类的isLike属性)

实现:
1、 给Blog类增加一个isLike字段,标示是否被该用户点赞;
Blog实体类:

	@TableField(exist = false)
    private Boolean isLike;

2、 修改点赞功能,利用Redis的set集合判断是够点赞过,未点赞过的则点赞数+1,否则点赞数-1;
根据业务需求可以看出,这里用Redis的set结构是很适合的,key为笔记的id,并记录点赞过的所有用户(集合、唯一性),修改BlogServiceImpl:

	@Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result likeBlog(Long id) {
   
     
        //获取当前用户
        Long userId = UserHolder.getUser().getId();
        //判读当前登录用户是否已经点赞
        String key = "blog:liked:" + id;
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
        if(BooleanUtil.isFalse(isMember)) {
   
     //包装类,不要直接判断,防止拆箱操作
            //未点赞,可以点赞
            //数据库点赞数+1
            boolean isSuccess = update().setSql("liked = liked + 1")
                    .eq("id", id)
                    .update();
            //保存到Redis的set集合
            if(isSuccess){
   
     
                stringRedisTemplate.opsForSet().add(key, userId.toString());
            }
        }else{
   
     
            //已点赞,取消点赞
            //数据库点赞数-1
            boolean isSuccess = update().setSql("liked = liked - 1")
                    .eq("id", id)
                    .update();
            //把用户从Redis的set集合中移除
            if(isSuccess){
   
     
                stringRedisTemplate.opsForSet().remove(key, userId.toString());
            }
        }
        return Result.ok();
    }

3、 根据id查询Blog的业务(笔记详情页),判断当前登录用户是否点赞过,赋值给isLike字段(只需要给isLike字段赋值即可);
*

4、 修改分页查询Blog业务(首页笔记列表),判断当前登录用户是否点赞过,赋值给isLike字段(只需修改forEach内的逻辑即可);
*
上面的isBlogLiked标示对isLike字段进行赋值,进行封装:

	private void isBlogLiked(Blog blog) {
   
     
        //获取当前用户
        Long userId = UserHolder.getUser().getId();
        //判读当前登录用户是否已经点赞
        String key = "blog:liked:" + blog.getId();
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
        blog.setIsLike(BooleanUtil.isTrue(isMember));
    }

测试点赞功能,成功高亮显示,Redis也新增了数据:
*
*
再次点击即可取消点赞,再次点赞就会取消。

Redis实现点赞排行榜

在探店笔记详情页,应该要将给笔记点赞的人显示出来,而且只需要显示出点赞的前几个人,点赞的排行榜是按时间来进行排序的。
分析一下这个需求,我们需要用GET请求去根据id进行查询,并且返还List
由于我们要进行排序,因此set不适合了,可以比较一下下面3个数据结构:

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

因此,SortedSet是最符合业务需求的,查找的效率也更高。

所以我们需要修改之前的点赞业务,在此之前我们需要熟悉一下SortedSet的操作:

增加元素:ZADD
查找元素:用ZSCORE,元素存在即可返回分数,否则返回空

另外一个比较重要的点,我们除了要保证存入SortedSet后取出来的用户Id的顺序是按时间顺序正确排列的,还要保证查询数据库后返还到前端也得是正确的。
但是如果有2个用户点赞,id=5的用户先查询,id=1的用户后查询,简单使用数据库查询结果顺序就会反了,因为SQL代码为:

select id,phone,password,...
from tb_user
where id in(5, 1)

这样的话底层会先查询id为1的用户
应当将SQL改为:

select id,phone,password,...
from tb_user
where id in(5, 1)
order by field(id, 5, 1)

所以查询的逻辑需要改造(当然查询完以后把数组倒转一下也是阔以滴)。
所有代码如下:
BlogController:

@RestController
@RequestMapping("/blog")
public class BlogController {
   
     

    @Resource
    private IBlogService blogService;

    @PostMapping
    public Result saveBlog(@RequestBody Blog blog) {
   
     
        //标题、图片、内容、店铺id都已经在前端提交了,所以只需要保存用户id
        // 获取登录用户
        UserDTO user = UserHolder.getUser();
        blog.setUserId(user.getId());
        // 保存探店博文
        blogService.save(blog);
        // 返回id
        return Result.ok(blog.getId());
    }

    @PutMapping("/like/{id}")
    public Result likeBlog(@PathVariable("id") Long id) {
   
     
        return blogService.likeBlog(id);
    }

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

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

    @GetMapping("/likes/{id}")
    public Result queryBlogByLikes(@PathVariable("id") Long id){
   
     
        return blogService.queryBlogByLikes(id);
    }
}

BlogServiceImpl:

@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
   
     

    @Resource
    private IUserService userService;
    
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public 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 -> {
   
     
            this.queryBlogUser(blog);
            this.isBlogLiked(blog);
        });
        return Result.ok(records);
    }

    @Override
    public Result queryBlogById(Long id) {
   
     
        //查看blog
        Blog blog = getById(id);
        if(blog == null){
   
     
            return Result.fail("笔记不存在!");
        }
        //查询blog有关的用户
        queryBlogUser(blog);
        //查询blog是否被点赞过
        isBlogLiked(blog);
        return Result.ok(blog);
    }

    private void isBlogLiked(Blog blog) {
   
     
        //获取当前用户
        UserDTO user = UserHolder.getUser();
        if(user == null){
   
     
            //若用户未登录,无须查询其是否点赞
            return;
        }
        Long userId = user.getId();
        //判读当前登录用户是否已经点赞BLOG_LIKED_KEY = "blog:liked:"
        String key = BLOG_LIKED_KEY + blog.getId();
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
        blog.setIsLike(score != null);
    }

    @Override
    public Result likeBlog(Long id) {
   
     
        //获取当前用户Id
        Long userId = UserHolder.getUser().getId();
        //判读当前登录用户是否已经点赞
        String key = BLOG_LIKED_KEY + id;
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
        if(score == null) {
   
     
            //未点赞,可以点赞
            //数据库点赞数+1
            boolean isSuccess = update().setSql("liked = liked + 1")
                    .eq("id", id)
                    .update();
            //保存到Redis的SortedSet集合,score为时间戳
            if(isSuccess){
   
     
                stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
            }
        }else{
   
     
            //已点赞,取消点赞
            //数据库点赞数-1
            boolean isSuccess = update().setSql("liked = liked - 1")
                    .eq("id", id)
                    .update();
            //把用户从Redis的SortedSet集合中移除
            if(isSuccess){
   
     
                stringRedisTemplate.opsForZSet().remove(key, userId.toString());
            }
        }
        return Result.ok();
    }

    @Override
    public Result queryBlogByLikes(Long id) {
   
     
        //查询点赞用户取出前五名
        String key = BLOG_LIKED_KEY + id;
        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        if(top5 == null || top5.isEmpty()){
   
     
            return Result.ok(Collections.emptyList());
        }
        //解析其中的用户Id
        List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
        //根据Id查询用户
        //拼接一下字符串,表示ORDER BY的顺序
        String idStr = StrUtil.join(",", ids);
        List<UserDTO> userDTOS = userService.query().in("id", ids).
                last("ORDER BY FIELD(id," + idStr +")")//last表示在背后拼接
                .list()
                .stream()
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .collect(Collectors.toList());
        return Result.ok(userDTOS);
    }

    private void queryBlogUser(Blog blog) {
   
     
        //查询用户直接封装成通用方法
        Long userId = blog.getUserId();
        User user = userService.getById(userId);
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());
    }
}

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: