SpringBoot集成Redis—缓存穿透解决方案与哨兵模式实战

news2024/9/25 9:34:55

目录

1、环境准备

1)pom.xml引入Redis依赖

2) 演示业务场景

2、SpringBoot集成Redis单机模式

1) 通过MyBatis逆向工程生成实体Bean和数据持久层

2) application.yml 中配置redis连接信息

3) 启动redis服务

4)  XinTuProductRedisController类

5) XinTuProductRedisService实现

6)  启动类SpringbootApplication

7)  启动SpringBootCase应用,访问测试

8)  打开Redis 客户端

3、缓存穿透现象

1) 穿透测试

XinTuRedisPenetrateController测试类。 

2 )启动应用程序,浏览器访问测试

3)造成的问题

4)解决方法

4、SpringBoot集成Redis哨兵模式(一主三从三哨兵)

5. 哨兵配置

1)验证主从数据同步

2) 主节点选举


 

以下案例依然在SpringBootCase项目基础上完成。(Redis采用Redis-x64-3.2.100版本)

1、环境准备

1)pom.xml引入Redis依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2) 演示业务场景

根据商品总数功能,先从Redis缓存中查找,如果找不到,再从MySQL数据库中查找,然后将数据放到Redis缓存。

2、SpringBoot集成Redis单机模式

1) 通过MyBatis逆向工程生成实体Bean和数据持久层

实体类:

package com.xintu.demo.entity;

import java.util.Date;

public class TProduct {
    private Integer id;

    private Integer categoryId;

    private String itemType;

    private String title;

    private String sellPoint;

    private String price;

    private Integer num;

    private String image;

    private Integer status;

    private Integer priority;

    private String createdUser;

    private Date createdTime;

    private String modifiedUser;

    private Date modifiedTime;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getCategoryId() {
        return categoryId;
    }

    public void setCategoryId(Integer categoryId) {
        this.categoryId = categoryId;
    }

    public String getItemType() {
        return itemType;
    }

    public void setItemType(String itemType) {
        this.itemType = itemType == null ? null : itemType.trim();
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title == null ? null : title.trim();
    }

    public String getSellPoint() {
        return sellPoint;
    }

    public void setSellPoint(String sellPoint) {
        this.sellPoint = sellPoint == null ? null : sellPoint.trim();
    }

    public String getPrice() {
        return price;
    }

    public void setPrice(String price) {
        this.price = price == null ? null : price.trim();
    }

    public Integer getNum() {
        return num;
    }

    public void setNum(Integer num) {
        this.num = num;
    }

    public String getImage() {
        return image;
    }

    public void setImage(String image) {
        this.image = image == null ? null : image.trim();
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public Integer getPriority() {
        return priority;
    }

    public void setPriority(Integer priority) {
        this.priority = priority;
    }

    public String getCreatedUser() {
        return createdUser;
    }

    public void setCreatedUser(String createdUser) {
        this.createdUser = createdUser == null ? null : createdUser.trim();
    }

    public Date getCreatedTime() {
        return createdTime;
    }

    public void setCreatedTime(Date createdTime) {
        this.createdTime = createdTime;
    }

    public String getModifiedUser() {
        return modifiedUser;
    }

    public void setModifiedUser(String modifiedUser) {
        this.modifiedUser = modifiedUser == null ? null : modifiedUser.trim();
    }

    public Date getModifiedTime() {
        return modifiedTime;
    }

    public void setModifiedTime(Date modifiedTime) {
        this.modifiedTime = modifiedTime;
    }
}

数据层Mapper: 

package com.xintu.demo.mapper;

import com.xintu.demo.entity.TProduct;
import com.xintu.demo.entity.TProductExample;
import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper //方式一:添加@Mapper注解,等同于主类上加@MapperScan("com.demo.demo.mapper")
public interface TProductMapper {
    long countByExample(TProductExample example);

    int deleteByExample(TProductExample example);

    int deleteByPrimaryKey(Integer id);

    int insert(TProduct record);

    int insertSelective(TProduct record);

    List<TProduct> selectByExample(TProductExample example);

    TProduct selectByPrimaryKey(Integer id);

    int updateByExampleSelective(@Param("record") TProduct record, @Param("example") TProductExample example);

    int updateByExample(@Param("record") TProduct record, @Param("example") TProductExample example);

    int updateByPrimaryKeySelective(TProduct record);

    int updateByPrimaryKey(TProduct record);
}

2) application.yml 中配置redis连接信息

完整application.yml配置文件如下:

