点评Day06 剩下的卡拉米,我不都写,只写一些新奇的

前提:这些业务和钱的关系不大,并不需要重视并发的问题,所以我就不加锁了。

1.博客点赞限制,同一个用户只能点赞一次。我感觉没必要。

复制代码
需求:

* 同一个用户只能点赞一次,再次点击则取消点赞
* 如果当前用户已经点赞,则点赞按钮高亮显示(前端已实现,判断字段Blog类的isLike属性)

实现步骤:

* 给Blog类中添加一个isLike字段,标示是否被当前用户点赞(用户查询博客的时候返回给前端的),如果当前用户已经点赞,则点赞按钮高亮显示。
* 修改点赞功能,利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1(给每一个博客一个redis的set集合)
* 所有根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段。
java 复制代码
 @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();
            //3.2 保存用户到Redis的set集合
            if(isSuccess){
                stringRedisTemplate.opsForSet().add(key,userId.toString());
            }
        }else{
             //4.如果已点赞,取消点赞
            //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());
            }
        }

BooleanUtil.isFalse(isMember)还是那个问题,只要是redis查出来的,可能是null(发生问题),所以不能写成if(isMember)可能会出现空指针异常,这个要习惯并且记住

2.达人探店-点赞排行榜,看的是谁点赞的时间,而不是那个博客点赞多,笑死了

复制代码
之前的点赞是放到set集合,但是set集合是不能排序的,所以这个时候,可以采用一个可以排序的set集合,就是咱们的sortedSet,以时间搓作为得分值。
改进之前的代码即可:
java 复制代码
        // 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();
            // 3.2.保存用户到Redis的set集合  zadd key value score
            if (isSuccess) {
                stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());
            }
        } else {
            // 4.如果已点赞,取消点赞
            // 4.1.数据库点赞数 -1
            boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
            // 4.2.把用户从Redis的set集合移除
            if (isSuccess) {
                stringRedisTemplate.opsForZSet().remove(key, userId.toString());
            }
        }

然后就是实现这个查询功能:

java 复制代码
    public List<UserDTO> queryBlogLikes(Long id) {
        String key = BLOG_LIKED_KEY + id;
        //1.查询top5的点赞用户,根据Redis的zset数据结构 zrange key 0 4
        Set<String> range = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        if (range == null || range.isEmpty()){
            return Collections.emptyList();
        }
        //2.查出来的是string类型,需要转换成long类型
        List<Long> collect = range.stream().map(Long::valueOf).collect(Collectors.toList());
        //3.根据id列表查询用户信息 where id in ()
        List<User> users = userMapper.selectBatchIds1(collect);
        //4.转换成UserDTO列表
        List<UserDTO> collect1 = users.stream()
                .map(user -> {
                    UserDTO userDTO = new UserDTO();
                    BeanUtil.copyProperties(user, userDTO);
                    return userDTO;
                }).collect(Collectors.toList());
        //5.返回
        return collect1;
    }

if (range == null || range.isEmpty()){

return Collections.emptyList();

}这是不一样的前面是判断有没有,后面是判断是不是空集合,因为可能啥用查不出来。 Collections.emptyList()就是生成一个空的集合。

但是会出现一个问题:就是我们给数据库的集合是从前到后,执行的也是从前到后,

为什么返回的是从后到前呢?这是因为id in (),给的是从前到后,但是实际查询的其实是按照主键排序的,in只是起到一个升序的作用

最后xml文件里应该这样写:

XML 复制代码
    <select id="selectBatchIds1" resultType="com.hmdp.entity.User">
        select * from tb_user where id in
        <foreach item="id" collection="collect" separator="," open="(" close=")">
            #{id}
        </foreach>
        order by field(id,
        <foreach item="id" collection="collect" separator="," open="(" close=")">
            #{id}
        </foreach>
        )
    </select>

3.共同关注啥的我就不写了,很简单,用redis的set集合的交集即可。

java 复制代码
public Result followCommons(Long id) {
    // 1.获取当前用户
    Long userId = UserHolder.getUser().getId();
    String key = "follows:" + userId;
    // 2.求交集
    String key2 = "follows:" + id;
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
    if (intersect == null || intersect.isEmpty()) {
        // 无交集
        return Result.ok(Collections.emptyList());
    }
    // 3.解析id集合
    List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
    // 4.查询用户
    List<UserDTO> users = userService.listByIds(ids)
            .stream()
            .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
            .collect(Collectors.toList());
    return Result.ok(users);
}

我们重点看一下,feed流实现方案:

复制代码
当我们关注了用户后,这个用户发了动态,那么我们应该把这些数据推送给用户,这个需求,其实我们又把他叫做Feed流,关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。
Feed流产品有两种常见模式:
Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈

