智能BI项目第四期

news2024/9/20 5:43:52

开发图表管理功能

规划思路

首先需要做一个列表页。后端已经在星球提供了一个基础的万能项目模板,包含增删改查接口,我们只需要在此基础上进行定制化开发即可。所以本期后端的开发量不多,只需要复用即可,主要是前端。

规划功能设计

后端: 复用 springboot-init 初始化模板的增删改查代码 核心:获取个人创建的图表列表 listMyChartByPage

前端:

  1. 开发一个列表页
  2. 支持按照图表名称搜索
 前端开发
第一步

创建路由,进入routes.ts,图标可到 ant.design 组件库 挑选 , 点击图标自动复制。

  { name:'我的图表',path: '/my_chart', icon: 'pieChart', component: './MyChart' },

例如: , 可简写为:pieChart

(删除尾后Outlined , 首字母改为小写)

国内用户可以访问Ant Design 镜像网站

 

第二步

创建页面,复制AddChart目录。 粘贴至page目录下,并重命名为MyChart

第三步

这个时候可以试着访问一下,能不能访问嘚通

第四步

修改页面。 对MyChart目录下的index.tsx进行修改,把多余的内容删除。

第五步

获取数据:首先我们需要获取到最原始的数据,然后根据数据进行一步一步的美化处理。

import { listMyChartByPageUsingPOST } from '@/services/yubi/chartController';
import React, { useState } from 'react';

/**
 * 我的图表页面
 * @constructor
 */
const MyChartPage: React.FC = () => {
  // 把初始条件分离出来,便于后面恢复初始条件
  const initSearchParams = {
    // 初始情况下返回每页12条数据
    pageSize: 12,
  };
  /* 
    定义了一个状态(searchParams)和它对应的更新函数(setSearchParams),并初始化为initSearchParams;
    searchParams是我们要发送给后端的查询条件,它的参数类型是API.ChartQueryRequest;
     {...} 是展开语法,它将 initSearchParams 中的所有属性展开并复制到一个新对象中,而不改变原始对象,因此可以避免在现有对象上直接更改值的对象变异操作。
     因为在 React 中,不推荐直接修改状态或属性,而是创建一个新对象并将其分配给状态或属性,这个方法就非常有用。
  */
  const [searchParams, setSearchParams] = useState<API.ChartQueryRequest>({ ...initSearchParams });
  // 定义一个获取数据的异步函数
  const loadData = async () => {
    /* 
      调用后端的接口,并传入searchParams作为请求参数,返回一个响应res;
      listMyChartByPageUsingPOST方法是通过openapi根据Swagger接口文档自动生成的;
      当searchParams状态改变时,可以通过setSearchParams更新该状态并重新获取数据
    */
    const res = await listMyChartByPageUsingPOST(searchParams);
  }

  return (
    <div className="my-chart-page">

    </div>
  );
};
export default MyChartPage;

  在实际的开发中,前端和后端的职责是需要明确划分的。前端主要负责页面展示和与用户的交互,而后端则负责业务逻辑的实现和数据的处理。尽管前端的逻辑相对较少,但为了提高整个应用的性能和用户体验,我们应该尽可能地减少前端的计算复杂度,让后端来处理这些复杂的运算。这样,前端只需要调用后端的接口,传递需要的参数即可,后端则负责返回处理好的数据给前端,让前端根据数据进行页面展示。这样的划分可以使得前后端的开发更加高效和有效。

继续优化

import { listMyChartByPageUsingPOST } from '@/services/yubi/chartController';
import { message } from 'antd';
import React, { useEffect, useState } from 'react';

/**
 * 我的图表页面
 * @constructor
 */
const MyChartPage: React.FC = () => {

  const initSearchParams = {
    pageSize: 12,
  };

  const [searchParams, setSearchParams] = useState<API.ChartQueryRequest>({ ...initSearchParams });
  // 定义变量存储图表数据
  const [chartList, setChartList] = useState<API.Chart[]>();
  // 数据总数,类型为number,默认为0 
  const [total, setTotal] = useState<number>(0);

  const loadData = async () => {
    try {
      const res = await listMyChartByPageUsingPOST(searchParams);

      if (res.data) {
        // 如果成功,把图表数据回显到前端;如果为空,传一个空数组
        // 这里返回的是分页,res.data.records拿到数据列表
        setChartList(res.data.records ?? []);
        // 数据总数如果为空就返回0
        setTotal(res.data.total ?? 0);
      } else {
        // 如果后端返回的数据为空,抛出异常,提示'获取我的图表失败'
        message.error('获取我的图表失败');
      }

    } catch (e:any) {
      // 如果出现异常,提示'获取我的图表失败'+错误原因
      message.error('获取我的图表失败,' + e.message);
    }
  }

  // 首次页面加载时,触发加载数据
  useEffect(() => {
    // 这个页面首次渲染的时候,以及这个数组中的搜索条件发生变化的时候,会执行loadData方法,自动触发重新搜索
    loadData();
  },[searchParams]);

  return (
    <div className="my-chart-page">
      {/* 先把数据展示出来。直接展示对象会报错,所以要把后端拿到的对象数组进行格式化;把对象转为JSON字符串*/}
      数据列表:
      {JSON.stringify(chartList) }

      {/* 换行 */}
      <br/>
      总数:{total}
    </div>
  );
};
export default MyChartPage;
第六步