#spring:
#  profiles:
#      active: test #激活对应环境配置,以测试环境为例
server:
  port: 8888 # 设置内嵌Tomcat端口号
  servlet:
    context-path: /springbootcase # 设置项目上下文根路径,这个在请求访问的时候需要用到

test:
  site: 35xintu.com #测试站点
  user: xintu #测试用户

spring:
  datasource: # mysql相关配置
    url: jdbc:mysql://localhost:3306/xintu?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: xxx #根据自己的本地配置情况设置
  devtools:
    restart:
      enabled: true  #设置开启热部署
      additional-paths: src/main/java #重启目录
      exclude: WEB-INF/** #排除一些不需要自动重启的资源
      log-condition-evaluation-delta: false #关闭在什么情况下重启的日志记录,需要时可以打开

  thymeleaf:
    cache: false #使用Thymeleaf模板引擎,关闭缓存

  redis: #配置redis连接信息(单机模式)
    host: 192.168.92.134
    port: 6379
    password: #根据自己的本地配置情况设置


#在application.yml配置文件中指定映射文件的位置,这个配置只有接口和映射文件不在同一个包的情况下,才需要指定:
mybatis:
  mapper-locations: classpath:mapper/*.xml


3) 启动redis服务

4)  XinTuProductRedisController类

package com.xintu.demo.controller;

import com.xintu.demo.service.XinTuProductRedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
 * @author XinTu
 * @classname XinTuProductRedisController
 * @description TODO
 * @date 2023年05月05日 5:21
 */

@RestController
public class XinTuProductRedisController {
    @Autowired
    private XinTuProductRedisService productRedisService;

    @GetMapping(value = "/productredis/allProductNumber")
    public String allProductNumber(HttpServletRequest request) {
        Long allProductNumber = productRedisService.allProduct();
        return "商品数量:" + allProductNumber;
    }
}

5) XinTuProductRedisService实现

package com.xintu.demo.service;

import com.xintu.demo.entity.TProductExample;
import com.xintu.demo.mapper.TProductMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

/**
 * @author XinTu
 * @classname XinTuProductRedisService
 * @description TODO
 * @date 2023年05月05日 5:22
 */
@Service
public class XinTuProductRedisService {

    @Autowired
    private TProductMapper productMapper;

    // 注入 spring data当中的 RedisTemplate 类
    @Autowired
    private RedisTemplate redisTemplate;

    public Long allProduct() {
        //设置redisTemplate对象key的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //从redis缓存中获取总商品数
        Long productCount = (Long) redisTemplate.opsForValue().get("product_count");
        System.out.println("查询Redis数据库..."+productCount);
        //判断是否为空
        if (null == productCount) { //去mysql数据库查询,并存放到redis缓存中
            System.out.println("查询MySQL数据库...");
            TProductExample example = new TProductExample();
            productCount = productMapper.countByExample(example);
            redisTemplate.opsForValue().set("product_count",
                    productCount, 1, TimeUnit.SECONDS); // 会影响缓存穿透执行时长
        }
        return productCount;
    }

}

6)  启动类SpringbootApplication

package com.xintu.demo;

import com.xintu.demo.config.XinTuConfigInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@EnableTransactionManagement //开启事务
@RestController
@SpringBootApplication
public class SpringbootApplication {

	@Autowired
	private XinTuConfigInfo configInfo; //测试@ConfigurationProperties

	@Value("${test.site}")
	private String site;

	@Value("${test.user}")
	private String user;
	public static void main(String[] args) {
		SpringApplication.run(SpringbootApplication.class, args);
	}

	@GetMapping("/hello")
	public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
		return String.format("欢迎 %s 来到<a href=\"http://www.35xintu.com\">35新途</a>!", name);
	}

	@GetMapping("/value")
	public String testValue() { //测试 @Value 注解
		return String.format("欢迎 %s 来到<a href=\"http://www.35xintu.com\">%s</a>!" , user,site);
	}

	@GetMapping("/config")
	public String config() { //测试 @ConfigurationProperties 注解
		System.out.println("hello");
		return String.format("欢迎 %s 来到<a href=\"http://www.35xintu.com\">%s</a>!" , configInfo.getUser(),configInfo.getSite());
	}

}

7)  启动SpringBootCase应用,访问测试

http://localhost:8888/springbootcase/productredis/allProductNumber 

8)  打开Redis 客户端

启动命令:redis-cli.exe

查询命令:get product_count

3、缓存穿透现象

1) 穿透测试

XinTuRedisPenetrateController测试类。 

package com.xintu.demo.controller;

import com.xintu.demo.service.XinTuProductRedisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author XinTu
 * @classname XinTuRedisPenetrateController
 * @description 模拟缓存穿透
 * @date 2023年05月05日 6:00
 */

