【Spring Cloud】全面解析服务容错中间件 Sentinel 持久化两种模式

news2024/10/7 13:27:21

文章目录

  • 推送模式
  • 本地文件持久化(拉模式)
    • 配置yml
    • 编写处理类
    • 添加配置
    • 演示
  • 配置中心持久化(推模式)
    • 修改nacos在sentinel中生效
      • 引入依赖
      • 配置文件
    • 修改sentinel在nacos中生效
      • 下载源码
      • 更改代码
      • 演示
  • 总结

推送模式

Sentinel 规则的推送有下面三种模式:
在这里插入图片描述

通过前面的讲解,我们已经知道,可以通过 Dashboard 来为每个 Sentinel 客户端设置各种各样的规则,这种属于原始模式。这种模式存在一个问题,就是这些规则默认是存放在内存中的,极不稳定,所以需要将其持久化。

在这里插入图片描述

为了达到持久化的目标,我们需要进行改造,改造的方案有两种:本地文件持久化(拉模式)、配置中心持久化(推模式)

本地文件持久化(拉模式)

拉模式又被称为 pull 模式,它的数据源(如本地文件、RDBMS等)一般是可写入的。本地文件数据源会定时轮询文件的变更,读取规则。这样我们既可以在应用本地直接修改文件来更新规则,也可以通过 Sentinel 控制台推送规则。以本地文件数据源为例,推送过程如下图所示:

在这里插入图片描述

首先 Sentinel 控制台通过API将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。使用 pull模式的数据源时一般不需要对Sentinel控制台进行改造。这种实现方法好处是简单,坏处是无法保证监控数据的一致性。

配置yml

#数据库配置
spring:
  cloud:
    sentinel:
      eager: true
      transport:
        port: 9998 #跟控制台交流的端口,随意指定一个未使用的端口即可
        dashboard: localhost:8080  #指定控制台服务的地址
      filter:
        enabled: false

编写处理类

实现 InitFunc 接口,在 init 中处理 DataSource 初始化逻辑,并利用 SPI 机制实现加载。

public class FilePersistence implements InitFunc {

    @Value("${spring.application.name}")
    private String applicationName;

    @Override
    public void init() throws Exception {
        //创建规则文件
        String ruleDir = System.getProperty("user.home") + "/sentinel-rules/" + applicationName;
        String flowRulePath = ruleDir + "/flow-rule.json";
        String degradeRulePath = ruleDir + "/degrade-rule.json";
        String systemRulePath = ruleDir + "/system-rule.json";
        String authorityRulePath = ruleDir + "/authority-rule.json";
        String paramFlowRulePath = ruleDir + "/param-flow-rule.json";
        this.mkdirIfNotExits(ruleDir);
        this.createFileIfNotExits(flowRulePath);
        this.createFileIfNotExits(degradeRulePath);
        this.createFileIfNotExits(systemRulePath);
        this.createFileIfNotExits(authorityRulePath);
        this.createFileIfNotExits(paramFlowRulePath);

        //流控规则
        //创建流控规则的可读数据源
        ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(flowRulePath,
                source -> JSON.parseObject(source, new TypeReference<List<FlowRule>>(){}));
        //将可读数据源注册至 FlowRuleManager,这样当规则文件发生变化时,就会更新规则到内存
        FlowRuleManager.register2Property(flowRuleRDS.getProperty());

        WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(flowRulePath,this::encodeJson);
        //将可写数据源注册至 transport 模块的 WritableDataSourceRegistry 中.
        //这样收到控制台推送的规则时,Sentinel 会先更新到内存,然后将规则写入到文件中.
        WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);


        //降级规则
        ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(degradeRulePath,
                source -> JSON.parseObject(source, new TypeReference<List<DegradeRule>>(){}));
        DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());

        WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(degradeRulePath,this::encodeJson);
        WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);


        //系统规则
        ReadableDataSource<String, List<SystemRule>> systemRuleRDS= new FileRefreshableDataSource<>(systemRulePath,
                source -> JSON.parseObject(source, new TypeReference<List<SystemRule>>(){}));
        SystemRuleManager.register2Property(systemRuleRDS.getProperty());

        WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(systemRulePath,this::encodeJson);
        WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);

        //授权规则
        ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(authorityRulePath,
                source-> JSON.parseObject(source, new TypeReference<List<AuthorityRule>>(){}));
        AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());

        WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(authorityRulePath, this::encodeJson);
        WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);

        //热点参数规则
        ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(paramFlowRulePath,
                source-> JSON.parseObject(source, new TypeReference<List<ParamFlowRule>>(){}));
        ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());

        WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(paramFlowRulePath,this::encodeJson);
        ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
    }
}
  • FileRefreshableDataSource:每次更新规则时自动读取持久化文件更新到map缓存。
  • FileWritableDataSource:写数据源,将 sentinel 控制台发送过来的规则信息写到持久化文件中。在客户端的socket接收到规则信息后,更新缓存的时候也会将规则信息写入文件中持久化。