看看是否展示出了数据,想办法优化

 第七步

美化数据:这里需要引入 Ant Design 的列表组件(list),访问Ant Design 组件库; 找一个符合我们要求的,点击显示代码按钮。

复制List组件到return里的div标签中

第八步

修改List组件

<List
        itemLayout="vertical"
        size="large"
        pagination={{
          onChange: (page) => {
            console.log(page);
          },
          pageSize: 3,
        }}
        // 把数据源改成图表数据;列表组件就会自动把我们的数据列表展示成一条一条的形式
        dataSource={chartList}
        footer={
          <div>
            <b>ant design</b> footer part
          </div>
        }
        renderItem={(item) => (
          // List.Item就是你要怎么展示每一条数据
          <List.Item
            // key改成图表的id
            key={item.id}
            // 这里要展示图表(先不改)
            extra={
              <
                width={272}
                alt="logo"
                src="https://gw.alipayobjects.com/zos/rmsportal/mqaQswcyDLcXyDKnZfES.png"
                />
            }
            >
            {/* 你要展示的列表的元素信息 */}
            <List.Item.Meta
              // 先把头像写死
              avatar={<Avatar src={'https://randomuser.me/api/portraits/men/34.jpg'} />}
              // 图表的名称
              title={item.name}
              // 描述改成图表类型,如果没有图表类型,就不展示了
              description={item.chartType ? '图表类型' + item.chartType : undefined}
              />
            {/* 最终展示的内容 */}
            {'分析目标' + item.goal}
          </List.Item>
        )}
        />
      总数:{total}
    </div>

继续优化

import { listMyChartByPageUsingPOST } from '@/services/yubi/chartController';
import { Avatar, List, message } from 'antd';
import React, { useEffect, useState } from 'react';
import ReactECharts from 'echarts-for-react';

/**
 * 我的图表页面
 * @constructor
 */
const MyChartPage: React.FC = () => {
  const initSearchParams = {
    pageSize: 12,
  };

  const [searchParams, setSearchParams] = useState<API.ChartQueryRequest>({ ...initSearchParams });
  const [chartList, setChartList] = useState<API.Chart[]>();
  const [total, setTotal] = useState<number>(0);

  const loadData = async () => {
    try {
      const res = await listMyChartByPageUsingPOST(searchParams);

      if (res.data) {
        setChartList(res.data.records ?? []);
        setTotal(res.data.total ?? 0);
      } else {
        message.error('获取我的图表失败');
      }
    } catch (e: any) {
      message.error('获取我的图表失败,' + e.message);
    }
  };

  useEffect(() => {
    loadData();
  }, [searchParams]);

  return (
    <div className="my-chart-page">
      <List
        itemLayout="vertical"
        size="large"
        pagination={{
          onChange: (page) => {
            console.log(page);
          },
          pageSize: 3,
        }}
        dataSource={chartList}
        footer={
          <div>
            <b>ant design</b> footer part
          </div>
        }
        renderItem={(item) => (
          <List.Item
            key={item.id}
            // 在extra展示图表默认没有width(宽度),需要自己设置,无法适配
            // extra={
            // }
            >
            <List.Item.Meta
              avatar={<Avatar src={'https://randomuser.me/api/portraits/men/34.jpg'} />}
              title={item.name}
              description={item.chartType ? '图表类型' + item.chartType : undefined}
              />
            {'分析目标' + item.goal}	
             {/* 
              把在智能分析页的图表展示复制粘贴到此处;
              要把后端返回的图表字符串改为对象数组,如果后端返回空字符串,就返回'{}' 
            */}
            <ReactECharts option={JSON.parse(item.genChart ?? '{}')} />
          </List.Item>
        )}
        />
      总数:{total}
    </div>
  );
};
export default MyChartPage;

 这个时候访问可能会出现访问不了的情况,大概率是因为后端AI生成了脏数据

        可以通过检查genChart字段的数据,判断数据是否合法。比如:

  • 检查开头是否有中文;
  • 检查前后是否有回车、空行;
  • 检查 xAxis(yAxis、series、type、data等)是否被双引号包裹。 等等。
  • 这里的标题没必要再出现了
{
  "title": {
    "text": "用户增长情况"
  },
  "tooltip": {
    "trigger": "axis"
  },
  "legend": {
    "data": ["用户数"]
  },
  "grid": {
    "left": "3%",
    "right": "4%",
    "bottom": "3%",
    "containLabel": true
  },
  "toolbox": {
    "feature": {
      "saveAsImage": {}
    }
  },
  "xAxis": {
    "type": "category",
    "boundaryGap": false,
    "data": ["1号", "2号", "3号", "4号", "5号", "6号", "7号", "8号", "9号"]
  },
  "yAxis": {
    "type": "value"
  },
  "series": [
    {
      "name": "用户数",
      "type": "line",
      "stack": "总量",
      "data": [10, 20, 90, 70, 20, 50, 110, 0, 8]
    }
  ]
}

