当前位置:首页 » 《随便一记》 » 正文

Redis(三)主从与哨兵架构详解 Redis主从架构 如何在同一台机器搭建主从架构 Redis主从工作原理 数据部分复制 Jedis使用 Redis的管道(Pipeline) Redis Lua脚本_T_Antry

23 人参与  2021年11月05日 07:24  分类 : 《随便一记》  评论

点击全文阅读


上一章提到了Redis(二)Redis持久化 其目的是出现故障重启时的数据恢复,这一章要提到Redis主从与哨兵架构。文中所用图片来自copy

文章目录

  • 前言
  • Redis主从架构
  • 如何在同一台机器搭建主从架构
  • Redis主从工作原理
  • 数据部分复制
  • Jedis使用
  • Redis的管道(Pipeline)
  • Redis Lua脚本
  • Redis哨兵高可用架构
    • 搭建
    • 假设master挂了
    • Jedis使用通过哨兵获取信息连接
  • 哨兵的Spring Boot整合Redis连接
  • Redis客户端命令对应的RedisTemplate中的方法列表:


前言

本章主要是和大家一起探索一下redis主从架构的搭建,这里有的参数暂不解释,更多的是搭建应用。


Redis主从架构

主(master)和 从(slave)部署在不同的服务器上,当主节点服务器写入数据时会同步到从节点的服务器上,一般主节点负责写入数据,从节点负责读取数据。

优点

  • 读写分离,提高效率

  • 数据热备份,提供多个副本
    在这里插入图片描述
    缺点

  • 主节点故障,集群则无法进行工作,可用性比较低,从节点升主节点需要人工手动干预

  • 单点容易造成性能低下

  • 主节点的存储能力受到限制

  • 主节点的写受到限制(只有一个主节点)- 全量同步可能会造成毫秒或者秒级的卡顿现象

如何在同一台机器搭建主从架构

  • copy一份redis.conf文件
  • 修改port
 port 6380
  • 配置主从复制
replicaof 192.168.0.60 6379 # 从本机6379的redis实例复制数据,Redis 5.0之前使用slaveof

修改完,启动新的实例只要指定这个新的配置文件即可

[root@localhost redis-5.0.13]# src/redis-server config/redis-6380.conf 
43394:C 14 Sep 2021 00:04:29.506 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
43394:C 14 Sep 2021 00:04:29.506 # Redis version=5.0.13, bits=64, commit=00000000, modified=0, pid=43394, just started
43394:C 14 Sep 2021 00:04:29.506 # Configuration loaded

启动完,可以查到多了个6380的实例

[root@localhost redis-5.0.13]# ps -ef | grep redis
root      40955      1  0 Sep13 ?        00:00:08 src/redis-server *:6379
root      43395      1  0 00:04 ?        00:00:00 src/redis-server *:6380
root      43400  42366  0 00:04 pts/0    00:00:00 grep --color=auto redis

Redis主从工作原理

为master配置一个slave,不管这个slave是否第一次连接上Master,它都会发送一个PSYNC命令给master请求复制数据。master收到PSYNC命令后,会在后台进行数据持久化通过bgsave生成最新的rdb快照文件,持久化期间,master会继续接收客户端的请求,它会把这些可能修改数据集的请求缓存在内存中。当持久化进行完毕以后,master会把这份rdb文件数据集发送给slave,slave会把接收到的数据进行持久化生成rdb,然后再加载到内存中。然后,master再将之前缓存在内存中的命令发送给slave。
在这里插入图片描述
当master与slave之间的连接由于某些原因而断开时,slave能够自动重连Master,如果master收到了多个slave并发连接请求,它只会进行一次持久化,而不是一个连接一次,然后再把这一份持久化的数据发送给多个并发连接的slave。

数据部分复制

当master和slave断开重连后,一般都会对整份数据进行复制。但从redis2.8版本开始,redis改用可以支持部分数据复制的命令PSYNC去master同步数据,slave与master能够在网络连接断开重连后只进行部分数据复制(断点续传)。
master会在其内存中创建一个复制数据用的缓存队列,缓存最近一段时间的数据,master和它所有的
slave都维护了复制的数据下标offset和master的进程id,因此,当网络连接断开后,slave会请求master
继续进行未完成的复制,从所记录的数据下标开始。如果master进程id变化了,或者从节点数据下标
offset太旧,已经不在master的缓存队列里了,那么将会进行一次全量数据的复制。
在这里插入图片描述
如果有很多从节点,为了缓解主从复制风暴(多个从节点同时复制主节点导致主节点压力过大),可以做如下架构,让部分从节点与从节点(与主节点同步)同步数据
在这里插入图片描述