@RestController
public class XinTuRedisPenetrateController {
    @Autowired
    private XinTuProductRedisService productRedisService;

    @GetMapping(value = "/productredispenetrate/allProductNumber")
    public String allProductNumber(HttpServletRequest request) {
        Long allProductNumber = productRedisService.allProduct();
        //线程池个数,一般建议是CPU内核数 或者 CPU内核数据*2
        ExecutorService executorService = Executors.newFixedThreadPool(8);
        for (int i = 0; i < 2000; i++) {
            executorService.submit(new Runnable() {
                @Override public void run() {
                    productRedisService.allProduct();
                }
            });
        }
        return "商品数量:" + productRedisService.allProduct();
    }
}

2 )启动应用程序,浏览器访问测试

3)造成的问题

多个线程都去查询数据库,这种现象就叫做缓存穿透,如果并发比较大,对数据库的压力过大,有可能造成数据库宕机。

4)解决方法

方案一:加同步锁

修改StudentServiceImpl中的代码

public Long allProduct() {
        //设置redisTemplate对象key的序列化方式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //从redis缓存中获取总商品数
        Long productCount = (Long) redisTemplate.opsForValue().get("product_count");
        System.out.println("查询Redis数据库..."+productCount);
        //判断是否为空
        if (null == productCount) { //去mysql数据库查询,并存放到redis缓存中
            //设置同步代码块
            synchronized (this) { //加同步锁
                productCount = (Long) redisTemplate.opsForValue().get("product_count");
                if (null == productCount) { // 双重验证
                    System.out.println("查询MySQL数据库...");
                    TProductExample example = new TProductExample();
                    productCount = productMapper.countByExample(example);
                    redisTemplate.opsForValue().set("product_count",
                            productCount, 1, TimeUnit.SECONDS); // 会影响缓存穿透执行时长
                }
            }
        }
        return productCount;
}

启动应用程序,浏览器访问测试,查看控制台输出只有第一个线程查询数据库,其它线程查询Redis缓存,这样的解决的小问题就是第一批进来的用户会有一个等待,但是这样的影响可以忽略。

① 为什么要做双层验证?

防止线程获取到cpu执行权限的时候,其他线程已经将数据放到Redis中了,所以再次判断;

不能将synchronized范围扩大,因为如果Redis缓存中如果有数据,线程不应该同步,否则影响效率。

② 加同步锁是否是最优方案?

如何是在集群模式下,这种方式依然会有问题。这个时候就需要考虑采用redis分布式锁了,具体方案大家可以自行研究。

4、SpringBoot集成Redis哨兵模式(一主三从三哨兵)

6379是主节点,6380和6381是从节点。

分别修改每个redis.windows.conf和redis.windows-service.conf中的端口号为:6379(主节点保持不变)、6380、6381。

从节点配置文件需要加:

slaveof localhost 6379

从节点整体配置文件:

# 端口配置
port 6380
# 日志文件名
logfile "redis_log_6380.log"
# rdb持久化文件名字
dbfilename "dump6380.rdb"
# 本地ip
bind 127.0.0.1
# 绑定主从关系【该设置说明端口6380的服务为从机,它的主机为:6379】

# 从机是否只能读 默认是yes
slave-read-only no

4. 分别启动三台Redis服务器

主节点启动,

从节点启动,

验证主节点,

从节点验证,

5. 哨兵配置

#哨兵模式redis集群配置(哨兵模式)

  redis: #配置redis连接信息(单机模式)
    host: localhost
#    port: 6379 #f哨兵模式下不要写端口号
#    password: 123456
    sentinel:  #哨兵模式redis集群配置(哨兵模式)
      master: mymaster #与哨兵中的sentinel monitor xxx 保持一致
      nodes: localhost:26379,localhost:26380,localhost:26381

哨兵直接复制Redis文件目录即可。老王这里分为Redis-Sentinel-26379、Redis-Sentinel-26380、Redis-Sentinel-26381。

三个哨兵节点分别增加 sentinel.conf 文件, 文件内容如下:

# 哨兵sentinel实例运行的端口 默认26379
port 26379

# 保护模式
protected-mode no

# 本地ip
bind 127.0.0.1

# 哨兵监听的主服务器 后面的1表示主机挂掉以后进行投票,只需要2票就可以从机变主机
sentinel myid 9c65a6f7aad9e2419a6abce1ce56ff28cb81df34

# 设置主机的密码(无密码可以省略)
# sentinel auth-pass mymaster 35xintu

