Redis:原理速成+项目实战——Redis实战11(达人探店(Redis实现点赞、热榜))

本文介绍了如何在项目中使用Redis实现点赞功能和排行榜,包括用户只能点赞一次的逻辑、点赞状态的存储与查询,以及如何结合Docker、Mybatis-Plus和Nginx进行前后端分离和性能优化。
摘要由CSDN通过智能技术生成

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

之前一直在用Redis做缓存,并利用Redis的性能、利用Redis的一些数据结构去不断的做业务的优化,耗时很长,每次功能做完总得啃一点源码,梳理一下知识,并查缺补漏,还是挺有收获的。
其实走到现在,不光是Redis的很多操作都会了,中途也为了这个项目学了很多东西,例如docker,mybatis-plus,nginx的负载均衡、反向代理等等,但没去做总结。
现在要用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<UserDTO>。
由于我们要进行排序,因此set不适合了,可以比较一下下面3个数据结构:

ListSetSortedSet
排序方式按添加顺序排序无法排序根据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());
    }
}
  • 22
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

布布要成为最负责的男人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值