Jedis使用

Jedis集成了redis的一些命令操作,封装了redis的java客户端,提供了连接池管理,类似阿里巴巴的(Druid)德鲁伊。

简单使用,先引入包

<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>3.7.0</version>
    </dependency>

JedisTest

public class JedisTest {
    private JedisPool jedisPool;

    @Before
    public void before(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(20);
        jedisPoolConfig.setMaxIdle(10);
        jedisPoolConfig.setMinIdle(5);
        // timeout,这里既是连接超时又是读写超时,从Jedis 2.8开始有区分connectionTimeout和soTimeout的构造函数
        jedisPool = new JedisPool(jedisPoolConfig, "192.168.200.135", 6379, 3000, null);
    }
    @Test
    public void test1() {
        Jedis jedis = null;
        //******* jedis普通操作示例 ********
        try {
            //从redis连接池里拿出一个连接执行命令
            jedis = jedisPool.getResource();
            //******* jedis普通操作示例 ********
            System.out.println(jedis.set("antry", "antry"));
            System.out.println(jedis.get("antry"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis!=null)
                jedis.close();
        }

    }
}

Redis的管道(Pipeline)

redis命令在从提交到返回处理结果的过程中,消耗的时间我们称之为RTT(往返时间)。
在需要批量执行redis 命令的场景下,如果命令单条逐个执行,那么总共花费的时间是命令条数 N * RTT。
redis 提供了管道技术来提高批量执行效率,即将多个命令打包发送给redis服务端。
需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。
所有命令执行完后,再将所有结果打包返回。
在redis-cli命令行中,使用redis管道技术时,我们通常将待执行的命令放到一个文本里,比如commands.txt ,然后使用命令:

cat commands.txt | redis-cli --pipe 

去读取文本里的命令,然后打包已pipe管道的方式发送给redis服务端。管道中前面命令失败,后面命令
不会有影响,继续执行。
Jedis使用管道技术:

    @Test
    public void testPipeline(){

        //******* 管道示例 ********
        //管道的命令执行方式:cat redis.txt | redis-cli -h 127.0.0.1 -a password - p 6379 --pipe
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            Pipeline pl = jedis.pipelined();
            for (int i = 0; i < 10; i++) {
                pl.incr("pipelineKey");
                pl.set("zhuge" + i, "zhuge");
                //模拟管道报错
                //pl.setbit("zhuge", -1, true);
            }
            List<Object> results = pl.syncAndReturnAll();
            System.out.println(results);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            jedis.close();
        }
    }

Redis Lua脚本

上面的管道操作,即使有一个执行失败也会继续执行下一个任务,因此不是原子操作,而lua是原子操作,Redis会将整个脚本作为一个整体执行。由于redis自带的事务比较鸡肋,所以使用lua实现事务。
Lua是一个高效的轻量级脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

使用lua脚本的测试

    @Test
    public void  testLua(){
        //******* lua脚本示例 ********
        //模拟一个商品减库存的原子操作
        //lua脚本命令执行方式:redis-cli --eval /tmp/test.lua , 10
        Jedis jedis = null;
        //从redis连接池里拿出一个连接执行命令
        try {
            jedis = jedisPool.getResource();
            jedis.set("product_stock_10016", "15");  //初始化商品10016的库存
            String script = " local count = redis.call('get', KEYS[1]) " +
                    " local a = tonumber(count) " +
                    " local b = tonumber(ARGV[1]) " +
                    " if a >= b then " +
                    "   redis.call('set', KEYS[1], a-b) " +
                    //模拟语法报错回滚操作
                    //"   bb == 0 " +
                    "   return 1 " +
                    " end " +
                    " return 0 ";
            Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10"));
            System.out.println(obj);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (jedis!=null)
                jedis.close();
        }
    }

具体的lua使用,可以查看菜鸟教程。稍微看一下能懂的,就是突然一看有点迷糊很正常。

Redis哨兵高可用架构

sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。有点类似rocke的namespace,哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)
在这里插入图片描述