* 优点:信息全面,不会有缺失。并且实现也相对简单
* 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低

智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户

* 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
* 缺点:如果算法不精准,可能起到反作用
本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:
我们本次针对好友的操作,采用的就是Timeline的方式,只需要拿到我们关注用户的信息,然后按照时间排序即可,因此采用Timeline的模式。该模式的实现方案有三种:

* 拉模式:也叫做读扩散

该模式的核心含义就是:当张三和李四和王五发了消息后,都会保存在自己的邮箱中,假设赵六要读取信息,那么他会读取他自己的收件箱,此时系统会从他关注的人群中,把他关注人的信息全部都进行拉取,然后在进行排序

优点:比较节约空间,因为赵六在读信息时,并没有重复读取,而且读取完之后可以把他的收件箱进行清楚。

缺点:比较延迟,当用户读取数据时才去关注的人里边去读取数据,假设用户关注了大量的用户,那么此时就会拉取海量的内容,对服务器压力巨大。
复制代码
* 推模式:也叫做写扩散。

推模式是没有写邮箱的,当张三写了一个内容,此时会主动的把张三写的内容发送到他的粉丝收件箱中去,假设此时李四再来读取,就不用再去临时拉取了

优点:时效快,不用临时拉取

缺点:内存压力大,假设一个大V写信息,很多人关注他, 就会写很多分数据到粉丝那边去
复制代码
* 推拉结合:也叫做读写混合,兼具推和拉两种模式的优点。

推拉模式是一个折中的方案,站在发件人这一段,如果是个普通的人,那么我们采用写扩散的方式,直接把数据写入到他的粉丝中去,因为普通的人他的粉丝关注量比较小,所以这样做没有压力。
如果是大V,那么他是直接将数据先写入到一份到发件箱里边去,然后再直接写一份到活跃粉丝收件箱里边去(不是只写入自己的),现在站在收件人这端来看,如果是活跃粉丝,那么大V和普通的人发的都会直接写入到自己收件箱里边来,而如果是普通的粉丝,由于他们上线不是很频繁,所以等他们上线时,再从发件箱里边去拉信息。

所以业务的需求应该是(人不多采取推模式):

复制代码
* 修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱。
* 收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现,可以采用zset做。

核心的意思:就是我们在保存完探店笔记后,获得到当前笔记的粉丝,然后把数据推送到粉丝的redis中去。
java 复制代码
@Override
public Result saveBlog(Blog blog) {
    // 1.获取登录用户
    UserDTO user = UserHolder.getUser();
    blog.setUserId(user.getId());
    // 2.保存探店笔记
    boolean isSuccess = save(blog);
    if(!isSuccess){
        return Result.fail("新增笔记失败!");
    }
    // 3.查询笔记作者的所有粉丝 select * from tb_follow where follow_user_id = ?
    List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
    // 4.推送笔记id给所有粉丝
    for (Follow follow : follows) {
        // 4.1.获取粉丝id
        Long userId = follow.getUserId();
        // 4.2.推送
        String key = FEED_KEY + userId;
        stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
    }
    // 5.返回id
    return Result.ok(blog.getId());
}
复制代码
* 查询收件箱数据时,可以实现分页查询要注意的是:传统了分页在feed流是不适用的,因为我们的数据会随时发生变化,t2的时间搓更大,所以11在最上边,如果按传统的查。如图会发现6重复查询了。

模拟:

比如在redis里如果按角标查询(zrange key a b是升序查,这里我们要倒叙查所以加一个rev):第一次是排名第一二三的,然后进来一个m7在最上边,因为第一页查的是0-2,下一次就是3-5了,因为数据的变动所以又查出来一个m4.

所以应该采取feed流滚动分页查询:

不使用排名进行查询,使用分数进行查询:先讲一下这个命令zrevrangebyscore key 最大值 最小值 (withscore)limit 偏移量 查多少:加上byscore说明用分数查,同样加上rev是降序了,然后最大值和最小值是限定从这个范围查,withscore表示结果你带不带分数,limit 偏移量 查多少:偏移量表示从最大值偏移几个,0就是不偏移,查多少很好理解不说了。这就是模拟:刚开始最大值随便给的大一点,最小值是0就行。第一次查完后,记录这一次的最小值m5作为下一次的最大值,然后偏移量肯定得是1了,因为不能包括这个m5。这样就完成了滚动分页

总结一下:实现滚动分页需要四个参数,首先最大值,最小值,偏移量,查多少,当然在这个业务里查多少是固定的,写死就行了,那个最小值因为存的是时间错我们写死是0即可,最大值需要给,要的是上一次的最小分数(除了第一次,第一次可以令最大值是当前时间戳),然后偏移量第一次是0,其他是1,但是这里还有一种特殊情况:某一页最后分数有一样的,

