Redis实战案例及问题分析之好友关注功能(关注、共同好友、消息推送)

这篇博客探讨了社交网络中关注和取关的接口实现,以及如何使用Redis进行共同关注功能。同时,详细介绍了关注推送(Feed流)的Timeline模式,包括拉模式、推模式和推拉结合模式,并提供了基于推模式的关注推送功能的实现,涉及收件箱的Redis数据结构和分页查询。此外,还展示了如何在个人主页查询和展示推送的Blog信息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

关注和取关

需求:基于该表数据结构,实现两个接口

关注和取关接口

    @Override
    public Result follow(Long followUserId, Boolean isFollow) {
        //1.获取登录的用户
        Long userId = UserHolder.getUser().getId();
        //2.判断到底是关注还是取关
        if (isFollow) {
            //3.关注,新增数据
            Follow follow = new Follow();
            follow.setUserId(userId);
            follow.setFollowUserId(followUserId);
            save(follow);
        }else {
            //4取关,删除数据
            remove(new QueryWrapper<Follow>()
                    .eq("user_id",userId)
                    .eq("follow_user_id",followUserId));
        }
        return Result.ok();
    }

判断是否关注的接口

 

 关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示:

 

    @Override
    public Result isFollow(Long followUserId) {
        //1.获取用户id
        Long userId = UserHolder.getUser().getId();
        //2.查询是否关注
        Integer count = query().eq("user_id", userId)
                .eq("follow_user_id", followUserId).count();
        return  Result.ok(count > 0);

    }

共同关注

需求:利用Redis中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。

其中redis中的set可以查询两个集合的交集。所以需要把当前用户、目标用户的关注列表放入set集合。

 

 

 

    @Override
    public Result followCommons(Long followUserId) {
        //1、获取当前用户
        Long userId = UserHolder.getUser().getId();
        String userKey = "follow:" + userId;
        String followKey = "follow:" + followUserId;
        //2.求交集
        Set<String> intersect = stringRedisTemplate.opsForSet().intersect(userKey, followKey);
        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流产品有两种常见模式:

Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
  • 优点:信息全面,不会有缺失。并且实现也相对简单
  • 缺点:信息噪音较多,用户不一定感兴趣,内容获取效率低
智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
  • 优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
  • 缺点:如果算法不精准,可能起到反作用

本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:

  1. 拉模式

 也叫读扩散,优点是节省内存空间,缺点是耗时比较久,读取延时高

 2.推模式

也叫写扩散,把消息写到所有粉丝的收件箱。优点是延时低,缺点是内存占有高。

 

3.推拉结合

 也叫做读写混合,兼具了推和拉两种模式的优点。对普通人,粉丝不多的就用推模式,对于大V,如果是活跃粉丝,就用推模式,普通粉丝就用拉模式

 

 基于推模式实现关注关注推送功能

需求:

  1. 修改新增探店笔记的业务,在保存blog到数据库的同时,推送到粉丝的收件箱
  2. 收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现
  3. 查询收件箱数据时,可以实现分页查询

feed流的分页问题 

Feed流中的数据会不断更新,所以数据的角标也在变化,因此不能采用传统的分页模式。

 feed流的滚动分页

 因为数据会有变化,所以如果采用list结构来保存会出现分页的时候重复读的问题,而sortset数据结构中提供一个根据score范围查的功能,只需要记录下每一次查询最后一条的score值,下一次查询去查小于这个score值的数据(根据时间戳的先后顺序)

@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.查询笔记作者的所有粉丝
        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());
    }

实现关注推送页面的分页查询

需求:在个人主页的“关注”卡片中,查询并展示推送的Blog信息

 

  @Override
    public Result queryBlogOfFollow(Long max, Integer offset) {
        //1.获取当前用户
        Long userId = UserHolder.getUser().getId();
        //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.解析数据:blogId、score(时间戳)、offset
        List<Long> ids = new ArrayList<>(typedTuples.size());
        long minTime = 0;
        int os = 1;
        for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
            //4.1获取id
            ids.add(Long.valueOf(tuple.getValue()));
            //4.2获取分数(时间戳)
            long time = tuple.getScore().longValue();
            if (time == minTime) {
                os++;
            }else {
            minTime = time;
            os = 1;
            }
        }
        //5.根据id查询blog
        String idStr = StrUtil.join(",", ids);
        List<Blog> blogs = query().in("id", ids)
                .last("ORDER BY FIELD(id," + idStr + ")")
                .list();
        for (Blog blog : blogs) {
            queryBlogUser(blog);
            //判断是否已经给当前博客点赞
            //如果已经点赞给isliked字段设置为true
            isBlogLiked(blog);
        }
        //6.封装并返回
        ScrollResult result = new ScrollResult();
        result.setList(blogs);
        result.setOffset(os);
        result.setMinTime(minTime);
        return Result.ok(result);
    }

 