搭建

目录下有个sentinel.conf文件,复制三份,分别命名sentinel‐26379.conf,sentinel‐26380.conf,sentinel‐26381.conf
修改配置

port 26379 # 端口号
daemonize yes
# quorum是一个数字,指明当有多少个sentinel认为一个master失效时(值一般为:sentinel总数/2 +1),master才算真正失效
sentinel monitor mymaster 192.*.*.*(你的redis master ip) 6379 2 # mymaster这个名字随便取,客户端访问时会用到

启动

src/redis-sentinel sentinel-26379.conf
src/redis-sentinel sentinel-26380.conf
src/redis-sentinel sentinel-26381.conf

启动完之后在sentinel的配置文件末尾可以看到新增了几行

protected-mode no
sentinel known-replica mymaster 192.168.200.135 6381
sentinel known-replica mymaster 192.168.200.135 6380
sentinel known-sentinel mymaster 192.168.200.135 26381 bc0e7eb8c645e8f8be0a61e528e6d218cdc30722
sentinel known-sentinel mymaster 192.168.200.135 26380 f631c92ac7311f6ca99c89be80b46f7a0b0f112e
sentinel current-epoch 2
  • sentinel known-replica mymaster 192.168.200.135 6381是slave的信息

  • sentinel known-sentinel mymaster 192.168.200.135 26381
    bc0e7eb8c645e8f8be0a61e528e6d218cdc30722是其他哨兵的信息

如果你没有其他哨兵信息,那么可能是你在启动完一个哨兵之后复制的sentinel.conf的配置文件,是因为myid已经生产没删掉造成的。所以要在没启动之前复制配置文件,或者删掉myid就行了
如果是从节点信息没有,那就是你的从节点配置有问题,没有上线,检查下端口之类的

假设master挂了

把master的进程的进程kill之后

[root@localhost redis-5.0.13]# ps -ef | grep redis
root      61438      1  0 21:04 ?        00:00:05 src/redis-sentinel *:26379 [sentinel]
root      61457      1  0 21:04 ?        00:00:05 src/redis-sentinel *:26380 [sentinel]
root      61467      1  0 21:04 ?        00:00:05 src/redis-sentinel *:26381 [sentinel]
root      61565  42435  0 21:09 pts/2    00:00:00 src/redis-cli
root      61566  59731  0 21:09 pts/3    00:00:00 src/redis-cli -p 6380
root      61603  60091  0 21:10 pts/4    00:00:00 src/redis-cli -p 6381
root      61803      1  0 21:19 ?        00:00:02 src/redis-server 0.0.0.0:6379
root      62048      1  0 21:28 ?        00:00:01 src/redis-server 0.0.0.0:6381
root      62191      1  0 21:30 ?        00:00:01 src/redis-server 0.0.0.0:6380
root      62401  42366  0 21:48 pts/0    00:00:00 grep --color=auto redis
[root@localhost redis-5.0.13]# kill -9 61803

一开始查看连个从节点的info角色还是slave

# Replication
role:slave
master_host:192.168.200.135
master_port:6379
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:265023
master_link_down_since_seconds:5
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:97ebdca2e26494047dd88a5b3c8b741c324729b7
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:265023
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:265023

过了一会儿,其中有一台slave会被选举成新的master

# Replication
role:master
connected_slaves:1
slave0:ip=192.168.200.135,port=6381,state=online,offset=265626,lag=0
master_replid:1dde684c1e2bd3f8f8b264c4d51ea567f4561df4
master_replid2:97ebdca2e26494047dd88a5b3c8b741c324729b7
master_repl_offset:265916
second_repl_offset:265024
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:265916

此时把挂掉的master重启,一开始这台机器的状态会显示是master,但随后会很快变成slave

# Replication
role:master
connected_slaves:1
slave0:ip=192.168.200.135,port=6381,state=online,offset=265626,lag=0
master_replid:1dde684c1e2bd3f8f8b264c4d51ea567f4561df4
master_replid2:97ebdca2e26494047dd88a5b3c8b741c324729b7
master_repl_offset:265916
second_repl_offset:265024
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:265916

Jedis使用通过哨兵获取信息连接