比如这个第一页是m8,m7,m6,然后你就去写zrevrangebyscore z1 6 0 limit 1 3是不对的

这是因为m7和m6分数一样,第二次你只跳过了一个,所以还查到了6,所以我们应该偏移量应该是上一次最小值大小一样的元素个数,至此现在就可以实现了。

所以每次查询返回值就需要包括最小时间搓和偏移量,然后每次请求携带这俩个。第一次呢只需要给当前时间戳,然后默认我们让偏移量是0就行。

实现:

1.创建一个实体dto类,用于传递,这里泛型我们用?,因为可能这个滚动分页还要用于其他业务,不能简简单单写成Blog

java 复制代码
public class ScrollResult {
    private List<?> list;
    private Long minTime;
    private Integer offset;
}

2.定义controller

java 复制代码
@GetMapping("/of/follow")
    public Result queryBlogOfFollow(
            @RequestParam("lastId") Long max, @RequestParam(value = "offset", defaultValue = "0") Integer offset){
        return blogService.queryBlogOfFollow(max, offset);
    }

注意因为第一次前端没有给偏移量,我们默认给0,用defaultValue参数表面默认值即可。

3.写业务层

java 复制代码
    public Result queryBlogOfFollow(Long max, Integer offset) {
       //1.获取当前用户
        Long userId = UserHolder.getUser().getId();
       //2.查询收件箱 zrevrangebyscore key max 0 limit offset 2
        String key = FEED_KEY + userId;
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max,  offset, 2);
        //3.如果收件箱啥也没有,或者是空集合直接返回
        if (typedTuples == null || typedTuples.isEmpty()) {
            return Result.ok();
        }
       //4.有的话就去得到ids,偏移量和最小分数
        //4.1准备一个数组存放博客的id
        ArrayList<Long> ids = new ArrayList<>(typedTuples.size());
        //4.2准备变量存放偏移量和最小得分
        long minTime = 0;
        int os = 1;
        //5循环set集合
        for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
            //5.1得到id并且放入数组
            ids.add(Long.valueOf(typedTuple.getValue()));
            //5.2为了得到最小分数,和偏移量,遍历时间搓
            long time = typedTuple.getScore().longValue();
            if (time == minTime){
                os++;
            } else {
                minTime = time;
                os = 1;
            }
        }
        //6.根据ids查询blog
        List<Blog> blogs = blogMapper.selectByIds1(ids);
        //7.封装成分页查询对象
        ScrollResult r = new ScrollResult();
        r.setList(blogs);
        r.setOffset(os);
        r.setMinTime(minTime);

        return Result.ok(r);
    }
复制代码
细节一:
Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max,  offset, 2);我们带了分数,所以返回的应该也带有分数,函数返回的是ZSetOperations.TypedTuple<String>,很复杂,其实就是一个元组,里面就是有值和那个分数,你看源码:得到直接调用这两个方法即可。

细节二:

取出来的时间是double类型,我要变成long类型的,不能和上别一样用Long.Valueof,因为这个要求传入string类型的参数,所以用这个方法.longValue()转化成long类型

细节三:还是和前面一样,你where id in 是无法保证顺序的,想保证顺序必须使用order by field

细节四:比较巧妙的就是那个遍历集合获取id和偏移量以及最小分数,可以多看一下,其实就是先假设一个,然后去遍历时间搓,如果遍历出来更小的那么就更换,并且让那个计数器重置为一。

4.附近商户,这里不用redis来实现,我们采取es的方法。接下来会学习docker,es,然后回过头来把这个写完了

相关推荐
木斯佳1 天前
前端八股文面经大全:京东零售前端实习一面(2026-1-20)·面经深度解析
前端·状态模式·零售
木斯佳1 天前
前端八股文面经大全:字节前端一面(2026-2-1)·面经深度解析
前端·状态模式
前端不太难2 天前
Flutter 页面切换后为什么会“状态丢失”或“状态常驻”?
flutter·状态模式
前端不太难2 天前
从零写一个完整的原生鸿蒙 App
华为·状态模式·harmonyos
木斯佳2 天前
前端八股文面经大全:小红书前端一面(2026-2-3)·面经深度解析
前端·状态模式
geovindu2 天前
python: State Pattern
python·状态模式
阿珊和她的猫3 天前
前端应用首屏加载速度优化全攻略
前端·状态模式
J_liaty3 天前
23种设计模式一状态模式
设计模式·状态模式
木斯佳3 天前
前端八股文面经大全:有赞前端一面二面HR面(2026-1-13)·面经深度解析
前端·状态模式