# 设置未得到主机响应时间,此处代表5秒未响应视为宕机
sentinel monitor mymaster 127.0.0.1 6380 2

# 设置等待主机活动时间,此处代表15秒主机未活动,则重新选举主机
sentinel down-after-milliseconds mymaster 5000

# 设置重新选举主机后,同一时间同步数据的从机数量,此处代表重新选举主机后,每次2台从机同步主机数据,直到所有从机同步结束
sentinel failover-timeout mymaster 15000

现在我们启动3个哨兵.

注意,启动redis主备集群时要先启动主,后启动从,哨兵先启动哪个都可以。

启动哨兵命令:redis-server.exe sentinel.conf --sentinel 

分被启动成功之后,就可以进行测试了。

1)验证主从数据同步

客户端连接命令:redis-cli.exe -h 127.0.0.1 -p 6380。

2) 主节点选举

停掉主节点:

验证主节点是否关闭,

哨兵模式中,进行重新选举,

然后看SpringBoot控制台,页切换为了6380. 

此时,说明哨兵模式已经生效,关于主从复制和哨兵机制的原理部分,会在后面的redis相关课程当中给大家详细分析,本篇注重的是SpringBoot集成Redis实战。

以上!可关注,持续输出优质内容!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/498032.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一图看懂 yarl 模块:为URL解析和更改提供了方便的URL类, 资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 yarl 模块&#xff1a;为URL解析和更改提供了方便的URL类, 资料整理笔记&#xff08;大全&#xff09; 摘要模块图类关系图模块全展开【yarl】统计常量模块1 yarl._quoting…

Python图形界面开发——系统资源监视器System-Monitor

Python图形界面程序怎么开发呢&#xff1f;很多人推荐python自带的tkinter自带库&#xff0c;还有pyqt这个这种拖拽式界面开发方案&#xff0c;但是他们开发界面比较难定制界面样式。现在web前端这么多框架用来开发python的图形界面其实不是很好&#xff1f;下面这么案例就是用…

Python爬虫 | 一文解决文章付费限制问题

本文概要 本篇文章主要介绍利用Python爬虫爬取付费文章&#xff0c;适合练习爬虫基础同学&#xff0c;文中描述和代码示例很详细&#xff0c;干货满满&#xff0c;感兴趣的小伙伴快来一起学习吧&#xff01; &#x1f31f;&#x1f31f;&#x1f31f;个人简介&#x1f31f;&…

项目内训(2023.5.6)

目录 Nacos是什么&#xff1f; 领域模型是什么&#xff1f; domain模块一般是干什么的&#xff1f; 在小乌龟中合并其他分支的作用是什么&#xff1f; nacos的配置文件 服务集群、服务提供、服务更加灵活庞大、消费服务、访问比较麻烦&#xff0c;A和B服务一起访问 系统结…

Qt5.9学习笔记-事件(四)Qt5.9中常见事件

⭐️我叫忆_恒心&#xff0c;一名喜欢书写博客的在读研究生&#x1f468;‍&#x1f393;。 如果觉得本文能帮到您&#xff0c;麻烦点个赞&#x1f44d;呗&#xff01; 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧&#xff0c;喜欢的小伙伴给个三…

5月1日 9H45min|5月2日 8H20min+30min|时间轴复盘

8:00 起床 8:00-8:30 洗漱吃饭 8:30-10:40 temporary pools阅读真题精读 (真的很慢了 不知道什么原因 感觉也没有彻底完全弄懂)【2h+10min】 10:40-11:10 午餐+酸奶(423+174KJ) 11:20-12:30 三篇阅读【1h+10min】 13:10-14:50 健身 14:50-15:45诵默写list…

【Stable Diffusion】ControlNet基本教程(四)

本文概要 接上篇【Stable Diffusion】ControlNet基本教程&#xff08;三&#xff09;&#xff0c;本篇再介绍两个ControlNet常见的基本用法&#xff1a;控制人物动作和建筑/室内生成。让人物摆出特定的动作&#xff0c;这是ControlNet最神级的操作&#xff01;这意味着可以自定…

密码学【java】初探究之springboo集成mybatis,swagger,数字签名

