往期开发(已经完成)

今日后端开发过程

  • 系统优化

    • 提出问题

      • 1.如果用户上传一个很大的文件,例如 100G、200G 等超大文件

      • 2.用户使用第三方软件进行疯狂提交。

      • 3.从前几次 AI 绘制情况来看速度很慢,如果多个用户同时需要生成,给系统造成了巨大的压力,如何不影响用户体验的情况下维护系统的可用性。

    • 解决问题

      • 1.如果用户上传很大的文件

        涉及到用户自主上传操作,必须校验文件

        校验的角度:

        1. 文件的大小
        2. 文件的后缀
        3. 文件的内容(成本较高)
        4. 文件的合规性(比如敏感内容,建议使用第三方的审核功能来进行)

        扩展点:接入腾讯云的图片万象数据审核(COS 对象存储的审核功能)

        1
        2
        3
        4
        5
        // 文件校验
        long size = multipartFile.getSize();
        String originalFilename = multipartFile.getOriginalFilename();
        ThrowUtils.throwIf(size > MAX_FILE_SIZE, ErrorCode.PARAMS_ERROR, "文件过大不得超过1MB");
        ThrowUtils.throwIf(!FILE_NAME_LIST.contains(FileUtil.getSuffix(originalFilename)), ErrorCode.PARAMS_ERROR, "不支持此文件");
      • 2.数据存储

        目前,AI 分析的原始数据都存储到了一张表中的一个字段(chart 表)

        问题:

        1. 用户上传的原始数据量很大,图表日益增多,查询 Chart 表就会很慢。
        2. 对于 BI 平台,用户可以对原始数据进行定制分析,只取一行或者一列进行分析,目前的存储方式只能取出该字段的所有内容

        解决方案

        • 分库分表

          把每个图标对应的原始数据单独保存为一个新的数据表,而不存在一个字段里面

          格式 表名+此次分析的 ID 额外在创建一张表

          优点:

          1. 存储时,能够分开进行存储,互相不影响,增加了安全性
          2. 查询时,可以使用各种 sql 语句灵活的提取需要的字段,查询性能更快
  • 分库分表

    • 水平分表

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

      水平分表的优点:

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

      水平分表的缺点:

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

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

      垂直分库的优点:

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

      垂直分库的缺点:

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

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

  • 限流

    • 提出问题

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

    解决:控制成本,限制用户调用总次数。用户在短时间内疯狂使用,导致服务器资源被占满,其他用户无法使用,需要进行限流。

    • 限流的几种做法

      访问链接:https://juejin.cn/post/6967742960540581918

      1. 固定窗口限流

        单位时间内允许部分操作
        优点:最简单
        缺点:可能出现流量突刺,产生流量高峰

      2. 滑动窗口限流

        单位时间内允许部分操作,但是时间单位是滑动的,需要指定一个滑动单位
        优点:能够解决流量突刺问题
        缺点:实现相对复杂,并且与滑动单位有关,滑动单位越小,限流效果越好,但是很难选取到合适的滑动单位

      3. 漏桶窗口限流

        以固定的速率处理请求,当请求桶满了之后,拒绝请求
        优点:能够一定程度上应对流量突刺,能够固定速率处理请求,保证服务器的安全
        缺点:没有办法迅速处理一批请求,只能一批一批的按顺序来处理

      4. 令牌桶限流(推荐)

        管理员在访问钱生成一批令牌,每隔一段时间生成令牌,用户操作之前要先获取领盘才能够执行操作,拿不到令牌的就需要等待
        优点:能够并发处理请求,并发性能会更高
        需要考虑:还是会存在时间单位的选取问题

    • 限流的粒度

      1. 针对某个方法限流,即单位时间内最多允许同时 XX 个操作使用这个方法

      2. 针对某个用户限流,比如单个用户单位时间内最多执行 XX 次操作

      3. 针对某个用户 x 方法限流,比如单用户单位时间内最多执行 XX 次这个方法

    • Redisson 限流的实现

      1. 引入 Redisson 的依赖包

        1
        2
        3
        4
        5
        <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson</artifactId>
        <version>3.21.3</version>
        </dependency>
      2. 创建 RedissonConfig 配置类,用户初始化 RedissonClient 对象单例

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        @Configuration
        @ConfigurationProperties(prefix = "spring.redis")
        @Data
        public class RedissonConfig {

        private Integer database;

        private String host;

        private Integer port;

        private String password;

        @Bean
        public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer()
        .setDatabase(database)
        .setAddress("redis://" + host + ":" + port);
        RedissonClient redisson = Redisson.create(config);
        return redisson;
        }
        }
      3. 编写 RedisLimiterManager

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        /**
        * 限流操作
        *
        * @param key 区分不同的限流器,比如不同的用户 id 应该分别统计
        */
        public void doRateLimit(String key) {
        // 创建一个名称为user_limiter的限流器,每秒最多访问 2 次
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
        rateLimiter.trySetRate(RateType.OVERALL, 2, 1, RateIntervalUnit.SECONDS);
        // 每当一个操作来了后,请求一个令牌
        boolean canOp = rateLimiter.tryAcquire(1);
        if (!canOp) {
        throw new BusinessException(ErrorCode.TOO_MANY_REQUEST);
        }
        }
      4. 完善 AI 对话接口增加代码如下:

        1
        redisLimiterManager.doRateLimit("genChartByAI_" + loginUser.getId());
  • 完整的 AI 对话代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    /**
    * AI对话分析
    *
    * @param multipartFile
    * @param genChartByAiRequest
    * @param request
    * @return
    */
    @PostMapping("/gen")
    public BaseResponse<BiResponse> genChartByAI(@RequestPart("file") MultipartFile multipartFile,
    GenChartByAiRequest genChartByAiRequest, HttpServletRequest request) {
    // 第二天内容
    // 参数获取
    String goal = genChartByAiRequest.getGoal();
    String name = genChartByAiRequest.getName();
    String chartType = genChartByAiRequest.getChartType();
    // 参数校验
    ThrowUtils.throwIf(StringUtils.isEmpty(goal), ErrorCode.PARAMS_ERROR, "目标不能为空");
    ThrowUtils.throwIf(StringUtils.isEmpty(chartType), ErrorCode.PARAMS_ERROR, "类型不能为空");
    ThrowUtils.throwIf(StringUtils.isEmpty(name), ErrorCode.PARAMS_ERROR, "图表名称不能为空");
    // 文件校验
    long size = multipartFile.getSize();
    String originalFilename = multipartFile.getOriginalFilename();
    ThrowUtils.throwIf(size > MAX_FILE_SIZE, ErrorCode.PARAMS_ERROR, "文件过大不得超过1MB");
    ThrowUtils.throwIf(!FILE_NAME_LIST.contains(FileUtil.getSuffix(originalFilename)), ErrorCode.PARAMS_ERROR, "不支持此文件");
    // 判断是否登录
    User loginUser = userService.getLoginUser(request);
    ThrowUtils.throwIf(Objects.isNull(loginUser), ErrorCode.NOT_LOGIN_ERROR);
    redisLimiterManager.doRateLimit("genChartByAI_" + loginUser.getId());
    // 用户消息拼接
    StringBuilder userMessageBuilder = new StringBuilder();
    userMessageBuilder.append("分析需求:").append("\n");
    String newGoal = goal + ",请使用" + chartType;
    userMessageBuilder.append(newGoal).append("\n");
    userMessageBuilder.append("原始数据:").append("\n");
    String dataStr = ExcelUtils.excelToCsv(multipartFile);
    userMessageBuilder.append(dataStr);
    String content = aiManager.doChat(userMessageBuilder.toString());
    String[] splits = content.split("【【【【【");
    if (splits.length != 3) {
    throw new RuntimeException("AI 生成错误");
    }
    BiResponse biResponse = new BiResponse();
    Chart chart = new Chart();
    chart.setUserId(loginUser.getId());
    chart.setExecMessage(userMessageBuilder.toString());
    chart.setGoal(goal);
    chart.setName(name);
    chart.setChartType(chartType);
    chart.setChartData(dataStr);
    chart.setGenResult(splits[2].trim());
    chart.setGenChart(splits[1].trim());
    boolean isSave = chartService.save(chart);
    ThrowUtils.throwIf(!isSave, ErrorCode.SYSTEM_ERROR, "图表保存失败!");
    biResponse.setGenChart(splits[1].trim());
    biResponse.setGenResult(splits[2].trim());
    biResponse.setChartId(chart.getId());
    return ResultUtils.success(biResponse);
    }
  • 后端收获

    数据的分库分表存储,使用的场景以及对于数据的处理。对接口进行限流,学习了四种的分流算法,主要推荐令牌桶限流,效果好,代码实现不复杂。