继续美化页面内容,对数据进行处理,统一隐藏图表标题、增加分页、搜索框。

import { listMyChartByPageUsingPOST } from '@/services/yubi/chartController';
import { useModel } from '@@/exports';
import {Avatar, Card, List, message} from 'antd';
import ReactECharts from 'echarts-for-react';
import React, { useEffect, useState } from 'react';
import Search from "antd/es/input/Search";

/**
 * 我的图表页面
 * @constructor
 */
const MyChartPage: React.FC = () => {
  const initSearchParams = {
    // 默认第一页
    current: 1,
    // 每页展示4条数据
    pageSize: 4,
  };

  const [searchParams, setSearchParams] = useState<API.ChartQueryRequest>({ ...initSearchParams });
  // 从全局状态中获取到当前登录的用户信息
  const { initialState } = useModel('@@initialState');
  const { currentUser } = initialState ?? {};
  const [chartList, setChartList] = useState<API.Chart[]>();
  const [total, setTotal] = useState<number>(0);
  // 加载状态,用来控制页面是否加载,默认正在加载
  const [loading, setLoading] = useState<boolean>(true);
  
  const loadData = async () => {
    // 获取数据中,还在加载中,把loading设置为true
    setLoading(true);
    try {
      const res = await listMyChartByPageUsingPOST(searchParams);
      if (res.data) {
        setChartList(res.data.records ?? []);
        setTotal(res.data.total ?? 0);
        // 有些图表有标题,有些没有,直接把标题全部去掉
        if (res.data.records) {
          res.data.records.forEach(data => {
            // 要把后端返回的图表字符串改为对象数组,如果后端返回空字符串,就返回'{}'
            const chartOption = JSON.parse(data.genChart ?? '{}');
            // 把标题设为undefined
            chartOption.title = undefined;
            // 然后把修改后的数据转换为json设置回去
            data.genChart = JSON.stringify(chartOption);
          })
        }
      } else {
        message.error('获取我的图表失败');
      }
    } catch (e: any) {
      message.error('获取我的图表失败,' + e.message);
    }
    // 获取数据后,加载完毕,设置为false
    setLoading(false);
  };

  useEffect(() => {
    loadData();
  }, [searchParams]);

  return (
    <div className="my-chart-page">
      {/* 引入搜索框 */}
      <div>
        {/* 
          当用户点击搜索按钮触发 一定要把新设置的搜索条件初始化,要把页面切回到第一页;
          如果用户在第二页,输入了一个新的搜索关键词,应该重新展示第一页,而不是还在搜第二页的内容
        */}
        <Search placeholder="请输入图表名称" enterButton loading={loading} onSearch={(value) => {
          // 设置搜索条件
          setSearchParams({
            // 原始搜索条件
            ...initSearchParams,
            // 搜索词
            name: value,
          })
        }}/>
      </div>
      <List
        /*
          栅格间隔16像素;xs屏幕<576px,栅格数1;
          sm屏幕≥576px,栅格数1;md屏幕≥768px,栅格数1;
          lg屏幕≥992px,栅格数2;xl屏幕≥1200px,栅格数2;
          xxl屏幕≥1600px,栅格数2
        */
        grid={{
          gutter: 16,
          xs: 1,
          sm: 1,
          md: 1,
          lg: 2,
          xl: 2,
          xxl: 2,
        }}
        pagination={{
          /*
            page第几页,pageSize每页显示多少条;
            当用户点击这个分页组件,切换分页时,这个组件就会去触发onChange方法,会改变咱们现在这个页面的搜索条件
          */
          onChange: (page, pageSize) => {
            // 当切换分页,在当前搜索条件的基础上,把页数调整为当前的页数
            setSearchParams({
              ...searchParams,
              current: page,
              pageSize,
            })
          },
          // 显示当前页数
          current: searchParams.current,
          // 页面参数改成自己的
          pageSize: searchParams.pageSize,
          // 总数设置成自己的
          total: total,
        }}
        // 设置成我们的加载状态
        loading={loading}
        dataSource={chartList}
        renderItem={(item) => (
          <List.Item key={item.id}>
            {/* 用卡片包裹 */}
            <Card style={{ width: '100%' }}>
              <List.Item.Meta
                // 把当前登录用户信息的头像展示出来
                avatar={<Avatar src={currentUser && currentUser.userAvatar} />}
                title={item.name}
                description={item.chartType ? '图表类型:' + item.chartType : undefined}
              />
              {/* 在元素的下方增加16像素的外边距 */}
              <div style={{ marginBottom: 16 }} />
              <p>{'分析目标:' + item.goal}</p>
              {/* 在元素的下方增加16像素的外边距 */}
              <div style={{ marginBottom: 16 }} />
              <ReactECharts option={item.genChart && JSON.parse(item.genChart)} />
            </Card>
          </List.Item>
        )}
      />
    </div>
  );
};
export default MyChartPage;

