智能BI项目第四期

开发图表管理功能

规划思路

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

规划功能设计

后端: 复用 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 ?? []);// 数据总数如果为空就返回0setTotal(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组件

<ListitemLayout="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改成图表的idkey={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"><ListitemLayout="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.Itemkey={item.id}// 在extra展示图表默认没有width(宽度),需要自己设置,无法适配// extra={// }><List.Item.Metaavatar={<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设置为truesetLoading(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 ?? '{}');// 把标题设为undefinedchartOption.title = undefined;// 然后把修改后的数据转换为json设置回去data.genChart = JSON.stringify(chartOption);})}} else {message.error('获取我的图表失败');}} catch (e: any) {message.error('获取我的图表失败,' + e.message);}// 获取数据后,加载完毕,设置为falsesetLoading(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 {@Resourceprivate ChartMapper chartMapper;@Testvoid 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) {// 创建RedissonClientRedissonClient 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对象@Beanpublic 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 {@Resourceprivate 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 {@Resourceprivate RedisLimiterManager redisLimiterManager;@Testvoid 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.xdnf.cn/news/1539595.html

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

相关文章

Ubuntu20.04 搜索不到任何蓝牙设备

电脑信息 联想扬天YangTianT4900k 问题描述 打开蓝牙之后&#xff0c;一直转圈&#xff0c;搜索不到任何蓝牙设备 排查 dmesg | grep -i blue 有如下错误&#xff1a; Bluetooth: hci0: RTL: unknown IC info, lmp subver 8852, hci rev 000b, hci ver 000b lsusb 芯片型号如…

基于springboot+vue图书管理系统的设计与实现

摘 要 传统信息的管理大部分依赖于管理人员的手工登记与管理&#xff0c;然而&#xff0c;随着近些年信息技术的迅猛发展&#xff0c;让许多比较老套的信息管理模式进行了更新迭代&#xff0c;图书信息因为其管理内容繁杂&#xff0c;管理数量繁多导致手工进行处理不能满足广…

C语言菜鸟入门·各种typedef用法超详细解析

目录 1. 什么是typedef 2. typedef的用法 2.1 对于数据类型的重定义 2.2 对于函数的重定义 2.3 对于指针的重定义 2.4 对于数组指针的重定义 2.5 对于指针数组的重定义 2.6 对于结构体的重定义&#xff08;typedef struct&#xff09; 2.6.1 对结构体起别名 …

2.4 数据库表字段约束

一、数据库三大范式 我们构造数据库的时候必须要遵守一定的原则&#xff0c;那这个规则就是范式关系型数据库&#xff0c;一共有六种范式&#xff0c;一般情况下只需要满足第三范式即可。 ​第一范式&#xff1a;原子性构造数据库必须遵循一定的规则&#xff0c;这种规则就是…

语音识别与语音控制的原理介绍

硬件平台 机器硬件&#xff1a;OriginBot(导航版/视觉版)PC主机&#xff1a;Windows&#xff08;>10&#xff09;/Ubuntu(>20.04)扩展硬件&#xff1a;X3语音版 运行案例 首先进入OriginBot主控系统&#xff0c;运行一下指令。请注意&#xff0c;部分操作OriginBot内暂…

深度学习笔记(8)预训练模型

深度学习笔记&#xff08;8&#xff09;预训练模型 文章目录 深度学习笔记&#xff08;8&#xff09;预训练模型一、预训练模型构建一、微调模型&#xff0c;训练自己的数据1.导入数据集2.数据集处理方法3.完形填空训练 使用分词器将文本转换为模型的输入格式参数 return_tenso…

docker从容器提取镜像并上传至dockerhub

一、使用commit从容器中提取镜像 例如 //docker commit 容器名 想要创建的镜像名:版本号 docker commit epsilon_planner epsilon_planner:latest导出完成后镜像如图所示 二、登陆dockerhub并创建仓库 登陆dockerhub&#xff0c;点击Create repository创建仓库&#xff0c…

【机器学习(八)】分类和回归任务-因子分解机(Factorization Machines,FM)-Sentosa_DSML社区版

文章目录 一、算法概念二、算法原理&#xff08;一&#xff09; FM表达式&#xff08;二&#xff09;时间复杂度&#xff08;三&#xff09;回归和分类 三、算法优缺点&#xff08;一&#xff09;优点&#xff08;二&#xff09;缺点 四、FM分类任务实现对比&#xff08;一&…

【ShuQiHere】 探索数据挖掘的世界:从概念到应用