public class JedisSentinelTest {
    public static void main(String[] args) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(20);
        config.setMaxIdle(10);
        config.setMinIdle(5);
        String masterName = "mymaster";
        Set<String> sentinels = new HashSet<String>();
        sentinels.add(new HostAndPort("192.168.200.135",26379).toString());
        sentinels.add(new HostAndPort("192.168.200.135",26380).toString());
        sentinels.add(new HostAndPort("192.168.200.135",26381).toString());
        //JedisSentinelPool其实本质跟JedisPool类似,都是与redis主节点建立的连接池
        //JedisSentinelPool并不是说与sentinel建立的连接池,而是通过sentinel发现redis主节点并与其建立连接
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, config, 3000, null);
        Jedis jedis = null;
        try {
            jedis = jedisSentinelPool.getResource();
            System.out.println(jedis.set("sentinel", "antry"));
            System.out.println(jedis.get("sentinel"));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。
            if (jedis != null)
                jedis.close();
        }
    }
}

哨兵的Spring Boot整合Redis连接

pom

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath /> <!-- lookup parent from repository -->
  </parent>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>

    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

  </dependencies>

yml

server:
  port: 8080
spring:
  redis:
    database: 0
    connect-timeout: 3000
    sentinel:
      master: mymaster
      nodes: 192.168.200.135:26379,192.168.200.135:26380,192.168.200.135:26381
    lettuce:
      pool:
        max-idle: 50
        min-idle: 10
        max-active: 100
        max-wait: 1000

Controller

@RestController
public class IndexController {
    public static final Logger logger = LoggerFactory.getLogger(IndexController.class);
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 测试节点挂了哨兵重新选举新的master节点,客户端是否能动态感知到
     * 新的master选举出来后,哨兵会把消息发布出去,客户端实际上是实现了一个消息监听机制,
     * 当哨兵把新master的消息发布出去,客户端会立马感知到新master的信息,从而动态切换访问的masterip
     */
    @RequestMapping("/test_sentinel")
    public void testSentinel()throws InterruptedException {
        int i = 1;
         while (true){
             try {
                 stringRedisTemplate.opsForValue().set("antry"+i, i+"");
                 System.out.println("设置key:"+ "antry" + i);
                 i++;
                 Thread.sleep(1000);
                 }catch (Exception e){
                 logger.error("错误:", e);
                 }
             }
    }
}

启动后访问http://127.0.0.1:8080/test_sentinel

设置key:antry1
设置key:antry2
设置key:antry3
设置key:antry4
设置key:antry5
设置key:antry6
设置key:antry7

关闭master

设置key:antry37
设置key:antry38
2021-09-15 13:49:33.712  INFO 136712 --- [xecutorLoop-1-8] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was /192.168.200.135:6380
2021-09-15 13:49:35.735  WARN 136712 --- [ioEventLoop-4-4] i.l.core.protocol.ConnectionWatchdog     : Cannot reconnect to [192.168.200.135:6380]: Connection refused: no further information: /192.168.200.135:6380
2021-09-15 13:49:40.011  INFO 136712 --- [ecutorLoop-1-15] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was 192.168.200.135:6380
2021-09-15 13:49:42.029  WARN 136712 --- [oEventLoop-4-10] i.l.core.protocol.ConnectionWatchdog     : Cannot reconnect to [192.168.200.135:6380]: Connection refused: no further information: /192.168.200.135:6380
2021-09-15 13:49:46.313  INFO 136712 --- [xecutorLoop-1-5] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was 192.168.200.135:6380
2021-09-15 13:49:48.331  WARN 136712 --- [oEventLoop-4-16] i.l.core.protocol.ConnectionWatchdog     : Cannot reconnect to [192.168.200.135:6380]: Connection refused: no further information: /192.168.200.135:6380
2021-09-15 13:49:53.410  INFO 136712 --- [ecutorLoop-1-11] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was 192.168.200.135:6380
2021-09-15 13:49:55.421  WARN 136712 --- [ioEventLoop-4-6] i.l.core.protocol.ConnectionWatchdog     : Cannot reconnect to [192.168.200.135:6380]: Connection refused: no further information: /192.168.200.135:6380
2021-09-15 13:50:00.611  INFO 136712 --- [ecutorLoop-1-15] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was 192.168.200.135:6380
2021-09-15 13:50:02.632  WARN 136712 --- [oEventLoop-4-10] i.l.core.protocol.ConnectionWatchdog     : Cannot reconnect to [192.168.200.135:6380]: Connection refused: no further information: /192.168.200.135:6380
2021-09-15 13:50:06.811  INFO 136712 --- [xecutorLoop-1-1] i.l.core.protocol.ConnectionWatchdog     : Reconnecting, last destination was 192.168.200.135:6380
2021-09-15 13:50:06.817  INFO 136712 --- [oEventLoop-4-12] i.l.core.protocol.ReconnectionHandler    : Reconnected to 192.168.200.135:6381
设置key:antry39
设置key:antry40
设置key:antry41
设置key:antry42
设置key:antry43