把常用的样式设定成固定的 css 样式(俗称:原子化 css); 找到global.less(全局样式)。 

 系统优化

现在的网站足够安全么?

a. 如果用户上传一个超大的文件怎么办?

b. 如果用户用科技疯狂点击提交,怎么办?

c. 如果 AI 的生成太慢(比如需要一分钟),又有很多用户要同时生成,给系统造成了压力,怎么兼顾用户体验和系统的可用性?

🚨 现在我们的网站有哪几方面都不足?

1. 安全性:如果用户上传一个超大的文件怎么办?比如 1000 G?

2. 数据存储:我们将每个图表的原始数据全部存放在同一个数据表(chart表)中,后期数据量大的情况下,会导致查询图表或查询 chart表等操作变得缓慢。 

3. 限流:在做真正上线的系统中,如果系统需要付费才能使用,比如每次用户调用聪明 AI 发送一条消息,AI 给出一个回答,这背后都需要进行成本的扣除。

只要涉及到用户自主上传的操作,一定要校验文件(图像) 校验的维度:

  1. 文件的大小
  2. 文件的后缀
  3. 文件的内容(成本要高一些)
  4. 文件的合规性(比如敏感内容,建议用第三方的审核功能) 扩展点:接入腾讯云的图片万象数据审核(COS 对象存储的审核功能)

后端校验

来到后端,找到ChartController.java下的genChartByAi接口,编写校验文件代码:

事实上,仅仅校验文件后缀并不能完全保证文件的安全性,因为攻击者可以通过将非法内容更改后缀名来绕过校验。通过修改文件后缀的方式欺骗校验机制,将一些恶意文件伪装成安全的文件类型。 现在这个校验的维度是从浅到深,仅仅依靠校验后缀是远远不够的,还需要结合其他严格措施来加强文件的安全性。给大家提供了一些思路,这是安全性的一个优化点。


  1. 一般文件多大考虑分片? 有人认为,当我们处理大型文件时,考虑分片上传可以提高上传速度和稳定性。分片上传可以使得当文件上传失败时,不用重新上传整个文件,而是只需要重新上传未完成的那部分分片。然而,对于分片上传的具体实现,建议使用现有的第三方组件,如腾讯云 TOS 对象存储,而不是自行实现。 因为没有一个标准的实现方式,自行实现可能会导致代码质量不稳定。一般来说,建议对于百兆到几个 G 的文件,考虑使用分片上传方式。对于大小不到十几兆的文件,可能没有必要进行分片上传。 如果能开发一个秒传系统,在简历中会起到很大的亮点作用,因为秒传这个功能涉及到技术含量较高的领域,如断点上传、文件校验和数据分片等。此外,还需要注意的是,秒传系统的实现需要考虑很多细节,例如如何保证文件的完整性和隐私安全,如何在高并发环境下实现高效的上传和下载等问题,这些也是最终系统能否得以成功运作的关键。
  2. @Validated 推荐用吗? 在选择技术时,往往需要根据具体的场景来进行判断和决策。当涉及到校验字段的规则时,是否采用@Validated 注解并没有一个绝对的答案,而是需要根据具体情况来考虑。如果你的校验规则相对简单,可以通过@Validated 注解中已经提供的一些规则来实现,那么直接使用@Validated 注解便是一个非常好的选择。 但如果你的校验规则比较复杂,可能涉及到多个条件和计算,这时候可以直接在业务代码中进行校验并灵活处理。所以说,我们需要根据具体的情况来选择合适的技术和方法来解决问题。

存在的不足

**现状:**我们把每个图表的原始数据全部存放在了同一个数据表(chart表)的字段里。 问题:

  1. 如果用户上传的原始数据量很大、图表数日益增多,查询 chart表就会很慢
  2. 对于 BI 平台,用户是有查看原始数据、对原始数据进行简单查询的需求的。现在如果把所有数据存放在一个字段(列)中,查询时,只能取出这个列的所有内容。

解决方案构思: 如果将原始数据以表格的形式存储在一个独立的数据表中,而不是放在一个小的格子里,实际上会更方便高效。由于数据表采用了标准的结构方式存储,我们可以通过使用 SQL 语句进行高效的数据检索,仅查询需要的列或行。 此外,我们还可以利用数据库的索引等高效技术,更快、更精确地对数据进行定位和查询,从而提高查询效率和系统的响应速度。