<think>好的,用户让我详细介绍一下Redis。首先,我需要确定用户对Redis的了解程度。之前用户询问过后端生成HTML页面的内容,可能有一定的后端开发经验,但可能对Redis还不熟悉。用户可能是在项目中遇到了性能问题,或者需要缓存解决方案,所以想了解Redis的基本概念和用途。 接下来,我需要思考如何组织内容。用户要求详细介绍,所以应该涵盖Redis的定义、特点、数据结构、使用场景、架构模式、持久化机制、高可用方案、性能优化以及实际应用示例。这样用户可以从多个角度全面了解Redis。 需要确保解释清晰,避免使用过多专业术语,但也要准确。比如在介绍数据结构时,应该简明扼要地说明每种结构的作用和常见应用场景。比如字符串、哈希、列表、集合、有序集合,这些是Redis的核心数据结构,用户可能想知道为什么选择Redis而不是其他数据库,所以需要强调其高性能和丰富的数据类型。 另外,用户可能对Redis的实际应用案例感兴趣,比如缓存、会话管理、排行榜等,结合之前用户提到的后端项目,可以举一些与Web开发相关的例子,比如使用Redis存储用户会话,减轻数据库压力。 还要考虑用户可能关心的持久化和高可用性问题。比如RDB和AOF的区别,主从复制、哨兵和集群的区别,这些是部署Redis时需要面对的问题。需要解释这些机制的原理和适用场景,帮助用户根据实际需求选择配置。 此外,性能优化部分也很重要,用户可能想知道如何充分发挥Redis的性能,比如使用Pipeline、合理选择数据结构、设置过期时间等。这部分需要给出实际建议,帮助用户避免常见陷阱。 最后,总结部分要强调Redis的优势和适用场景,同时提醒不适合Redis的情况,比如大数据量的持久化存储,确保用户理解Redis的定位和局限性。 在写作过程中,要确保结构清晰,分点说明,每个部分有子标题和示例,方便用户快速查找信息。同时,语言要简洁明了,避免冗长,让用户能够轻松理解。检查是否有遗漏的重要点,比如Redis的发布订阅功能、事务支持等,这些也是其重要特性,应该涵盖进去。 可能还需要对比其他缓存解决方案,比如Memcached,说明Redis的优势,比如更丰富的数据结构和持久化支持。这样用户能更好地理解为什么选择Redis。 总之,需要从基础到进阶,全面覆盖Redis的各个方面,结合用户可能的实际应用场景,提供有价值的参考信息。</think>好的!以下是对 **Redis** 的详细介绍,包括核心概念、特性、使用场景、架构设计及实战示例,力求通俗易懂且覆盖关键细节。 --- ### 一、Redis 是什么? **Redis**(Remote Dictionary Server)是一个开源的 **内存键值数据库**,支持多种数据结构(如字符串、哈希、列表、集合等),同时提供持久化、事务、高可用等功能。 - **定位**:高性能缓存、消息中间件、分布式数据存储。 - **特点**:单线程模型(避免锁竞争)、纯内存操作、非阻塞 I/O 多路复用。 - **性能**:读写速度可达 10万+/秒(单机),适合高并发场景。 --- ### 二、核心特性 #### 1. **支持多种数据结构** | 数据结构 | 描述 | 典型应用场景 | |------------|-----------------------------------|-----------------------------| | **String** | 字符串(文本、数字、二进制数据) | 缓存、计数器、分布式锁 | | **Hash** | 键值对集合(类似 Java 的 `Map`) | 存储对象(用户信息、商品详情) | | **List** | 双向链表(有序、可重复) | 消息队列、最新消息列表 | | **Set** | 无序集合(元素唯一) | 标签系统、共同好友 | | **ZSet** | 有序集合(元素唯一,按分数排序) | 排行榜、优先级队列 | | **Stream** | 消息流(持久化消息队列) | 日志收集、事件驱动架构 | #### 2. **持久化机制** - **RDB(快照)**:定时生成内存数据的二进制快照文件(默认方式),适合备份和恢复。 ```bash # 配置示例(redis.conf) save 900 1 # 900秒内至少1次修改则触发保存 save 300 10 # 300秒内至少10次修改则触发保存 ``` - **AOF(追加日志)**:记录所有写操作命令,重启时重放命令恢复数据(更安全但性能略低)。 ```bash appendonly yes # 启用AOF appendfsync everysec # 每秒同步一次(平衡性能与安全) ``` #### 3. **高可用方案** - **主从复制**:主节点(Master)异步复制数据到从节点(Slave),实现读写分离。 - **哨兵模式(Sentinel)**:监控主从节点,自动故障转移(主节点宕机时选举新主)。 - **集群模式(Cluster)**:数据分片(16384个槽),多主多从,支持水平扩展。 --- ### 三、典型使用场景 #### 1. **缓存** - **加速数据库查询**:将热点数据(如商品详情)存入 Redis,减少数据库压力。 ```bash # 伪代码示例(查询商品) product = redis.get("product:1001") if product == null: product = db.query("SELECT * FROM products WHERE id=1001") redis.setex("product:1001", 3600, product) # 缓存1小时 return product ``` #### 2. **分布式锁** - **实现互斥访问**:通过 `SET key value NX EX` 命令确保原子性。 ```bash # 加锁(NX:不存在才设置,EX:过期时间) SET lock:order 12345 NX EX 30 # 解锁(需校验锁的值,防止误删) if redis.get("lock:order") == 12345: redis.del("lock:order") ``` #### 3. **排行榜** - **使用 ZSet 实现**:按分数排序用户积分。 ```bash # 添加用户积分 ZADD leaderboard 1000 "user1" ZADD leaderboard 800 "user2" # 获取前10名 ZREVRANGE leaderboard 0 9 WITHSCORES ``` #### 4. **消息队列** - **使用 List 或 Stream**:生产者推送消息,消费者阻塞获取。 ```bash # 生产者 LPUSH messages "task1" # 消费者(BRPOP 阻塞获取) BRPOP messages 0 ``` --- ### 四、架构设计原理 #### 1. **单线程模型** - **核心逻辑**:所有命令按顺序执行,避免多线程竞争,依赖非阻塞 I/O 多路复用(epoll/kqueue)。 - **为何高效**:内存操作速度极快,单线程避免了上下文切换和锁开销。 #### 2. **数据分片(Cluster 模式)** - **分片规则**:对 Key 进行 CRC16 哈希后取模 16384,分配到不同节点。 - **迁移与重定向**:客户端访问错误分片时,返回 `MOVED` 错误并引导重试。 #### 3. **内存管理** - **淘汰策略**:当内存不足时,按策略删除数据(如 `volatile-lru`、`allkeys-random`)。 - **碎片整理**:通过 `MEMORY PURGE` 或重启清理内存碎片。 --- ### 五、实战优化技巧 #### 1. **避免大 Key** - **问题**:单个 Key 的 Value 过大(如 1MB 的 String),导致网络传输和操作延迟高。 - **优化**:拆分为多个小 Key,或使用 Hash/List 分片存储。 #### 2. **Pipeline 批量操作** - **减少网络开销**:将多个命令打包发送,降低 RTT(Round-Trip Time)。 ```python # Python 示例(redis-py) pipeline = redis.pipeline() pipeline.set("key1", "value1") pipeline.get("key2") result = pipeline.execute() ``` #### 3. **合理设置过期时间** - **避免内存泄漏**:对缓存数据设置 TTL(Time-To-Live),如 `EXPIRE key 60`。 --- ### 六、与其他技术对比 | **对比项** | **Redis** | **Memcached** | **数据库(MySQL)** | |-----------------|------------------------------------------|-----------------------------------|-----------------------------| | **数据持久化** | 支持(RDB/AOF) | 不支持 | 支持 | | **数据结构** | 丰富(String/Hash/List/Set/ZSet/Stream) | 单一(Key-Value) | 关系型(表结构) | | **性能** | 极高(内存操作) | 高(纯内存,但功能简单) | 较低(依赖磁盘 I/O) | | **适用场景** | 缓存、消息队列、实时计算 | 简单缓存 | 持久化存储、复杂查询 | --- ### 七、总结 - **Redis 核心价值**:通过内存操作和丰富数据结构,解决高并发下的性能瓶颈。 - **适用场景**:缓存、实时排行榜、分布式锁、消息队列等。 - **不适合场景**:大数据量持久化存储(成本高)、复杂 SQL 查询(需用关系型数据库)。 建议结合具体业务需求选择数据结构和部署模式(如单机/集群),同时注意内存管理和性能调优。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值