NoSQL 之 Redis 集群

目录

[一、Redis 集群方式](#一、Redis 集群方式)

1、主从模式

2、哨兵模式

3、集群模式

二、集群模式简介

三、数据分片方式

1、客户端分片

2、代理分片

[(1)Twemproxy 代理分片机制](#(1)Twemproxy 代理分片机制)

[(2)Codis 代理分片机制](#(2)Codis 代理分片机制)

3、服务器端分片

四、负载均衡的实现

五、故障的处理

1、故障转移

[2、多slave 选举](#2、多slave 选举)

[六、Redis 群集部署](#六、Redis 群集部署)

1、安装redis(每个节点都要安装)

2、修改配置文件(每个节点都要配置,只有IP地址不同,其他都相同)

[3、创建redis 群集](#3、创建redis 群集)

4、测试群集

5、集群信息查看

6、添加节点

7、添加节点的另一种方法

8、删除节点

(1)清除10.108的槽位信息

(2)删除10.108的节点

9、修改新节点为其他节点的从节点

10、重新分配槽位

七、集群排错思路


一、Redis 集群方式

Redis 有三种模式:分别是主从复制、哨兵模式、Cluster(集群)

1、主从模式

主从复制是高可用 Redis的基础,哨兵和群集都是在主从复制基础上实现高可用的。主从复制主要实现了数据的多机备份,以及对于读操作的负载均衡和简单故障恢复。

缺陷:故障恢复无法自动化,写操作无法负载均衡,存储能力受到单机的限制。

2、哨兵模式

在主从复制的基础上,哨兵实现了自动化的故障恢复。

缺陷:写操作无法负载均衡,存储能力受到单机的限制,哨兵无法对从节点进行自动故障转移:在读写分离场景下,从节点故障会导致读服务不可用,需要对从节点做额外的监控、切换操作。

3、集群模式

通过集群,Redis解决了写操作无法负载均衡,以及存储能力受到单机限制的问题,实现了较为完善的高可用方案。

Redis-Cluster 是在 Redis3.0 版中推出,支持 Redis 分布式集群部署模式,采用无中心分布式架构。所有的 Redis 节点彼此互联(PING-PONG 机制),内部使用二进制协议优化传输速度和带宽。节点的 fail 是通过集群中超过半数的节点检测失败时才生效。客户端与 Redis 节点直连,不需要中间代理层。客户端也不需要连接集群所有节点,连接集群中任何一个可用节点即可,这样即减少了代理层,也大大提高了 Redis 集群性能。

Redis Cluster 通过创建多个副本来保证数据的可靠性。每个分片都有一个主节点和多个从节点,主节点负责处理读写操作,而从节点则复制主节点的数据。当主节点发生故障时,从节点会自动切换为主节点,提供读写服务。这样可以确保即使发生节点故障,系统仍然能够继续提供服务,保证数据的高可用性。

|------------------|----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 模式 | 版本 | 优点 | 缺点 |
| 主从模式 | redis2.8之前 | 1、解决数据备份问题2、做到读写分离,提高服务器性能 | 1、master 故障,无法自动故障转移,需人工介入 2、master 无法实现动态扩容 |
| 哨兵模式 | redis2.8级之后的模式 | 1、Master 状态监测 2、master 节点故障,自动切换主从,故障自愈 3、所有 slave 从节点,随之更改新的 master 节点 | 1、slave 节点下线,sentinel不会对其进行故障转移,连接从节点的客户端因为无法获取到新的可用从节点 2、master 无法实行动态扩容 |
| redis cluster 模式 | redis3.0版本之后 | 1、有效的解决了 redis 在分布式方面的需求 2、遇到单机内存,并发和流量瓶颈等问题时,可采用 Cluster 方案达到负载均衡的目的 3、可实现动态扩容 4、P2P 模式,无中心化 5、通过 Gossip 协议同步节点信息 6、自动故障转移、Slot 迁移中数据可用 7、自动分割数据到不同的节点上 8、整个集群的部分节点失败或者不可达的情况下能够继续处理命令 | 1、架构比较新,最佳实践较少 2、为了性能提升,客户端需要缓存路由表信息 3、节点发现、reshard 操作不够自动化 4、不支持处理多个 keys 的命令,因为这需要在不同的节点间移动数据 5、Redis 集群不像单机 Redis 那样支持多数据库功能,集群只使用默认的0号数据库,并且不能使用SELECTindex 命令 |

二、集群模式简介

集群,即 Redis Cluster, 是 Redis 3.0开始引入的分布式存储方案。集群由多个节点(Node)组成,Redis 的数据分布在这些节点中。集群中的节点分为主节点和从节点;只有主节点负责读写请求和集群信息的维护;从节点只进行主节点数据和状态信息的复制。

redis 集群采用 master-slave 的方式,即1个master 节点可以包含多个 slave 节点,slave 节点主要对 master 节点的数据进行备份,如果 master 节点挂了,可以启动 salve 节点替换掉原先的 master 节点,作为新的 master 节点。redis 集群使用投票容错机制,如果集群中超过半数以上的节点投票认为某节点挂了,那么这个节点就会被认为挂掉了,所以,在设置 redis 集群时,最少的 master 节点为3个,如果每一个master 节点都添加一个 slave节点的话,搭建一个redis 集群总共需要6个节点,即3个master 节点,3 个slave 节点。

redis 集群没有统一的入口,客户端连接集群的时候,连接集群中的任意节点即可。

Redis 集群是一个由多个主从节点群组成的分布式服务集群,它具有复制、高可用和分片特性。Redis 集群不需要 sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过 1000 个节点)。redis 集群的性能和高可用性均优于之前版本的哨兵模式,且集群配置非常简单。redis 集群的运用主要是针对海量数据+高并发+高可用的场景。

三、数据分片方式

对象保存到 redis 之前先经过 CRC16 哈希到一个指定的 Node 上,这个过程即 rediscluster 的分片,集群内部将所有的key映射到16384个Slot 中,集群中的每个 RedisInstance 负责其中的一部分的 Slot 的读写。集群客户端连接集群中任- Redis Instance即可发送命令,当 Redis Instance 收到自己不负责的 Slot 的请求时,会将负责请求 Key所在 Slot 的 Redis Instance 地址返回给客户端,客户端收到后自动将原请求重新发往这个地址,对外部透明。**一个Key 到底属于哪个Slot由(HASH_SLOT=CRC16(key) mod 16384)决定,也就是先对 key 进行哈希,然后对哈希值%16384 求余数算出槽位号。**只有 master节点会被分配槽位,slave节点不会分配槽位。

Redis Cluster 通过哈希算法将数据分散到多个节点上,实现数据的分片。每个节点负责管理一部分数据,并提供相应的读写操作。分片机制可以将数据均匀地分布在多个节点上,提高系统的并发处理能力。当某个节点发生故障时,只会影响该节点上的数据,而不会影响整个系统的可用性。

具体的分片方式有客户端分片、代理分片和Twemproxy 代理分片机制。

1、客户端分片

客户端分片方案是将分片工作放在业务程序端。程序代码根据预先设置的路由规则,直接对多个 Redis 实例进行分布式访问。这样的好处是,群集不依赖于第三方分布式中间件,实现方法和代码都自己掌控,可随时调整,不用担心踩到坑。这实际上是一种静态分片技术,Redis 实例的增减,都得手工调整分片程序。基于此分片机制的开源产品,现在仍不多见。

这种分片机制的性能比代理式更好(少了一个中间分发环节),但缺点是升级麻烦,对研发人员的个人依赖性强,需要有较强的程序开发能力做后盾。如果主力程序员离职,可能新的负责人会选择重写一遍。所以这种方式下可运维性较差,一旦出现故障,定位和解决都得研发人员和运维人员配合解决,故障时间变长。因此这种方案,难以进行标准化运维,不太适合中小公司。

2、代理分片

代理分片方案是将分片工作交给专门的代理程序来做。代理程序接收到来自业务程序的数据请求,根据路由规则,将这些请求分发给正确的 Redis 实例并返回给业务程序。这种机制下,一般会选用第三方代理程序(而不是自己研发)。因为后端有多个 Redis 实例,所以这类程序又称为分布式中间件。这种分片机制的好处是业务程序不用关心后端 Redis实例,维护起来也方便。虽然会因此带来些性能损耗,但对于 Redis 这种内存读写型应用,相对而言是能容忍的。这是比较推荐的集群实现方案。像Twemproxy、Codis 就是基于该机制的开源产品的其中代表,应用非常广泛

(1)Twemproxy 代理分片机制

Twemproxy 是一种代理分片机制,由 Twitter 开源。Twemproxy 作为代理,可接受来自多个程序的访问,按照路由规则,转发给后台的各个Redis 服务器,再原路返回。这个方案顺理成章地解决了单个 Redis 单实例问题。当然Twemproxy 本身也是单点,需要用Keepalived 做高可用方案。在很长的时间内,Twemproxy 是应用范围最广、稳定性最高、最久经考验的分布式中间件。

这种分片方式无法平滑地扩容/缩容,增加了运维难度。当业务量突增需要增加 Redis服务器或业务量萎缩需要减少Redis 服务器时,Twemproxy 都无法平滑地进行扩容或缩容等操作。

并且 Twemproxy 代理分片机制对运维操作不友好,甚至没有控制面板。由于使用了中间件代理,相比客户端直接连接服务器方式,性能上有所损耗,实测性能效果降低了 20%左右。

(2)Codis 代理分片机制

Codis 由豌豆荚于 2014 年 11 月开源,基于 Go 语言和 C 语言开发,是近期涌现的、国人开发的优秀开源软件之一,现已广泛用于豌豆英的各种Redis业务场景。从各种压力测试来看,Codis 的稳定性符合高效运维的要求。实测 Codis 集群业务处理性能改善很多,最初集群处理数据性能要比Twemproxy差20%左右,现在比 Twemproxy 好很多。并且 Codis具有可视化运维管理界面。因此综合方面会优于Twemproxy 很多。目前越来越多的公司选择 Codis。

Codis 引入了 Group 的概念,每个Group包括1个Redis Master 及至少 1个RedisSlave,这是和 Twemproxy 的区别之一。这样做的好处是,如果当前 Master 有问题,则运维人员可通过 Dashboard"自助式"切换到 Slave,而不需要小心翼翼地修改程序配置文件。

为支持数据热迁移(Auto Rebalance),出品方修改了 Redis Server 源码,并称之为CodisServer。Codis 采用预先分片(Pre-Sharding)机制,事先规定分成1024个 slots(也就是说,最多能支持后端 1024 个 Codis Server),这些路由信息保存在 zookeeper 中。

3、服务器端分片

服务器端分片是 Redis 官方的集群分片技术。Redis-Cluster 将所有Key 映射到16384个 slot 中,集群中每个 Redis 实例负责一部分,业务程序是通过集成的Redis-Cluster 客户端进行操作。客户端可以向任一实例发出请求,如果所需数据不在该实例中,则该实例引导客户端自动去对应实例读写数据。Redis-Cluster 成员之间的管理包括节点名称、IP、端口、状态、角色等,都通过节点与之间两两通讯,定期交换信息并更新。

四、负载均衡的实现

客户端路由:在 Redis Cluster 中,客户端需要根据数据的键名选择正确的节点进行读写操作。Redis Cluster 使用哈希槽(hash slot)来划分数据,并将每个哈希槽映射到相应的节点上。客户端可以根据键名的哈希值,将请求发送到对应的节点上,实现负载均衡。

自动迁移:Redis Cluster 具有自动迁移功能,当节点加入或离开集群时,系统会自动重新分配数据,保持各个节点上哈希槽的均衡。当节点加入集群时,系统会将一部分哈希槽从其他节点迁移到新节点上;当节点离开集群时,系统会将该节点上的哈希槽重新分配给其他节点。这种机制可以在节点数目变化时,自动调整数据的分布,实现负载均衡。

故障检测与恢复:Redis Cluster 具有故障检测和自动恢复的机制。集群中的节点会相互监控,检测节点的健康状态。当某个节点被检测到不可用时,系统会自动将该节点标记为下线,并将该节点上的哈希槽重新分配给其他节点。当节点恢复正常时,系统会将其重新加入集群,并进行数据迁移,保持数据的一致性。

Redis Cluster 通过分片和副本机制实现了数据的高可用性和负载均衡。分片机制将数据均匀地分布在多个节点上,提高了系统的并发处理能力:副本机制则保证了数据的可靠性即使发生节点故障,也能够继续提供服务。同时,Redis Cluster 通过客户端路由、自动迁移和故障检测与恢复等机制,实现了负载均衡和故障自动转移。在实际应用中,合理设计和配置 Redis Cluster,可以提高系统的性能、可靠性和可扩展性。

五、故障的处理

1、故障转移

当从节点发现自己的主节点变为已下线(FAIL)状态时,便尝试进 Failover,以期成为新的主。

以下是故障转移的执行步骤:

(1)从下线主节点的所有从节点中选中一个从节点
(2)被选中的从节点执行 SLAVEOF NO NOE 命令,成为新的主节点
(3)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己

(4)新的主节点对集群进行广播 PONG 消息,告知其他节点已经成为新的主节点

(5)新的主节点开始接收和处理槽相关的请求
(6)集群slots是否必须完整才能对外提供服务

2、多slave 选举

在多个 slave 情况下如果一个 master 宕机,需要基于 Raft 协议选举方式来实现的新主的选举。步骤如下:

(1)当从节点发现自己的主节点进行已下线状态时,从节点会广播一条CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到这条消息,并且具有投票权的主节点向这个从节点投票。

(2)如果一个主节点具有投票权,并且这个主节点尚未投票给其他从节点,那么主节点将向要求投票的从节点返回一条,CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示这个主节点支持从节点成为新的主节点。

(3)每个参与选举的从节点都会接收CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息,并根据自己收到了多少条这种消息来统计自己获得了多少主节点的支持。

(4)如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于集群 N/2+1张支持票时,这个从节点就成为新的主节点。

(5)如果在一个选举周期没有从能够收集到足够的支持票数,那么集群进入一个新的选举周期,并再次进行选主,直到选出新的主节点为止。

六、Redis 群集部署

|-------------|-------------|----------------|-------|
| 操作系统 | 主机 | IP | 应用 |
| OpenEuler24 | master1(01) | 192.168.10.101 | Redis |
| OpenEuler24 | master2(02) | 192.168.10.102 | Redis |
| OpenEuler24 | master3(03) | 192.168.10.103 | Redis |
| OpenEuler24 | slave1(01) | 192.168.10.104 | Redis |
| OpenEuler24 | slave2(02) | 192.168.10.105 | Redis |
| OpenEuler24 | slave3(03) | 192.168.10.106 | Redis |

1、安装redis(每个节点都要安装)

复制代码
systemctl stop firewalld
setenforce 0

dnf -y install gcc zlib-devel tar
tar zxvf redis-5.0.14.tar.gz
cd redis-5.0.14

make && make PREFIX=/usr/local/redis install
ln -s /usr/local/redis/bin/* /usr/local/bin/
cd utils/
./install_server.sh

2、修改配置文件(每个节点都要配置,只有IP地址不同,其他都相同)

复制代码
vim /etc/redis/6379.conf
bind 0.0.0.0	#70
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize yes
supervised no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile /var/log/redis_6379.log
appendonly yes	#700
cluster-enabled yes	#833,去掉注释,启用群集
cluster-config-file nodes-6379.conf	#841,去掉注释
cluster-node-timeout 15000	#847,去掉注释
cluster-require-full-coverage no	#924,去掉注释,将yes改为no
  • 开启Cluster: cluster-enabled yes
  • 集群配置文件:cluster-config-file nodes-7000.conf。这个配置文件不是要我们去配的,而是 Redis 运行时保存配置的文件,所以我们也不可以修改这个文件。
  • 集群超时时间:cluster-node-timeout 15000。结点超时多久则认为它宕机了。
  • 槽是否全覆盖:cluster-require-full-coverage no。默认是yes,只要有结点宕机导致 16384个槽没全被覆盖,整个集群就全部停止服务,所以一定要改为no
复制代码
[root@bogon ~]# /etc/init.d/redis_6379 restart
Stopping ...
Redis stopped
Starting Redis server...
[root@bogon ~]# netstat -anpt | grep 6379
tcp        0      0 0.0.0.0:6379            0.0.0.0:*               LISTEN      6045/redis-server 0 
tcp        0      0 0.0.0.0:16379           0.0.0.0:*               LISTEN      6045/redis-server 0 

3、创建redis 群集

任意节点

复制代码
redis-cli --cluster create --cluster-replicas 1 192.168.10.101:6379 192.168.10.102:6379 192.168.10.103:6379 192.168.10.104:6379 192.168.10.105:6379 192.168.10.106:6379

需要至少3个master 节点,副本数至少为1,即每个master 需要一个从节点

4、测试群集

复制代码
[root@bogon ~]# redis-cli -h 192.168.10.105 -p 6379 -c
192.168.10.105:6379> set name zhangsan
-> Redirected to slot [5798] located at 192.168.10.102:6379
OK
192.168.10.102:6379> get name
"zhangsan"
192.168.10.102:6379> exit

[root@bogon ~]# redis-cli -h 192.168.10.102 -p 6379 -c
192.168.10.102:6379> get name
"zhangsan"
192.168.10.102:6379> exit

5、集群信息查看

复制代码
[root@bogon ~]# redis-cli
192.168.10.102:6379> cluster nodes
5539d8389c081c38a51ca24a5408819d1e26fb2b 192.168.10.103:6379@16379 master - 0 1749443231001 3 connected 10923-16383
ce52e212fae0ade10994851eba4870089c443362 192.168.10.106:6379@16379 slave bb6890691965611f311d281b1fe6a878e3e64608 0 1749443229000 6 connected
b3cdb1b46bef3d1db099f2c11088afefa0446528 192.168.10.105:6379@16379 slave 62a114414ead39cf193a2088ea20b4995bd5b00f 0 1749443233014 1 connected
bb6890691965611f311d281b1fe6a878e3e64608 192.168.10.102:6379@16379 myself,master - 0 1749443231000 2 connected 5461-10922
62a114414ead39cf193a2088ea20b4995bd5b00f 192.168.10.101:6379@16379 master - 0 1749443229000 1 connected 0-5460
01fb574adb38a0529027a1af6a0134e0a50df14c 192.168.10.104:6379@16379 slave 5539d8389c081c38a51ca24a5408819d1e26fb2b 0 1749443232006 4 connected

redis-cli --cluster check 192.168.10.101:6379

6、添加节点

例如存在节点 192.168.10.107(并且已在配置文件中开启集群相关配置),把192.168.10.107加入现有集群

复制代码
redis-cli -c -p 6379 cluster meet 192.168.10.107 6379

redis-cli
cluster nodes

注:添加完后此新节点是 master 的角色

7、添加节点的另一种方法

例如存在节点 192.168.10.108(并且已在配置文件中开启集群相关配置),把192.168.10.108 加入现有集群

复制代码
reids-cli --cluster add-node 192.168.10.108:6379 192.168.10.107:6379

注:10.108是要添加的节点,10.107可以是当前集群中的任意一个节点

8、删除节点

(1)清除10.108的槽位信息

复制代码
redis-cli -h 192.168.10.108 -p 6379
flushall
cluster reset

(2)删除10.108的节点

复制代码
redis-cli --cluster del-node 192.168.10.108:6379 5539d8389c081c38a51ca24a5408819d1e26fb2b

注:此ID为10.108的ID

注意:如果删除的 slave 节点,直接删除即可,如果删除的是 master 节点,需要先清除槽信息再删除,并且建议清除 redis 实例中的集群配置文件并重启实例,例如/var/lib/redis/6379/nodes-6379.conf

9、修改新节点为其他节点的从节点

复制代码
redis-cli -h 192.168.10.10.108 -p 6379
cluster replicate bb6890691965611f311d281b1fe6a878e3e64608

让10.108 成为10.107 的从节点,此ID(bb6890691965611f311d281b1fe6a878e3e64608)是10.107 的ID

注:10.108 需要先加入到集群,在设置为其他 master 的slave。

10、重新分配槽位

重新分片基本上意味着将 slot 重新分配,就像集群创建一样,它是使用 redis-cli实用程序完成的。

注意:平均分配所有的槽位,使用以下命令会自动将16384个槽位自动分配给集群的每-个 master,不用手动指定槽为分配。

复制代码
redis-cli --cluster rebalance --cluster-threshold 1 --cluster-use-empty-masters 192.168.10.101:6379

只需要指定一个老的节点(192.168.10.101:6379),redis 会自动查找其他节点。

如果某个 master 节点没有槽位,可以使用以下命令分槽,语法为:

redis-cli -h <node-ip> -p <node-port> CLUSTER ADDSLOTS 1 2 3 4 .... 5460

复制代码
redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0...5460}

七、集群排错思路

注意1:

在创建redis 集群服务时,提示以下错误:

错误提示是说:slot插槽被占用了、这是因为搭建集群前时,以前redis 的旧数据和配置信息没有清理干净。

解决方案:

用redis-cli登录到每个节点执行 flushall 和 cluster reset,并删除/var/lib/redis/6379/下的集群配置文件,就可以了。

注意2:

如果配置文件中没有监听 127.0.0.1,在使用/etc/init.d/redis_6379.conf start 启动redis 服务时(停止或重启都是如此),会提示连接127.0.0.1:6379 失败

解决方法:

使用如下方法启动

复制代码
redis-server /etc/redis/6379.conf

使用如下命令关闭

复制代码
redis-cli -h 192.168.10.103 -p 6379 shutdown

注意3:

配置文件中如果没有监听127.0.0.1的地址,在连接是需要使用如下方法:

复制代码
redis-cli -h 192.168.10.101 -p 6379

这里的ip 为本机监听的ip

注意4:

在做群集的时候各个节点不要监听 127.0.0.1的地址,否则,在建立群集时会有如下情况

下接

系统会一直停留在这里,这是在等待其他节点并入到群集中,需要在其他slave 的主机上接受其 master 才可以

按照这里的提示登录其他slave

复制代码
cluster meet 192.168.10.101 6379

然后等待连接即可

其他注意事项:

(1)redis master 宕机,恢复后不会自动切为主

(2)扩容 redis cluster 如果我们大量使用 redis cluster 的话,有一个痛点就是扩容的机器加入集群的时候,分配主从。现在只能使用命令去操作,非常的凌乱。而且如果 rediscluster 是线上集群或者是有数据的话,极其容易造成丢数据。

(3)redis cluster 从节点支持可读的前提下得在执行 readonly 执行,程序读取从节点时也需要执行。一次连接需要执行一次,客户端关闭即失效。

(4)redis cluster 启动必须以绝对路径的方式启动,如果不用绝对路径启动,会产生新的nodes.conf 文件,集群的主从对应关系就会变乱,从而导致集群奔溃。

相关推荐
保持学习ing32 分钟前
SpringBoot前后台交互 -- 登录功能实现(拦截器+异常捕获器)
java·spring boot·后端·ssm·交互·拦截器·异常捕获器
gadiaola38 分钟前
【JVM面试篇】高频八股汇总——类加载和类加载器
java·jvm·面试
七七&55643 分钟前
【Java开发日记】基于 Spring Cloud 的微服务架构分析
java·spring cloud·架构
小猫咪怎么会有坏心思呢1 小时前
华为OD机考-数字游戏-逻辑分析(JAVA 2025B卷)
java·游戏·华为od
Aesopcmc1 小时前
idea 启动jar程序并调试
java·intellij-idea·jar
南极Ou1 小时前
Mybatis逆向工程详解(附源码文件)动态创建实体类、条件扩展类、Mapper接口、Mapper.xml映射文件
xml·java·mybatis
Moshow郑锴2 小时前
IDEA为何一直无法使用超过4g内存
java·ide·intellij-idea
木头左2 小时前
Docker容器化镜像分层原理及优化策略
java·eureka
李少兄2 小时前
IntelliJ IDEA代码提示忽略大小写设置详解
java·ide·intellij-idea
白宇横流学长2 小时前
基于SpringBoot实现的课程答疑系统设计与实现【源码+文档】
java·spring boot·后端