添加配置

在resources下创建配置目录 META-INF/services,然后添加文件com.alibaba.csp.sentinel.init.InitFunc,在文件中添加配置类的全路径it.aq.cheetah.config.FilePersistence

这样当在 Dashboard 中修改了配置后,Dashboard 会调用客户端的接口修改客户端内存中的值,同时将配置写入文件中,这样操作的话规则是实时生效的,如果是直接修改文件中的内容,这样需要等定时任务3秒后执行才能读到最新的规则。接下来我们演示下:

演示

编写测试类

@RestController
@RequestMapping("/product2")
@Slf4j
public class ProductController2 {

    @RequestMapping("/test")
    @SentinelResource(value = "test")
    public String test() {
        return "product2";
    }
}

启动项目,发现在目录下生成了空的规则文件

在这里插入图片描述
在页面上增加流控规则

在这里插入图片描述

然后去看文件flow-rule.json,发现存到了本地文件中

在这里插入图片描述
接着我们仿照该规则仿写一个熔断规则,然后查看网页数据确实生效了

在这里插入图片描述

配置中心持久化(推模式)

推模式又叫 Push 模式,它是通过注册中心实现的,Sentinel控制台——>配置中心——>Sentinel数据源——>Sentinel
在这里插入图片描述
用户不仅可以通过sentinel控制台进行更新,也可以通过nacos配置中心进行更新,所以在sentinel控制台或nacos中修改规则后,都需要通知对方刷新最新的配置。

修改nacos在sentinel中生效

引入依赖

我们在之前项目的基础上引入新的依赖

<!--nacos配置中心-->
<dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
 </dependency>

 <!--以nacos作为sentinel数据源的依赖-->
 <dependency>
     <groupId>com.alibaba.csp</groupId>
     <artifactId>sentinel-datasource-nacos</artifactId>
 </dependency>

配置文件

nacos 配置:因为我们用nacos作为了配置中心,我们可以将sentinel的基本配置放入到nacos中就可以了,所以当前服务的yml配置文件中只需要写一些基本的配置就可以了。

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml

在nacos中配置sentinel信息

spring: 
  cloud:
    sentinel:
      transport:
        # 跟控制台交流的端口,随意指定一个未使用的端口即可
        port: 9998 
        # 指定控制台服务的地址
        dashboard: localhost:8080  
      # sentinel用nacos作为数据源的配置
      datasource: 
        #流控管理(这个名称可以自定义)
        flow-control: 
          # 告诉sentinel用nacos作为数据源
          nacos: 
            # 配置中心里执行文件的 dataId
            dataId: shop-product-flow.json  
            # nacos的地址
            serverAddr: 127.0.0.1:8848 
            # 指定文件配置的是哪种规则
            rule-type: flow

注意:如果使用的 namespace 不是默认的,记得配置 namespace 参数。

  • dataId:需要告诉 sentinel 读取配置中心中的哪个配置文件;
  • rule-type:告诉 sentinel 配置文件配置的控制规则,flow:流控、degrade:熔断、param-flow 热点参数,想看有哪些规则参数可以查看com.alibaba.cloud.sentinel.datasource包下的枚举类:RuleType。
public enum RuleType {