解决方案 => 分库分表: 把每个图表对应的原始数据单独保存为一个新的数据表,而不是都存在一个字段里。 比如:我们的网站数据.xlsx,如果要保存这个数据,就单独保存为一个新的数据表,表名为chart_{图表id}。 新建表,然后填入下图所示的数据,分开查询测试时会用到。

  1. 存储时,能够分开存储,互不影响(也能增加安全性)
  2. 查询时,可以使用各种 sql 语句灵活取出需要的字段,查询性能更快

优点构思: 使用分开存储的方式可以带来很多好处,其中一个好处就是存储的值相互独立,不会互相影响。例如,如果我们将一个 100 G 的数据保存到同一个表中,其他用户在访问这个数据表时会受到很大的影响,甚至在读取这个数据时可能会非常慢。 而通过将每个表单独存储,即使一个用户上传了很大的数据,其他用户在访问时也不会受到影响。这样可以保证数据的安全性和稳定性,同时也能提高系统的处理能力和效率。 以后进行图表数据查询时,可以先根据图表的 ID 来查找,然后进行数据查询,方便我们排查问题。甚至返回用户原始数据,通过全标扫描的方式直接捞出所有数据,这比对数据库查询数据进行处理更加快速和高效。

💡 分库分表的思路: 在数据库设计中考虑使用分库分表的思路可以有效地解决大数据量和高并发的问题。可以分水平分表和垂直分库两种方式。 水平分表指在数据量大的情况下,将表按照某个字段的值进行拆分和分散存储,例如拆分出前 1 万个用户一个表,后 1 万个用户一个表。 垂直分库则是将不同的业务按照相关性进行划分,例如将用户中心用户相关的内容划分到一个库中,订单、支付信息和订单相关的划分到另一个库中,从而提高系统的可扩展性和稳定性。 分库分表是数据库设计中重要的一部分,能有效地优化系统的性能,提高用户体验,也是一个优秀的简历亮点。

 分库分表介绍:

在大型互联网应用中,为了应对高并发、海量数据等挑战,往往需要对数据库进行拆分。常见的拆分方式有水平分表和垂直分库两种。

水平分表(Sharding) 

水平分表是将同一张表中的数据按一定的规则划分到不同的物理存储位置上,以达到分摊单张表的数据及访问压力的目的。对于 SQL 分为两类:id-based 分表和 range-based 分表。

水平分表的优点:

  • 单个表的数据量减少,查询效率提高。
  • 可以通过增加节点,提高系统的扩展性和容错性。

水平分表的缺点:

  • 事务并发处理复杂度增加,需要增加分布式事务的管理,性能和复杂度都有所牺牲。
  • 跨节点查询困难,需要设计跨节点的查询模块。
垂直分库(Vertical Partitioning)

 垂直分库,指的是根据业务模块的不同,将不同的字段或表分到不同的数据库中。垂直分库基于数据库内核支持,对应用透明,无需额外的开发代码,易于维护升级。

垂直分库的优点:

  • 减少单个数据库的数据量,提高系统的查询效率。
  • 增加了系统的可扩展性,比水平分表更容易实现。

垂直分库的缺点:

  • 不同数据库之间的维护和同步成本较高。
  • 现有系统的改造存在一定的难度。
  • 系统的性能会受到数据库之间互相影响的影响。

需要根据实际的业务场景和技术架构情况,综合考虑各种因素来选择适合自己的分库分表策略。

<!--  
queryChartData唯一标识符;parameterType查询语句的参数类型,类型为字符串;
resultType查询结果的返回类型,类型为map类型;
${querySql}是SQL查询语句的占位符;
select * from chart_#{chartId} 不够灵活,${querySql}是最灵活的方式,
就是把sql语句完全交给程序去传递,有一定的风险;
一旦使用$符号,就有sql注入的风险。
-->
<select id="queryChartData" parameterType="string" resultType="map">
  ${querySql}
</select>
<!-- 
可以在程序里面去做校验。只要保证这个SQL是通过你的后端生成的,
在生成的过程中做了校验,就不会有这种漏洞的风险。 
-->
package com.yupi.springbootinit.mapper;

import com.yupi.springbootinit.model.entity.Chart;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.util.List;
import java.util.Map;

/**
 * @Entity com.yupi.springbootinit.model.entity.Chart
 */
public interface ChartMapper extends BaseMapper<Chart> {
   /*
    * 方法的返回类型是 List<Map<String, Object>>,
    * 表示返回的是一个由多个 map 组成的集合,每个map代表了一行查询结果,
    * 并将其封装成了一组键值对形式的对象。其中,String类型代表了键的类型为字符串,
    * Object 类型代表了值的类型为任意对象,使得这个方法可以适应不同类型的数据查询。
    *
    */
    List<Map<String, Object>> queryChartData(String querySql);
}

 然后创建测试类,进行测试