Redis客户端命令对应的RedisTemplate中的方法列表:

String结构

redis客户端命令redisTemplate方法
RedisRedisTemplate rt
set key valuert.opsForValue().set(“key”,“value”)
get keyrt.opsForValue().get(“key”)
del keyrt.delete(“key”)
strlen keyrt.opsForValue().size(“key”)
getset key valuert.opsForValue().getAndSet(“key”,“value”)
getrange key startend rt.opsForValue().get(“key”,start,end)
append key valuert.opsForValue().append(“key”,“value”)

Hash结构

redis客户端命令redisTemplate方法
hmset key field1 value1 field2 value2…rt.opsForHash().putAll(“key”,map) //map是一个集合对象
hset key field valuert.opsForHash().put(“key”,“field”,“value”)
hexists key fieldrt.opsForHash().hasKey(“key”,“field”)
hgetall keyrt.opsForHash().entries(“key”) //返回Map对象
hvals keyrt.opsForHash().values(“key”) //返回List对象
hkeys keyrt.opsForHash().keys(“key”) //返回List对象
hmget key field1 field2…rt.opsForHash().multiGet(“key”,keyList)
hsetnx key field valuert.opsForHash().putIfAbsent(“key”,“field”,“value”
hdel key field1 field2rt.opsForHash().delete(“key”,“field1”,“field2”)
hget key fieldrt.opsForHash().get(“key”,“field”)

List结构

redis客户端命令redisTemplate方法
lpush list node1 node2 node3…rt.opsForList().leftPush(“list”,“node”)
lpush list node1 node2 node3…rt.opsForList().leftPushAll(“list”,list) //list是集合对象
rpush list node1 node2 node3…rt.opsForList().rightPush(“list”,“node”)
lpush list node1 node2 node3…rt.opsForList().rightPushAll(“list”,list) //list是集合对象
lindex key indexrt.opsForList().index(“list”, index)
llen keyrt.opsForList().size(“key”)
lpop keyrt.opsForList().leftPop(“key”)
rpop keyrt.opsForList().rightPop(“key”)
lpushx list nodert.opsForList().leftPushIfPresent(“list”,“node”)
rpushx list nodert.opsForList().rightPushIfPresent(“list”,“node”)
lrange list start endrt.opsForList().range(“list”,start,end)
lrem list count valuert.opsForList().remove(“list”,count,“value”)
lset key index valuert.opsForList().set(“list”,index,“value”)

Set结构

redis客户端命令redisTemplate方法
sadd key member1 member2…rt.boundSetOps(“key”).add(“member1”,“member2”,…)
sadd key member1 member2…rt.opsForSet().add(“key”, set) //set是一个集合对象
scard keyrt.opsForSet().size(“key”)
sidff key1 key2rt.opsForSet().difference(“key1”,“key2”) //返回一个集合对象
sinter key1 key2rt.opsForSet().intersect(“key1”,“key2”)//同上
sunion key1 key2rt.opsForSet().union(“key1”,“key2”)//同上
sdiffstore des key1 key2rt.opsForSet().differenceAndStore(“key1”,“key2”,“des”)
sinter des key1 key2rt.opsForSet().intersectAndStore(“key1”,“key2”,“des”)
sunionstore des key1 key2rt.opsForSet().unionAndStore(“key1”,“key2”,“des”)
sismember key memberrt.opsForSet().isMember(“key”,“member”)
smembers keyrt.opsForSet().members(“key”)
spop keyrt.opsForSet().pop(“key”)
srandmember key countrt.opsForSet().randomMember(“key”,count)
srem key member1 member2…rt.opsForSet().remove(“key”,“member1”,“member2”,…)

点击全文阅读


本文链接:http://m.zhangshiyu.com/post/30485.html

节点  命令  哨兵  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1