	/**
	 * flow.
	 */
	FLOW("flow", FlowRule.class),
	/**
	 * degrade.
	 */
	DEGRADE("degrade", DegradeRule.class),
	/**
	 * param flow.
	 */
	PARAM_FLOW("param-flow", ParamFlowRule.class),
	/**
	 * system.
	 */
	SYSTEM("system", SystemRule.class),
	/**
	 * authority.
	 */
	AUTHORITY("authority", AuthorityRule.class),
	/**
	 * gateway flow.
	 */
	GW_FLOW("gw-flow",
			"com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule"),
	/**
	 * api.
	 */
	GW_API_GROUP("gw-api-group",
			"com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition");

shop-product-flow.json文件中配置【流控规则】

[
	{
		"clusterConfig": {
			"acquireRefuseStrategy": 0,
			"clientOfflineTime": 2000,
			"fallbackToLocalWhenFail": true,
			"resourceTimeout": 2000,
			"resourceTimeoutStrategy": 0,
			"sampleCount": 10,
			"strategy": 0,
			"thresholdType": 0,
			"windowIntervalMs": 1000
		},
		"clusterMode": false,
		"controlBehavior": 0,
		"count": 10.0,
		"grade": 1,
		"limitApp": "default",
		"maxQueueingTimeMs": 500,
		"resource": "/product2/test",
		"strategy": 0,
		"warmUpPeriodSec": 10
	}
]

在这里插入图片描述

然后去 dashboard 中查看,发现流控规则已经在控制中显示了

在这里插入图片描述
目前我们已经实现了在 nacos 中配置的文件直接在sentinel dashboard中生效,但是我们在sentinel dashboard中修改了配置,nacos 是不会监听到并进行修改的。接下来我们实现一下通过 sentinel 控制台设置的规则直接持久化到 nacos配置中心。

修改sentinel在nacos中生效

Sentinel 控制台提供 DynamicRulePublisherDynamicRuleProvider 接口用于实现应用维度的规则推送和拉取。

下载源码

https://github.com/alibaba/Sentinel/releases下载dashboard的代码源码。

在这里插入图片描述

解压之后打开sentinel-dashboard项目,将 pom.xml 文件中作用域为 test 的注释掉,注释掉后默认的作用域为 compile。

  • test:作用域表示该依赖项只在测试时有用,在编译和运行时不会被用到。
  • compile:作用域范围的依赖项在所有情况下都是有效的,包括编译、运行和测试。

把 test 包下的两个类复制过来

在这里插入图片描述

  • NacosConfigUtil 类主要就是 nacos 配置的规则,比如配置文件的后缀,分组Group_ID等等。因为我用的分组是默认分组,所以改为DEFAULT_GROUP,我之前的规则文件是shop-product-flow.json,所以我把规则文件后缀FLOW_DATA_ID_POSTFIX改为"-flow.json"。
  • NacosConfig类是为了注入nacos的信息以及转换器类。

更改代码

application.properties 中增加 nacos 的配置

# nacos 配置
nacos.serverAddr=localhost:8848
nacos.username=nacos
nacos.password=nacos

NacosConfig 修改为从配置文件中获取nacos配置

 @Bean
 public ConfigService nacosConfigService() throws Exception {
     Properties properties = new Properties();
     //Nacos地址
     properties.put("serverAddr", serverAddr);
     //Nacos用户名
     properties.put("username", username);
     //Nacos密码
     properties.put("password", password);
     return ConfigFactory.createConfigService(properties);
 }

com.alibaba.csp.sentinel.dashboard.rule.FlowRuleApiPublisher#publish方法中增加推送到nacos的逻辑代码

//将规则推送到nacos
configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
       NacosConfigUtil.GROUP_ID, converter.convert(rules));

com.alibaba.csp.sentinel.dashboard.rule.FlowRuleApiProvider#getRules方法中修改为从nacos中读取配置的逻辑

@Override
public List<FlowRuleEntity> getRules(String appName) throws Exception {
    String rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,
            NacosConfigUtil.GROUP_ID, 3000);
    if (StringUtil.isEmpty(rules)) {
        return new ArrayList<>();
    }
    return converter.convert(rules);
}

改造流控的controller类FlowControllerV1,将配置保存到内存中的逻辑改为保存到nacos中