把光标放在类名上,Alt + 回车,会有创建测试类的快捷方式,使用junit5

 

package com.yupi.springbootinit.mapper;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class ChartMapperTest {

    @Resource
    private ChartMapper chartMapper;

    @Test
    void queryChartData() {
        String chartId = "1659210482555121666";
        String querySql = String.format("select * from chart_%s", chartId);
        List<Map<String, Object>> resultData = chartMapper.queryChartData(querySql);
        System.out.println(resultData);
    }
}

限流

使用系统是需要消耗成本的,用户有可能疯狂刷量,让你破产。 解决方案:

  1. 控制成本 => 限制用户调用总次数
  2. 用户在短时间内疯狂使用,导致服务器资源被占满,其他用户无法使用 => 限流

思考: 限流阈值多大合适?参考正常用户的使用,比如限制单个用户在每秒只能使用 1 次。

建议 阅读文章。

本地限流(单机限流)

每个服务器单独限流,一般适用于单体项目,就是你的项目只有一个服务器 。

举个例子,假设你的系统有三台服务器,每台服务器限制用户每秒只能请求一次。你可以为每台服务器单独设置限流策略,这样每个服务器都能够独立地控制用户的请求频率。但是这种限流方式并不是很可靠,因为你并不知道用户的请求会落在哪台服务器上,它的分布是有一定的偶然性的。即使你采用负载均衡技术,让用户请求轮流发送到每台服务器,仍然存在一定的风险。

在 Java 中,有很多第三方库可以用来实现单机限流: Guava RateLimiter:这是谷歌 Guava 库提供的限流工具,可以对单位时间内的请求数量进行限制。

分布式限流(多机限流) 

如果你的项目有多个服务器,比如微服务,那么建议使用分布式限流。

  1. 把用户的使用频率等数据放到一个集中的存储进行统计; 比如 Redis,这样无论用户的请求落到了哪台服务器,都以集中存储中的数据为准。 (Redisson -- 是一个操作 Redis 的工具库)
  2. 在网关集中进行限流和统计(比如 Sentinel、Spring Cloud Gateway)
import org.redisson.Redisson;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;

public static void main(String[] args) {
    // 创建RedissonClient
    RedissonClient redisson = Redisson.create();

    // 获取限流器
    RSemaphore semaphore = redisson.getSemaphore("mySemaphore");

    // 尝试获取许可证
    boolean result = semaphore.tryAcquire();
    if (result) {
        // 处理请求
    } else {
        // 超过流量限制,需要做何处理
    }
}
Redisson 限流实现

[官方项目仓库和文档]

1.引入依赖
<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.36.0</version>
</dependency>  
2.创建 RedissonConfig 配置类

    用于初始化 RedissonClient 对象单例; 在config目录下新建RedissonConfig.java

package com.yupi.springbootinit.config;

import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
// 从application.yml文件中读取前缀为"spring.redis"的配置项
@ConfigurationProperties(prefix = "spring.redis")
@Data
public class RedissonConfig {

    private Integer database;

    private String host;

    private Integer port;
    // 如果redis默认没有密码,则不用写
    //private String password;

    // spring启动时,会自动创建一个RedissonClient对象
    @Bean
    public RedissonClient getRedissonClient() {
        // 1.创建配置对象
        Config config = new Config();
        // 添加单机Redisson配置
        config.useSingleServer()
        // 设置数据库
        .setDatabase(database)
        // 设置redis的地址
        .setAddress("redis://" + host + ":" + port);
        // 设置redis的密码(redis有密码才设置)
        //                .setPassword(password);

        // 2.创建Redisson实例
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

怎么知道 redis 有没有密码? 在本地安装的 redis 目录下找到redis-server.exe,双击启动,放那别关掉。

然后在 redis 目录下找到redis-cli.exe,输入命令config get requirepass。 没有设置密码,所以2)为空。

3.创建 redis 客户端

去写一个管理类; 在manager目录下创建RedisLimiterManager.java

package com.yupi.springbootinit.manager;

import com.yupi.springbootinit.common.ErrorCode;
import com.yupi.springbootinit.exception.BusinessException;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 专门提供 RedisLimiter 限流基础服务的(提供了通用的能力,放其他项目都能用)
 */
@Service
public class RedisLimiterManager {

    @Resource
    private RedissonClient redissonClient;