今日前端开发过程

  • 添加路由,创建页面

    路由

  • 对接后端接口,发送请求加载数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    const myChart: React.FC = () => {
    const { initialState } = useModel('@@initialState');
    const { currentUser } = initialState || {};

    const [loading, setLoading] = useState<boolean>();

    const initialSearchParams = {
    current: 1,
    pageSize: 6,
    };

    const [searchParams, setSearchParams] = useState<API.ChartQueryRequest>({
    ...initialSearchParams,
    });

    const [chartList, setChartList] = useState<API.Chart[]>();

    const [total, setTotal] = useState<number>();

    const loadTableData = async () => {
    setLoading(true);
    try {
    const res = await listChartByPageUsingPOST(searchParams);
    if (res.data?.records) {
    const teChartList: API.Chart[] = [];
    res.data?.records.forEach((element: API.Chart) => {
    if (element.genChart) {
    try {
    const genChartParse = JSON.parse(element.genChart);
    if (genChartParse?.title) {
    genChartParse.title = undefined;
    element.genChart = JSON.stringify(genChartParse);
    }
    teChartList.push(element);
    } catch (e) {
    message.error(element.name + '图表,数据有误!');
    }
    }
    });
    setChartList(teChartList);
    setTotal(res.data?.total ?? 0);
    } else {
    message.error('获取我的图表失败');
    }
    } catch (e: any) {
    message.error('21321获取我的图表失败:' + e.message);
    }
    setLoading(false);
    };
    // searchParams发生变化时候进行调用
    useEffect(() => {
    const use = loadTableData();
    }, [searchParams]);
    return (
    <div className="my-chart">
    <div>
    <Search
    placeholder="请输入图表名称"
    enterButton
    onSearch={(value) => {
    setSearchParams({
    ...initialSearchParams,
    name: value,
    });
    }}
    loading={loading}
    style={{ width: 200 }}
    />
    </div>
    <List
    itemLayout="vertical"
    loading={loading}
    pagination={{
    onChange: (page, pageSize) => {
    setSearchParams({
    ...searchParams,
    current: page,
    pageSize: pageSize,
    });
    },
    current: searchParams.current,
    pageSize: searchParams.pageSize,
    total: total,
    }}
    dataSource={chartList}
    renderItem={(item) => (
    <Card style={{ marginTop: '20px' }}>
    <List.Item
    key={item.id}
    extra={
    item.genChart && (
    <div style={{ width: '600px', height: '300px', marginTop: '20px' }}>
    <ReactECharts option={JSON.parse(item.genChart)} />
    </div>
    )
    }
    >
    <List.Item.Meta
    avatar={<Avatar src={currentUser?.userAvatar} />}
    title={currentUser?.userName}
    description={'图表类型:' + item.chartType}
    />
    图表名称: {item.name}
    <Divider></Divider>
    分析目标: {item.goal}
    <Divider></Divider>
    分析结果: {item.genResult}
    </List.Item>
    </Card>
    )}
    />
    </div>
    );
    };
  • 优化 UI 界面,添加 CSS 布局效果如下:

    效果图

  • 其他优化

    • 添加列表的 loading 加载,可以采用弱网测试来进行模拟加载过程

    • 分页组件的使用

    • 使用 List 组件进行渲染

  • 添加了搜索按钮,指定搜索图表的名称

    搜索框

  • 前端收获

    如何渲染 list 组件,布局,以及指定搜索条件进行分页数据的获取,熟悉对 ant design 组件库的使用。扩展点,支持用户查看原始数据,支持跳转到图标编辑页面,去编辑图表信息重新进行 AI 分析绘制。