@Autowired
@Qualifier("flowRuleDefaultProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;

@Autowired
@Qualifier("flowRuleDefaultPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;

@GetMapping("/rules")
@AuthAction(PrivilegeType.READ_RULE)
public Result<List<FlowRuleEntity>> apiQueryMachineRules(@RequestParam String app,
                                                         @RequestParam String ip,
                                                         @RequestParam Integer port) {
    ......
    try {
//            List<FlowRuleEntity> rules = sentinelApiClient.fetchFlowRuleOfMachine(app, ip, port);
        //从nacos中读取规则
        List<FlowRuleEntity> rules = ruleProvider.getRules(app);
        rules = repository.saveAll(rules);
        return Result.ofSuccess(rules);
    } catch (Throwable throwable) {
        logger.error("Error when querying flow rules", throwable);
        return Result.ofThrowable(-1, throwable);
    }
}

private void publishRules(String app, String ip, Integer port) throws Exception {
      //将规则推送到nacos
      List<FlowRuleEntity> rules = repository.findAllByApp(app);
      rulePublisher.publish(app, rules);

//        List<FlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
//        return sentinelApiClient.setFlowRuleOfMachineAsync(app, ip, port, rules);
}

//其余调用publishRules方法的地方做下简单调整

演示

启动当前项目,流控规则中存在我们之前在nacos中创建的文件,我们将原来的单机阈值从10改为12,然后保存。查看nacos中配置文件的数据,发现已经生效了。

在这里插入图片描述
至于其他规则,大家可以自行实现,此处就不一一实现了

在这里插入图片描述

总结

到这儿,服务容错中间件Sentinel的两种持久化模式就已经介绍完了。下一篇将为大家带来Feign整合容错组件 Sentinel 的文章,敬请期待吧!

后续的文章,我们将继续完善我们的微服务系统,集成更多的Alibaba组件。想要了解更多JAVA后端知识,请点击文末名片与我交流吧。留下您的一键三连,让我们在这个寒冷的东西互相温暖吧!

参考链接:

  • https://blog.csdn.net/weixin_36279234/article/details/130922604;
  • https://blog.csdn.net/u022812849/article/details/131206976;

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

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

相关文章

网络安全快速入门(十五) linux用户管理

14.1 前言 前面我们已经大概了解了Linux的网络链接&#xff0c;今天我们来看看Linux用户管理的一些基础操作&#xff0c;话不多说&#xff0c;我们来开始吧&#xff01;&#xff01; 14.2 用户的基础知识 我们先了解一些有关linux用户的一些基础知识&#xff0c;我们从Linux用…

告别传统,拥抱未来——上门回收小程序引领变革

随着科技的飞速发展&#xff0c;我们生活的方方面面都在经历着前所未有的变革。在环保和可持续发展的背景下&#xff0c;传统的废品回收方式已经难以满足现代社会的需求。而上门回收小程序的出现&#xff0c;正以其便捷、高效的特点&#xff0c;引领着废品回收行业的变革。 一、…

Threejs路径规划_基于A*算法案例完整版

上节利用了A*实现了基础的路径规划&#xff0c;这节把整个功能完善好&#xff0c;A*算法一方面是基于当前点找到可以到达的点&#xff0c;计算从出发点到此点&#xff0c;以及此点到目的地的总成本&#xff0c;比较出最小的那个&#xff0c;再用最小成本的点继续找到它可以到达…

LabVIEW与串口通讯在运行一段时间后出现数据接收中断的问题

这些问题可能与硬件、软件或通信协议有关。以下是详细的原因分析和可能的解决方案&#xff1a; 一、硬件原因 串口线缆或接口问题&#xff1a; 由于长时间使用&#xff0c;串口线缆可能出现接触不良或损坏。接口松动也可能导致通讯中断。 解决方案&#xff1a;检查并更换串口…

Sentinel重要的前置知识

文章目录 1、雪崩问题及解决方案1.1、雪崩问题1.2、超时处理1.3、仓壁模式1.4、断路器1.5、限流1.6、总结 2、服务保护技术对比3、Sentinel介绍和安装3.1、初识Sentinel3.2、安装Sentinel 4、微服务整合Sentinel ​&#x1f343;作者介绍&#xff1a;双非本科大三网络工程专业在…

Java进阶-SpringCloud使用BeanUtil工具类简化对象之间的属性复制和操作

在Java编程中&#xff0c;BeanUtil工具类是一种强大且便捷的工具&#xff0c;用于简化对象之间的属性复制和操作。本文将介绍BeanUtil的基本功能&#xff0c;通过详细的代码示例展示其应用&#xff0c;并与其他类似工具进行对比。本文还将探讨BeanUtil在实际开发中的优势和使用…

QML英文拟态键盘,英文数字符号输入法

文章目录 QML 英文拟态键盘思维导图更多细节欢迎私信效果图&#xff1a;核心代码&#xff1a;KeyboardLetter.qml&#xff0c;**CustomerKeyboard.qml**&#xff0c;Example.qmlCustomerKeyboard.qmlButton.qmlButtonBase.qmlButtonStateImage.qmlDialogBase.qmlExample.qmlIma…

必示科技参与智能运维国家标准预研线下编写会议并做主题分享

近日&#xff0c;《信息技术服务 智能运维 第3部分&#xff1a;算法治理》&#xff08;拟定名&#xff09;国家标准预研阶段第一次编写工作会议在杭州举行。本次会议由浙商证券承办。 此次编写有来自银行、证券、保险、通信、高校研究机构、互联网以及技术方等29家单位&#xf…

ESP32 实现获取天气情况

按照小安派AiPi-Eyes天气站思路&#xff0c;在ESP32 S3上实现获取天气情况。 一、在ESP32 S3实现 1、main.c 建立2个TASK void app_main(void) {//lvgl初始化xTaskCreate(guiTask, "guiTask", 1024 * 6, NULL, 5, NULL);//wifi初始化、socket、json处理taskcustom_…

ue5 中ps使用记录贴

一、快捷键记录 放大图形 ctrlalt空格 放大图形 缩小视口 ctrl空格 ctrlD 取消选区 ctrlt缩小文字 w魔棒工具 选择魔棒的时候把容差打开的多一点 二、案例 移动文字 在相应的图层选择 移动文字 修改图片里的颜色 在通道里拷贝红色通道&#xff0c;复制红色通道粘贴给正常图…

Softing工业将亮相2024年阿赫玛展会——提供过程自动化的连接解决方案

您可于2024年6月10日至14日前往美因河畔法兰克福11.0号馆&#xff0c;Softing将在C25展位展出&#xff0c;欢迎莅临&#xff01; 作为工业应用中数据交换领域公认的专家&#xff0c;Softing工业致力于帮助各行各业的客户部署网络自动化和优化生产流程。 使用Softing产品&…

kind: Telemetry

访问日志 访问日志提供了一种从单个工作负载实例的角度监控和理解行为的方法。 Istio 能够以一组可配置的格式为服务流量生成访问日志&#xff0c; 使操作员可以完全控制日志记录的方式、内容、时间和地点。 有关更多信息&#xff0c;请参阅获取 Envoy 的访问日志。 https:/…

TAS5711带EQ和DRC支持2.1声道的20W立体声8V-26V数字输入开环D类数字功放音频放大器

前言 数字功放很难搞&#xff0c;寄存器很多&#xff0c;要配置正确才有声音&#xff0c;要想声音好&#xff0c;要好好调整。 TAS5711出道很多年了&#xff0c;现在仍然在不少功放、音箱中能看到。 TAS5711特征 音频输入/输出 从 18V 电源向 8Q 负载提供 20W 功率 宽 PVDD…

Plesk面板中如何导出的MS SQL server数据库

需要导出我的SQL Server 的数据库文件&#xff0c;由于我使用的Hostease的Windows虚拟主机产品默认带普通用户权限的Plesk面板&#xff0c;但是不知道如何在Plesk上操作导出&#xff0c;因为也是对于Hostease主机产品不是很了解&#xff0c;因此联系Hostease的咨询了Hostease技…

[论文笔记]Chain-of-Thought Prompting Elicits Reasoning in Large Language Models

引言 今天带来思维链论文 Chain-of-Thought Prompting Elicits Reasoning in Large Language Models的笔记。 作者探索了如何通过生成一系列中间推理步骤的思维链&#xff0c;显著提升大型语言模型在进行复杂推理时的能力。 1 总体介绍 语言模型的规模扩大已被证明能够带来…

redis--告警处理设置密码连接

解决当前告警提示 告警一 backlog参数控制的是三次握手的时候server端收到client ack确认号之后的队列值&#xff0c;即全连接队列 vim /etc/sysctl.conf net.core.somaxconn 1024sysctl -p 告警二 内核参数 0、表示内核将检查是否有足够的可用内存供应用进程使用&#xff1…

第八节 条件装配案例讲解

一、条件装配的作用是什么 条件装配是 Spring 框架中一个强大的特性&#xff0c;使得开发者能够创建更加灵活和可维护的应用程序。在 Spring Boot 中&#xff0c;这个特性被大量用于自动配置&#xff0c;极大地简化了基于 Spring 的应用开发。 二、条件装配注解 <dependen…

STM32_HAL_FLASH 模拟 EEPROM

1. STM32 FLASH简介 STM32F407ZGT6 的 FLASH 容量为1024K 字节&#xff0c; STM32F40xx/41xx 的闪存模块组织如图 STM32F4 的闪存模块由主存储器、系统存储器、 OPT 区域和选项字节等 4 部分组成。 主存储器&#xff0c;该部分用来存放代码和数据常数&#xff08;如 const 类型…

macOS平台安装PostgreSQL的五种方法

macOS 平台安装 PostgreSQL 数据库主要有以下五种方法。 EDB安装工具 EDB 公司提供的图像安装工具&#xff0c;支持 macOS 以及 Windows 平台。该工具可以安装 PostgreSQL 服务器、pgAdmin&#xff08;管理开发工具&#xff09;以及 StackBuilder&#xff08;安装 PostgreSQL…

漫画|基于SprinBoot+vue的漫画网站(源码+数据库+文档)

漫画网站 目录 基于SprinBootvue的漫画网站 一、前言 二、系统设计 三、系统功能设计 1系统功能模块 2管理员功能模块 3用户功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 博主介绍&#xff1a;✌️大…