    /**
     * 限流操作
     *
     * @param key 区分不同的限流器,比如不同的用户 id 应该分别统计
     */
    public void doRateLimit(String key) {
        // 创建一个名称为user_limiter的限流器,每秒最多访问 2 次
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
        // 限流器的统计规则(每秒2个请求;连续的请求,最多只能有1个请求被允许通过)
        // RateType.OVERALL表示速率限制作用于整个令牌桶,即限制所有请求的速率
        rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
        // 每当一个操作来了后,请求一个令牌
        boolean canOp = rateLimiter.tryAcquire(1);
        // 如果没有令牌,还想执行操作,就抛出异常
        if (!canOp) {
            throw new BusinessException(ErrorCode.TOO_MANY_REQUEST);
        }
    }
}

大家看不懂源码的话,点击到源码的包里。右上方有一个download下载按钮,就可以看解析了

4.测试

同样将鼠标放在类上,Alt + Enter,会出现创建测试类的快捷键,使用junit5

package com.yupi.springbootinit.manager;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class RedisLimiterManagerTest {

    @Resource
    private RedisLimiterManager redisLimiterManager;

    @Test
    void doRateLimit() throws InterruptedException {
        // 模拟一下操作
        String userId = "1";
        // 瞬间执行2次,每成功一次,就打印'成功'
        for (int i = 0; i < 2; i++) {
            redisLimiterManager.doRateLimit(userId);
            System.out.println("成功");
        }
        // 睡1秒
        Thread.sleep(1000);
        // 瞬间执行5次,每成功一次,就打印'成功'
        for (int i = 0; i < 5; i++) {
            redisLimiterManager.doRateLimit(userId);
            System.out.println("成功");
        }
    }
}

5.应用

在controller层中注入RedisLimiterManager

// 引用
@Resource
private RedisLimiterManager redisLimiterManager;


// 限流判断,每个用户一个限流器
redisLimiterManager.doRateLimit("genChartByAi_" + loginUser.getId());

 优化点:实现分库分表操作,减小查询压力