&#x1f310; 【ShuQiHere】 数据挖掘&#xff08;Data Mining, DM&#xff09; 是一种从大型数据集中提取有用信息的技术&#xff0c;无论是在商业分析、金融预测&#xff0c;还是医学研究中&#xff0c;数据挖掘都扮演着至关重要的角色。本文将带您深入了解数据挖掘的核心概…

机械设备产品资料方案介绍小程序系统开发制作

设备产品资料介绍小程序系统&#xff0c;是一家工业机械设备生产厂家为了更好的服务客户而定制开发的一套小程序系统&#xff0c;让用户通过小程序就可以了解公司产品介绍的详细参数、售后服务和产品操作手持等。 该小程序系统里面主要开发的功能模块有&#xff1a; 1、产品目…

智慧课堂学生行为数据集

智慧校园数据集合集概述 智慧校园旨在通过整合先进的信息技术来提升教育环境的安全性、效率以及互动性。一个关键组成部分是利用计算机视觉技术对校园内的各种活动进行监控与分析。为此&#xff0c;构建了一个全面的数据集合集&#xff0c;包含了密集行人、头部检测、抽烟行为…

一个手机号注册3个抖音号的绿色方法?一个人注册多个抖音号的方法!

下面这是我注册的新账号&#xff0c;显示未实名&#xff0c;在手机号这里显示辅助手机号绑定&#xff0c;手机号绑定这里显示未绑定。如果你需要矩阵&#xff0c;那么&#xff0c;还需要设置好头像&#xff0c;以及介绍&#xff0c;这些都可以正常设置。 再好的方法&#xff0c…

C++笔记21•C++11的新特性•

相比于 C98/03&#xff0c;C11则带来了数量可观的变化&#xff0c;其中包含了约140个新特性&#xff0c;以及对C03标准中约600个缺陷的修正&#xff0c;这使得C11更像是从C98/03中孕育出的一种新语言。相比较而言&#xff0c;C11能更好地用于系统开发和库开发、语法更加泛华和简…

VS code 创建与运行 task.json 文件

VS code 创建与运行 task.json 文件 引言正文创建 .json 文件第一步第二步第三步 运行 .json 文件 引言 之前在 VS code EXPLORER 中不显示指定文件及文件夹设置&#xff08;如.pyc, pycache, .vscode 文件&#xff09; 一文中我们介绍了 settings.json 文件&#xff0c;这里我…

唯徳知识产权管理系统 UploadFileWordTemplate 任意文件读取

0x01 漏洞描述&#xff1a; 唯徳于2014年成立&#xff0c;是专业提供企业、代理机构知识产权管理软件供应商&#xff0c;某公司凭借领先的技术实力和深厚的专利行业积累&#xff0c;产品自上市推广以来&#xff0c;已为1000多家企业及代理机构提供持续稳定的软件服务。其知识产…

安卓13长按电源按键直接关机 andriod13不显示关机对话框直接关机

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.编译6.彩蛋1.前言 有些设备需要在长按电源键的时候,直接关机。不需要弹出对话框进行询问。 2.问题分析 过滤电源按键,需要在系统里面处理的话,那么我们需要熟悉android的事件分发,然后再…

L67 【哈工大_操作系统】操作系统历史 学习任务

L6 操作系统历史 线条一 1、上古神机 IBM7094 专注于计算批处理操作系统&#xff08;Batch system&#xff09; 2、OS/360 一台计算机干多种事&#xff0c;多道程序作业之间的 切换和调度 成为核心 &#xff08;多进程结构和进程管理概念萌芽&#xff01;&#xff09; 3…

链式栈讲解

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

《黑神话悟空》开发框架与战斗系统解析

本文主要围绕《黑神话悟空》的开发框架与战斗系统解析展开 主要内容 《黑神话悟空》采用的技术栈 《黑神话悟空》战斗系统的实现方式 四种攻击模式 连招系统的创建 如何实现高扩展性的战斗系统 包括角色属性系统、技能配置文件和逻辑节点的抽象等关键技术点 版权声明 本…

考研数据结构——C语言实现有向图邻接矩阵

首先&#xff0c;定义了一些基本的数据结构和常量&#xff1a; VertexType&#xff1a;顶点的数据类型&#xff0c;这里定义为char。EdgeType&#xff1a;边的数据类型&#xff0c;这里定义为int&#xff0c;用于存储权重。MAXVEX&#xff1a;定义了图中最大顶点数为100。INFIN…