文章目录 项目环境一 swagger技术的补充1.1 [swagger](&#xff08;https://github.com/OAI/OpenAPI-Specification&#xff09;)介绍1.2 swagger的基础注解1.3 controller添加swagger注解 二 项目搭建2.1 创建数据库2.2 引入项目依赖2.3 配置数据库的连接2.4 配置swagger的配置…

USB 字节序,编码格式及位填充

字节序 LSB 发送一个字节时&#xff0c;先发送低位数据&#xff0c;再发送高位数据发送一个字时&#xff0c;先发送低字节数据&#xff0c;再发送高字节数据 例如&#xff1a; 发送 0x2D&#xff0c;发送的顺序为&#xff1a;10110100(低位在前&#xff0c;高位在后)发送 0…

项目前置准备

目录 项目前置准备 总体架构 CVPR2022是什么 一个项目架构图要如何进行看和学习呢&#xff1f;内容有点多有些摸不着头脑 我该如何理解架构图中的组件 Jenkins是什么&#xff1f; Docker是什么&#xff1f; FastDFS是什么&#xff1f; 项目前置准备 总体架构 CVPR2022是什…

golang grpc配置使用实战教程

什么是PRC&GRPC RPC是远程过程调用&#xff08;Remote Procedure Call&#xff09;的缩写形式, RPC 的主要功能目标是让构建分布式计算&#xff08;应用&#xff09;更容易&#xff0c;在提供强大的远程调用能力时不损失本地调用的语义简洁性。通俗地讲&#xff0c;使用RP…

软考 软件设计师 数据结构

大O表示法 常数阶&#xff0c;他的次数不会随着n的变大而变长 抓大头 取次方最大的 时间复杂度 没有循环 没有递归没有跟n相关的东西&#xff0c;那么他的复杂度就是o&#xff08;1&#xff09; 为什么ii*2那里会加1阿&#xff1f; 因为需要加一次才能跳出循环1 2 4 8 中间加…

有趣的回文检测

英文中有很多的回文词&#xff0c;回文词的拼法十分有趣&#xff0c;无论是从前往后拼读&#xff0c;还是从后往前拼读&#xff0c;他们的拼法和词义都不变。例如&#xff1a;dad&#xff08;爸爸&#xff09;&#xff0c;mum&#xff08;妈妈&#xff09;&#xff0c;noon&…

flac格式怎么转换mp3格式?

flac格式怎么转换mp3格式&#xff1f;什么是flac格式呢&#xff1f;通常来说&#xff0c;flac是一种无损音频压缩编码。flac格式主要特点就是无损压缩。对于flac格式而言&#xff0c;与其他有损压缩编码不同&#xff0c;比如与aac、mp3等相较而言&#xff0c;flac对原有的音频信…

PySide6/PyQT多线程之 生命周期:从创建到销毁的完整解析

前言 在PySide6/PyQT 中使用多线程时&#xff0c;多线程生命周期是一个重要的概念。如果不能正确地管理多线程生命周期&#xff0c;可能会导致程序崩溃、内存泄漏等问题。 在前面的文章中有介绍到 PySide6/PyQT 可以实现多线程的多个类&#xff0c; 有 QObject、QThread、QRun…

【C++刷题笔记】继承和多态常见面试题汇总

对C继承和多态方面的部分面试题进行了汇总 一、概念考察 1. 下面哪种面向对象的方法可以让你变得富有( ) A: 继承 B: 封装 C: 多态 D: 抽象 2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关&#xff0c;而对方法的调用则可以关联于具体…

双目测距--4 双目立体匹配 获取深度图

在这之前需要已经完成双目标定&#xff0c;这里是利用双目标定结果利用SGBM算法获取深度图&#xff0c;以及转伪彩图。 目录 StereoSGBM用到的参数&#xff1a; 一、 预处理参数 二 、代价参数 三 、动态规划参数 四、后处理参数 reprojectImageTo3D函数 获取真实距离 …

ChatGPT - 快速生成 流程图

文章目录 Prompt输出Copy 到 drawio Prompt 我想做一个研发标准化的流程,但是我是一个小白,不懂研发管理的流 程,我希望你作为一个经验丰富的技术管理人员,请帮我梳理一个完整流程,包括需求分析、概要设计,代码走查等等,输出的节点不少于18个,包含逻辑判断的分支,要通循实事求…

【SpringCloud微服务实践】服务注册与发现

注册与发现 在之前的示例中&#xff0c;采取的是硬编码的方式&#xff0c;需要调用的微服务的地址是被我们写死在文件或代码中的。在传统应用程序中&#xff0c;一般都是这么做的&#xff0c;然而这种方式存在不少缺陷&#xff1a; 静态配置&#xff1a;因为是写死的网络地址…

力扣sql中等篇练习(十五)

力扣sql中等篇练习(十五) 1 页面推荐 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 a 示例输入 b 示例输出 1.2 示例sql语句 # ①找到1所对应的朋友 ②找到其朋友喜欢的页面 ③删选掉自己喜欢的页面 # 可能朋友中存在喜欢同样的界面 SELECT distinct page_id reco…