             开发编辑图表的功能,允许用户再次发送请求

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

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

相关文章

【IPV6从入门到起飞】5-4 IPV6+Home Assistant(ESP32+MQTT+ILI9488)远程留言墙

IPV6Home Assistant[ESP32MQTTILI9488]远程留言墙 1 背景2 Home Assistant 配置2-1 配置 yaml2-2 效果 3 ESP32 配置3-1 使用 TFF_eSPI 库3-2 修改默认的SPI屏幕配置文件3-3 接线3-4 ESP32 工程代码 4 测试4-1 留言板设置内容4-2 ESP32 屏幕显示 5 后记 1 背景 在前面我们的几…

自动驾驶中的决策规划技术分享--轻舟智航

文章目录 0.概述&#xff1a;1 导航模块2 决策模块2.1 车道决策2.2 障碍物决策 3 轨迹规划3.1 时空分离规划3.2 时空联合规划 4 对比 0.概述&#xff1a; 李仁杰&#xff0c;轻舟智航规划算法负责人&#xff0c;自动驾驶决策与规划技术专家。 在自动驾驶系统中&#xff0c;决策…

Win10 录屏秘籍大公开:从新手到高手的进阶之路

之前因为某些原因不方便到客户那里进行软件培训&#xff0c;我们就发现录屏讲解供客户随时查看的方式好像更有效果。这次我就介绍一些能够实现win10怎么录屏操作的工具讲解。 1.福昕录屏大师 链接&#xff1a;www.foxitsoftware.cn/REC/ 这个工具是一款专业的电脑录屏软件&a…

SVN泄露 CTFHUB 解题笔记

参考大佬链接CTFHub | SVN泄露_ctfhubsvn泄露-CSDN博客 先下载插件 然后把GIT&#xff1b;里面的代码 乱盘上去 python2 不知道需不需要 先装了 再说。。。我的是裸机~ 开始作妖模式 Ubuntu 22.10 | Installati.one 上面一行的代码 链接 下面 插件 GITHUB页面下面的代码 d…

企业网络安全关键:防御措施和应急响应

感谢浪浪云支持发布 浪浪云活动链接 &#xff1a;https://langlangy.cn/?i8afa52 文章目录 什么是网络安全常见的网络安全威胁病毒和恶意软件网络钓鱼拒绝服务攻击中间人攻击社会工程学 基本的网络安全措施强密码策略双因素认证安装和更新防病毒软件定期备份 高级的网络安全方…

Java-面向对象编程(基础部分)

类和对象的区别和联系 类&#xff1a;类是封装对象的属性和行为的载体&#xff0c;在Java语言中对象的属性以成员变量的形式存在&#xff0c;而对象的方法以成员方法的形式存在。 对象&#xff1a;Java是面向对象的程序设计语言&#xff0c;对象是由类抽象出来的&#xff0c;…

使用 MobaXterm 远程连接 Linux 虚拟机并实现文件传输

文章目录 前言一、什么是 MobaXterm二 、MobaXterm 安装三、使用 MobaXterm 远程连接 Linux 虚拟机1. 准备工作2. 创建 SSH 连接3. 登录虚拟机 四、使用 MobaXterm 进行文件传输总结 前言 在日常开发和运维中&#xff0c;Windows 用户经常需要通过远程连接到 Linux 服务器进行…

链式栈讲解

文章目录 &#x1f34a;自我介绍&#x1f34a;链式栈入栈和出栈linkstack.hlinkstack.c 你的点赞评论就是对博主最大的鼓励 当然喜欢的小伙伴可以&#xff1a;点赞关注评论收藏&#xff08;一键四连&#xff09;哦~ &#x1f34a;自我介绍 Hello,大家好&#xff0c;我是小珑也要…

UBUNTU20.04安装CH384串口卡驱动

继续上文&#xff1a;统信UOS安装CH384串口卡驱动-CSDN博客 统信UOS系统成功安装CH384串口驱动后&#xff0c;继续在ubuntu20.04下安装驱动&#xff0c;发现一直报错&#xff0c;原因是内核驱动不一致。 解决办法&#xff1a; 1. 下载最新的驱动。CH35XCH384驱动源文件资源-C…

【WPF】02 按钮控件圆角配置及状态切换

按钮圆角 先从工具箱里拖进来一个Button控件&#xff0c;然后对这个按钮进行美化。 首先在 xaml 里按钮控件部分 添加如下代码&#xff1a; <Button x:Name"btnLogin" Content"登录" HorizontalAlignment"Center" Margin"0,399,0,0&q…

报错合计-1

向开发描述&#xff1a;先勾选一个病灶后复制&#xff0c;控制台报错 报错类型查询后为前端DOM节点相关报错&#xff0c;提给前端开发 报错解释&#xff1a; TypeError: Failed to execute selectNode on Range: parameter 1 is not of type Node 这个错误表明你尝试使用 Range…

【机器学习】:深潜智能的底层逻辑、前沿探索与未来展望】

欢迎来到 破晓的历程的 博客 ⛺️不负时光&#xff0c;不负己✈️ 在科技的浩瀚星空中&#xff0c;机器学习犹如一颗璀璨的新星&#xff0c;以其独特的魅力和无限潜力&#xff0c;引领着我们向智能的深处探索。今天&#xff0c;我们将一同踏上这场深度之旅&#xff0c;不仅解析…

AI时代,服务器厂商能否打破薄利的命运?

文&#xff5c;刘俊宏 编&#xff5c;王一粟 AI大模型正在引发新一轮的“算力焦渴”。 近日&#xff0c;OpenAI刚发布的o1大模型再次刷新了大模型能力的上限。对比上一次迭代的版本&#xff0c;o1的推理能力全方位“吊打”了GPT-4o。更优秀的能力&#xff0c;来自与o1将思维…

Flink官方文档

Flink官方文档&#xff08;全面、详细&#xff09;&#xff1a;https://nightlies.apache.org/flink/flink-docs-master/zh/

【Verilog学习日常】—牛客网刷题—Verilog快速入门—VL24

边沿检测 有一个缓慢变化的1bit信号a&#xff0c;编写一个程序检测a信号的上升沿给出指示信号rise&#xff0c;当a信号出现下降沿时给出指示信号down。 注&#xff1a;rise,down应为单脉冲信号&#xff0c;在相应边沿出现时的下一个时钟为高&#xff0c;之后恢复到0&#xff0…

116页PPT麦肯锡方法详解-用简单的方法做复杂的事

读者朋友大家好&#xff0c;最近有会员朋友咨询晓雯&#xff0c;需要《116页PPT麦肯锡方法详解-用简单的方法做复杂的事》资料&#xff0c;欢迎大家文末扫码下载学习。 以下是在实际工作中应用麦肯锡问题解决法的具体方式&#xff1a; 一、项目管理与流程优化领域 界定问题 …

再看Java-笔试

放在前面的话 最近确实有些空闲&#xff0c;分配的功能从一开始的两眼一黑到现在的一上午就能完成&#xff0c;这何尝不是一种进步呢。 该说不说&#xff0c;海康的API问题相比较其他第三方的API还是蛮多的&#xff0c;而且10月份人工客服还会停运&#xff0c;不过到那个时候…

LeetCode54. 螺旋矩阵(2024秋季每日一题 21)

给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5] 示例 2&#xff1a; 输入&#xff1a;matrix [[1,…

由于安全风险,安全领导者考虑禁止人工智能编码

安全团队与开发团队之间的紧张关系 83% 的安全领导者表示&#xff0c;他们的开发人员目前使用人工智能来生成代码&#xff0c;57% 的人表示这已成为一种常见做法。 然而&#xff0c;72% 的人认为他们别无选择&#xff0c;只能允许开发人员使用人工智能来保持竞争力&#xff0…

【Unity】对象池 - 未更新完

自定义泛型对象池 文章目录 自定义泛型对象池封装泛型类例子 使用Unity自带对象池 封装泛型类 public abstract class MyPool<T> : MonoBehaviour where T :Component {[SerializeField] protected T prefab; // 生成的预制体[SerializeField] protected int defaultNum…